Spring WebFlux配置

spring webflux在配置方面相对于以前的spring mvc有了比较大的区别,但基本上都能在官方文档中找到:spring webflux、spring boot、spring 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 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 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 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 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 {
# 自定义Writer
public class CustomMessageWriter extends EncoderHttpMessageWriter {


@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> getWritersInternal(boolean forMultipart) {
        List> 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配置)