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
不过这边有几个需要注意的地方: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核心原理吧。