Spring WebFlux配置

spring webflux在配置方面相對(duì)于以前的spring mvc有了比較大的區(qū)別,但基本上都能在官方文檔中找到:spring webflux、spring boot、spring boot gradle plugin,在文檔中搜索關(guān)鍵字或者直接google基本上都能解決配置方面的問(wèn)題,這邊主要是記錄筆者在項(xiàng)目實(shí)踐過(guò)程中的一些問(wèn)題,希望對(duì)大家有所幫助

項(xiàng)目創(chuàng)建

筆者這邊用的是intellij idea提供的spring initializer創(chuàng)建的gradle項(xiàng)目,項(xiàng)目地址:spring-webflux-demo,基本上是一鍵配置,中間過(guò)程記得選配webflux、lombok、gradle項(xiàng)目即可,由于項(xiàng)目創(chuàng)建過(guò)程中需要從mavenCentral和spring.io拉包,注意需要翻墻或者修改repositories配置,項(xiàng)目創(chuàng)建完成直接運(yùn)行SpringWebfluxApplication類(lèi)即可在本機(jī)啟動(dòng)NettyServer。
lombok的使用需要下載lombok插件以及打開(kāi)“Enable Annotation Processing”設(shè)置,不然部分依賴(lài)注解注入的代碼會(huì)飄紅,相信我,使用lombok之后你再也不想回去以前那種刀耕火種的原始編程方式了~

不同環(huán)境配置

spring boot提供了profile的配置以便實(shí)現(xiàn)不同環(huán)境的不同配置,intellij中可以在configuration面板簡(jiǎn)單添加Active Profile配置,生產(chǎn)部署時(shí)可以使用jar your.jar --spring.profiles.active=dev,pro,spring默認(rèn)加載的是resource/application.properties,當(dāng)指定spring.profiles.active時(shí),會(huì)同時(shí)加載application-profile.properties,后面的文件配置會(huì)覆蓋前面的文件配置。

讀取配置文件值

使用@Value能夠很簡(jiǎn)單的獲取配置文件中的取值,當(dāng)然前提是@Value所在的類(lèi)會(huì)被自動(dòng)注入

# 第一個(gè)冒號(hào)之后的值會(huì)被當(dāng)作默認(rèn)值處理,沒(méi)有默認(rèn)值的屬性必須在配置文件中配置,否則會(huì)導(dǎo)致應(yīng)用啟動(dòng)報(bào)錯(cuò)
    /**
     * 讀取自定義配置
     */
    @Value("${custom.dev:hhh:默認(rèn)值}")
    private String dev;

Configuration

@Configuration+@Bean的配置能夠很方便的實(shí)現(xiàn)子庫(kù)的動(dòng)態(tài)注入,再結(jié)合@Import注解,又能夠?qū)崿F(xiàn)Configuration之間的靈活組合。

# 子庫(kù)中的配置
@Configuration
@Slf4j
public class LibConfig {

    @Bean
    void testConfigImport(){
        log.info("lib config inject success");
    }
}

# 啟動(dòng)類(lèi)的配置使用import將子庫(kù)的配置注入
@SpringBootApplication(scanBasePackages = "com.hzy.spring.springwebflux")
@Import(LibConfig.class)
public class SpringWebfluxDemoConfig {

Condition實(shí)現(xiàn)配置的參數(shù)化注入(如mock等)

開(kāi)發(fā)過(guò)程中可能有很多的配置和環(huán)境相關(guān),最常見(jiàn)的就是mock只需要在dev環(huán)境才能開(kāi)啟,結(jié)合不同環(huán)境的配置文件+@Conditional就能夠很簡(jiǎn)單的實(shí)現(xiàn)不同環(huán)境下的不同配置注入

# 先定義一個(gè)Condition接受文件配置
public class CustomCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String match = context.getEnvironment().getProperty("custom.condition","false");
        return Boolean.valueOf(match);
    }
}

# 在Configuration處加上Conditional注解即可,這樣只有當(dāng)CustomCondition返回true時(shí),該Configuration才會(huì)被自動(dòng)注入
@Configuration
@Slf4j
@Conditional(CustomCondition.class)
public class LibConfig {

Bean注入的最佳實(shí)踐

spring boot啟動(dòng)時(shí)會(huì)自動(dòng)掃描@SpringBootApplication注解所在的package,把所有相關(guān)的類(lèi)都自動(dòng)注入,可以通過(guò)scanPackage配置掃描的package。結(jié)合上面關(guān)于Configuration和Conditional的描述,最佳的方式就是把scanPackage配置到比較明確的項(xiàng)目package,然后結(jié)合Configuration、Conditional實(shí)現(xiàn)其他類(lèi)庫(kù)Bean的組合注入,這樣就不需要因?yàn)橐胍粋€(gè)兩個(gè)類(lèi)而把整個(gè)類(lèi)庫(kù)都注入了,這同時(shí)也需要我們?cè)谠O(shè)計(jì)基礎(chǔ)類(lèi)庫(kù)的時(shí)候考慮類(lèi)庫(kù)功能組件的細(xì)分,而不是只暴露一個(gè)大而全的bean配置。

統(tǒng)一異常處理

統(tǒng)一異常處理有兩個(gè)部分,一個(gè)是controller部分的異常處理,配置方式如下:

@RestControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(Exception.class)
    public String convertExceptionMsg(Exception e){
        //自定義邏輯,可返回其他值
        return "error";
    }

    @ExceptionHandler(IllegalAccessException.class)
    public Mono<String> convertIllegalAccessError(Exception e){
        //自定義邏輯,可返回其他值
        return Mono.just("illegal access");
    }
}

還有一部分是通過(guò)RouterFunctions.route()配置的路由分發(fā),這部分的異常并不會(huì)走到@ExceptionHandler注解的方法中,需要在route配置的時(shí)候加上相應(yīng)的異常處理。

RouterFunctions.route(RequestPredicates.POST("/auth"),your handler).filter((request, next) -> next.handle(request)
                .onErrorResume(Exception.class, e -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).syncBody(your exception body)))

ContextPath問(wèn)題

spring mvc有ContextPath的配置選項(xiàng),webflux因?yàn)闆](méi)有DispatchServlet,已經(jīng)不支持ContextPath了,一般來(lái)說(shuō)都是在nginx統(tǒng)一配置路徑轉(zhuǎn)發(fā)就好了。本地調(diào)試時(shí)可能就需要稍微注意下了,要么本地也裝個(gè)nginx和線上環(huán)境保持一致,要么就做差異化配置,還有種方法,通過(guò)WebFilter的方式做一層ContextPath的轉(zhuǎn)發(fā),不過(guò)有一定風(fēng)險(xiǎn),不推薦使用。

@Component //所有/contextPath前綴的請(qǐng)求都會(huì)自動(dòng)去除該前綴
public class ContextPathFilter implements WebFilter {


    @Autowired
    private ServerProperties serverProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String contextPath = serverProperties.getServlet().getContextPath();
        String requestPath = exchange.getRequest().getPath().pathWithinApplication().value();
        if(contextPath != null && requestPath.startsWith(contextPath)){
            requestPath = requestPath.substring(contextPath.length());
        }
        return chain.filter(exchange.mutate().request(exchange.getRequest().mutate().path(requestPath).build()).build());
    }
}

跨域配置

webflux跨域配置有兩種方式:一種是復(fù)寫(xiě)WebFluxConfigurer#addCorsMappings,另一種是配置自定義的CorsWebFilter,兩種方式都有一定局限,CorsRegistry的方式無(wú)法實(shí)現(xiàn)RouteFunctions配置的路由跨域,而CorsWebFilter的方式只是單純的攔截請(qǐng)求,其他框架層的代碼無(wú)法讀取到跨域的配置,比如說(shuō)RequestMappingHandlerMapping#getHandler時(shí)就無(wú)法讀取到跨域配置,可以考慮兩者都配置。

@Configuration
public class CustomWebFluxConfig implements WebFluxConfigurer {

    /**
     * 全局跨域配置,根據(jù)各自需求定義
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedOrigins("*")
                .allowedHeaders("*")
                .allowedMethods("*")
                .exposedHeaders(HttpHeaders.SET_COOKIE);
    }

    /**
     * 也可以繼承CorsWebFilter使用@Component注解,效果是一樣的
     * @return
     */
    @Bean
    CorsWebFilter corsWebFilter(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addExposedHeader(HttpHeaders.SET_COOKIE);
        CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        ((UrlBasedCorsConfigurationSource) corsConfigurationSource).registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(corsConfigurationSource);
    }
}

interceptor實(shí)現(xiàn),攔截HandlerMethod

webflux已經(jīng)沒(méi)有了Interceptor的概念,但是可以通過(guò)WebFilter的方式實(shí)現(xiàn)

@Component
public class CustomWebFilter implements WebFilter {

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        Object handlerMethod = requestMappingHandlerMapping.getHandler(exchange).toProcessor().peek();
        //注意跨域時(shí)的配置,跨域時(shí)瀏覽器會(huì)先發(fā)送一個(gè)option請(qǐng)求,這時(shí)候getHandler不會(huì)時(shí)真正的HandlerMethod
        if(handlerMethod instanceof HandlerMethod){
            Valid valid = ((HandlerMethod) handlerMethod).getMethodAnnotation(Valid.class);
            //do your logic
        }
        //preprocess()
        Mono<Void> response = chain.filter(exchange);
        //postprocess()
        return response;
    }
}

HttpMessageReader/Writer

有時(shí)可能需要統(tǒng)一攔截Request/Response對(duì)象,webflux中可以通過(guò)HttpMessageReader/Writer來(lái)實(shí)現(xiàn),重寫(xiě)WebFluxConfigurer#configureHttpMessageCodecs方法,通過(guò)ServerCodecConfigurer注冊(cè)自定義的Reader/Writer即可

# 自定義Reader
public class CustomMessageReader extends DecoderHttpMessageReader<Object> {
# 自定義Writer
public class CustomMessageWriter extends EncoderHttpMessageWriter<Object> {


@Configuration
public class CustomWebFluxConfig implements WebFluxConfigurer {
   @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.customCodecs().reader(new CustomMessageReader());
        configurer.customCodecs().writer(new CustomMessageWriter());

        //由于AutoConfigure會(huì)自動(dòng)覆蓋jackson2JsonEncoder/Decoder,此配置無(wú)法生效
        //configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder());
        //configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder());
    }

不過(guò)這邊有幾個(gè)需要注意的地方:1、按照官方文檔,其實(shí)可以通過(guò)包裝Encoder/Decoder的方式實(shí)現(xiàn),但是實(shí)踐中發(fā)現(xiàn)這種配置方式會(huì)被默認(rèn)配置覆蓋,無(wú)法生效
2、customCodecs新增的Reader/Writer總是排在默認(rèn)的Reader/Writer的后面,所以在默認(rèn)的列表中已有的處理器會(huì)優(yōu)先執(zhí)行。根據(jù)規(guī)則Reader/Writer分兩種類(lèi)型,一種是Typed,只能解析具體類(lèi)型的數(shù)據(jù),一種是Object,能夠執(zhí)行多種類(lèi)型的數(shù)據(jù)。所以,自定義的Reader/Writer要么是默認(rèn)列表中沒(méi)有的具體類(lèi)型解析器,要么只能關(guān)閉默認(rèn)列表(不建議關(guān)閉,除非你能夠自定義接收所有可能數(shù)據(jù)類(lèi)型的Reader/Writer)。
除此之外,還能夠采用一直取巧的方式:添加一個(gè)可以解析Object類(lèi)型的Reader/Writer,然后復(fù)寫(xiě)canRead/canWrite方法使系統(tǒng)認(rèn)為是Typed類(lèi)型的Reader/Writer,這樣就能在默認(rèn)的Object解析器之前執(zhí)行了,具體代碼見(jiàn)demo

# BaseCodecConfigurer類(lèi)
    protected List<HttpMessageWriter<?>> getWritersInternal(boolean forMultipart) {
        List<HttpMessageWriter<?>> result = new ArrayList<>();

        result.addAll(this.defaultCodecs.getTypedWriters(forMultipart));
        result.addAll(this.customCodecs.getTypedWriters());

        result.addAll(this.defaultCodecs.getObjectWriters(forMultipart));
        result.addAll(this.customCodecs.getObjectWriters());

        result.addAll(this.defaultCodecs.getCatchAllWriters());
        return result;
    }

疑難雜癥

gradle項(xiàng)目有時(shí)需要小心依賴(lài)更新不及時(shí)的問(wèn)題,實(shí)踐過(guò)程中曾碰到自己庫(kù)里面的class introspect failed的問(wèn)題,google都是說(shuō)第三方庫(kù)compile配置問(wèn)題,結(jié)果最后發(fā)現(xiàn)是自己的api更新了但是gradle沒(méi)有拉下來(lái)導(dǎo)致的,清空gradle的緩存重新拉一下就好了。
本地調(diào)試時(shí),如果是子模塊項(xiàng)目,需要注意路徑設(shè)置的問(wèn)題,可能導(dǎo)致無(wú)法加載到資源

以上就是項(xiàng)目過(guò)程中遇到的一些配置問(wèn)題,配置只是皮毛,看一遍大家都會(huì),webflux的核心還是要把里面的響應(yīng)式編程、對(duì)異步的支持給吃透,前路漫漫其修遠(yuǎn)兮,希望后面能有機(jī)會(huì)繼續(xù)總結(jié)webflux核心原理吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容