Spring WebFlux配置实践

作者: 小智pikapika | 来源:发表于2018-12-08 19:55 被阅读16次

    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配置。

    统一异常处理

    @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");
        }
    }
    
    

    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核心原理吧。

    相关文章

      网友评论

        本文标题:Spring WebFlux配置实践

        本文链接:https://www.haomeiwen.com/subject/fujmqqtx.html