Spring WebFlux配置

spring webflux在配置方面相对于以前的spring mvc有了比较大的区别,但基本上都能在官方文档中找到:spring webfluxspring bootspring boot gradle plugin,在文档中搜索关键字或者直接google基本上都能解决配置方面的问题,这边主要是记录笔者在项目实践过程中的一些问题,希望对大家有所帮助

项目创建

笔者这边用的是intellij idea提供的spring initializer创建的gradle项目,项目地址:spring-webflux-demo,基本上是一键配置,中间过程记得选配webflux、lombok、gradle项目即可,由于项目创建过程中需要从mavenCentral和spring.io拉包,注意需要翻墙或者修改repositories配置,项目创建完成直接运行SpringWebfluxApplication类即可在本机启动NettyServer。
lombok的使用需要下载lombok插件以及打开“Enable Annotation Processing”设置,不然部分依赖注解注入的代码会飘红,相信我,使用lombok之后你再也不想回去以前那种刀耕火种的原始编程方式了~

不同环境配置

spring boot提供了profile的配置以便实现不同环境的不同配置,intellij中可以在configuration面板简单添加Active Profile配置,生产部署时可以使用jar your.jar --spring.profiles.active=dev,pro,spring默认加载的是resource/application.properties,当指定spring.profiles.active时,会同时加载application-profile.properties,后面的文件配置会覆盖前面的文件配置。

读取配置文件值

使用@Value能够很简单的获取配置文件中的取值,当然前提是@Value所在的类会被自动注入

# 第一个冒号之后的值会被当作默认值处理,没有默认值的属性必须在配置文件中配置,否则会导致应用启动报错
    /**
     * 读取自定义配置
     */
    @Value("${custom.dev:hhh:默认值}")
    private String dev;

Configuration

@Configuration+@Bean的配置能够很方便的实现子库的动态注入,再结合@Import注解,又能够实现Configuration之间的灵活组合。

# 子库中的配置
@Configuration
@Slf4j
public class LibConfig {

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

# 启动类的配置使用import将子库的配置注入
@SpringBootApplication(scanBasePackages = "com.hzy.spring.springwebflux")
@Import(LibConfig.class)
public class SpringWebfluxDemoConfig {

Condition实现配置的参数化注入(如mock等)

开发过程中可能有很多的配置和环境相关,最常见的就是mock只需要在dev环境才能开启,结合不同环境的配置文件+@Conditional就能够很简单的实现不同环境下的不同配置注入

# 先定义一个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注解即可,这样只有当CustomCondition返回true时,该Configuration才会被自动注入
@Configuration
@Slf4j
@Conditional(CustomCondition.class)
public class LibConfig {

Bean注入的最佳实践

spring boot启动时会自动扫描@SpringBootApplication注解所在的package,把所有相关的类都自动注入,可以通过scanPackage配置扫描的package。结合上面关于Configuration和Conditional的描述,最佳的方式就是把scanPackage配置到比较明确的项目package,然后结合Configuration、Conditional实现其他类库Bean的组合注入,这样就不需要因为引入一个两个类而把整个类库都注入了,这同时也需要我们在设计基础类库的时候考虑类库功能组件的细分,而不是只暴露一个大而全的bean配置。

统一异常处理

统一异常处理有两个部分,一个是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");
    }
}

还有一部分是通过RouterFunctions.route()配置的路由分发,这部分的异常并不会走到@ExceptionHandler注解的方法中,需要在route配置的时候加上相应的异常处理。

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问题

spring mvc有ContextPath的配置选项,webflux因为没有DispatchServlet,已经不支持ContextPath了,一般来说都是在nginx统一配置路径转发就好了。本地调试时可能就需要稍微注意下了,要么本地也装个nginx和线上环境保持一致,要么就做差异化配置,还有种方法,通过WebFilter的方式做一层ContextPath的转发,不过有一定风险,不推荐使用。

@Component //所有/contextPath前缀的请求都会自动去除该前缀
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跨域配置有两种方式:一种是复写WebFluxConfigurer#addCorsMappings,另一种是配置自定义的CorsWebFilter,两种方式都有一定局限,CorsRegistry的方式无法实现RouteFunctions配置的路由跨域,而CorsWebFilter的方式只是单纯的拦截请求,其他框架层的代码无法读取到跨域的配置,比如说RequestMappingHandlerMapping#getHandler时就无法读取到跨域配置,可以考虑两者都配置。

@Configuration
public class CustomWebFluxConfig implements WebFluxConfigurer {

    /**
     * 全局跨域配置,根据各自需求定义
     * @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实现,拦截HandlerMethod

webflux已经没有了Interceptor的概念,但是可以通过WebFilter的方式实现

@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();
        //注意跨域时的配置,跨域时浏览器会先发送一个option请求,这时候getHandler不会时真正的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

有时可能需要统一拦截Request/Response对象,webflux中可以通过HttpMessageReader/Writer来实现,重写WebFluxConfigurer#configureHttpMessageCodecs方法,通过ServerCodecConfigurer注册自定义的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会自动覆盖jackson2JsonEncoder/Decoder,此配置无法生效
        //configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder());
        //configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder());
    }

不过这边有几个需要注意的地方:1、按照官方文档,其实可以通过包装Encoder/Decoder的方式实现,但是实践中发现这种配置方式会被默认配置覆盖,无法生效
2、customCodecs新增的Reader/Writer总是排在默认的Reader/Writer的后面,所以在默认的列表中已有的处理器会优先执行。根据规则Reader/Writer分两种类型,一种是Typed,只能解析具体类型的数据,一种是Object,能够执行多种类型的数据。所以,自定义的Reader/Writer要么是默认列表中没有的具体类型解析器,要么只能关闭默认列表(不建议关闭,除非你能够自定义接收所有可能数据类型的Reader/Writer)。
除此之外,还能够采用一直取巧的方式:添加一个可以解析Object类型的Reader/Writer,然后复写canRead/canWrite方法使系统认为是Typed类型的Reader/Writer,这样就能在默认的Object解析器之前执行了,具体代码见demo

# BaseCodecConfigurer类
    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项目有时需要小心依赖更新不及时的问题,实践过程中曾碰到自己库里面的class introspect failed的问题,google都是说第三方库compile配置问题,结果最后发现是自己的api更新了但是gradle没有拉下来导致的,清空gradle的缓存重新拉一下就好了。
本地调试时,如果是子模块项目,需要注意路径设置的问题,可能导致无法加载到资源

以上就是项目过程中遇到的一些配置问题,配置只是皮毛,看一遍大家都会,webflux的核心还是要把里面的响应式编程、对异步的支持给吃透,前路漫漫其修远兮,希望后面能有机会继续总结webflux核心原理吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容