webmvc和webflux作为spring framework的两个重要模块,代表了两个IO模型,阻塞式和非阻塞式的。
webmvc是基于servlet的阻塞式模型(一般称为oio),一个请求到达服务器后会单独分配一个线程去处理请求,如果请求包含IO操作,线程在IO操作结束之前一直处于阻塞等待状态,这样线程在等待IO操作结束的时间就浪费了。
webflux是基于reactor的非阻塞模型(一般称为nio),同样,请求到达服务器后也会分配一个线程去处理请求,如果请求包含IO操作,线程在IO操作结束之前不再是处于阻塞等待状态,而是去处理其他事情,等到IO操作结束之后,再通知(得益于系统的机制)线程继续处理请求。
这样线程就有效地利用了IO操作所消耗的时间。
本文就webmvc和webflux两个框架做一个对比。有兴趣的可以看看官方文档做原始参考web servlet和web reactive。
服务器配置
webmvc
@Configuration
@EnableWebMvc //使用注解@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer { //继承WebMvcConfigurer
//配置静态资源
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
registry.addResourceHandler("/file/**")
.addResourceLocations("file:" + System.getProperty("user.dir") + File.separator + "file" + File.separator);
registry.addResourceHandler("/swagger-ui.html**")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
//配置拦截器
//配置编解码
...
}
webflux
@Configuration
@EnableWebFlux //使用注解@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer { //继承WebFluxConfigurer
//配置静态资源
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
registry.addResourceHandler("/file/**")
.addResourceLocations("file:" + System.getProperty("user.dir") + File.separator + "file" + File.separator);
registry.addResourceHandler("/swagger-ui.html**")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
//配置拦截器
//配置编解码
...
}
认证配置
webmvc-验证依赖于用户数据服务,需定义实现UserDetailsService的Bean
@Configuration
@EnableWebSecurity
public class WebMvcSecurityConfig extends WebSecurityConfigurerAdapter implements
AuthenticationEntryPoint, //未验证回调
AuthenticationSuccessHandler, //验证成功回调
AuthenticationFailureHandler, //验证失败回调
LogoutSuccessHandler { //登出成功回调
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
sendJson(response, new Response<>(HttpStatus.UNAUTHORIZED.value(), "Unauthorized"));
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
sendJson(response, new Response<>(1, "Incorrect"));
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
sendJson(response, new Response<>(0, authentication.getClass().getSimpleName()));
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
sendJson(response, new Response<>(0, "Success"));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/swagger*/**", "/webjars/**", "/v2/api-docs")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/static/**", "/file/**")
.permitAll()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.logout()
.logoutUrl("/user/logout") //虚拟路径,不是控制器定义的路径
.logoutSuccessHandler(this)
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(this)
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/user/login") //虚拟路径,不是控制器定义的路径
.successForwardUrl("/user/login") //是控制器定义的路径
.failureHandler(this)
.and()
.httpBasic()
.authenticationEntryPoint(this);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService);
}
webflux-验证依赖于用户数据服务,需定义实现ReactiveUserDetailsService的Bean
@Configuration
@EnableWebFluxSecurity //使用注解@EnableWebFluxSecurity
public class WebFluxSecurityConfig implements
WebFilter, //拦截器
ServerLogoutSuccessHandler, //登出成功回调
ServerAuthenticationEntryPoint, //验证入口
ServerAuthenticationFailureHandler, //验证成功回调
ServerAuthenticationSuccessHandler { //验证失败回调
//实现接口的方法
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
//配置webflux的context-path
ServerHttpRequest request = exchange.getRequest();
if (request.getURI().getPath().startsWith(contextPath)) {
exchange = exchange.mutate().request(request.mutate().contextPath(contextPath).build()).build();
}
//把查询参数转移到FormData中,不然验证过滤器(ServerFormLoginAuthenticationConverter)接受不到参数
if (exchange.getRequest().getMethod() == HttpMethod.POST && exchange.getRequest().getQueryParams().size() > 0) {
ServerWebExchange finalExchange = exchange;
ServerWebExchange realExchange = new Decorator(exchange) {
@Override
public Mono<MultiValueMap<String, String>> getFormData() {
return super.getFormData().map(new Function<MultiValueMap<String, String>, MultiValueMap<String, String>>() {
@Override
public MultiValueMap<String, String> apply(MultiValueMap<String, String> stringStringMultiValueMap) {
if (stringStringMultiValueMap.size() == 0) {
return finalExchange.getRequest().getQueryParams();
} else {
return stringStringMultiValueMap;
}
}
});
}
};
return chain.filter(realExchange);
}
return chain.filter(exchange);
}
@Override
public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
return sendJson(webFilterExchange.getExchange(), new Response<>("登出成功"));
}
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
return sendJson(exchange, new Response<>(HttpStatus.UNAUTHORIZED.value(), "未验证"));
}
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
return sendJson(webFilterExchange.getExchange(), new Response<>(1, "验证失败"));
}
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
return webFilterExchange.getChain().filter(
webFilterExchange.getExchange().mutate()
.request(t -> t.method(HttpMethod.POST).path("/user/login")) //转发到自定义控制器
.build()
);
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.addFilterAfter(this, SecurityWebFiltersOrder.FIRST)
.csrf().disable()
.authorizeExchange()
.pathMatchers("/swagger*/**", "/webjars/**", "/v2/api-docs") //swagger
.permitAll()
.and()
.authorizeExchange()
.pathMatchers("/static/**", "/file/**") //静态资源
.permitAll()
.and()
.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.logout() //登出
.logoutUrl("/user/logout")
.logoutSuccessHandler(this)
.and()
.exceptionHandling() //未验证回调
.authenticationEntryPoint(this)
.and()
.formLogin()
.loginPage("/user/login")
.authenticationFailureHandler(this) //验证失败回调
.authenticationSuccessHandler(this) //验证成功回调
.and()
.httpBasic()
.authenticationEntryPoint(this); //basic验证,一般用于移动端
return http.build();
}
}
Session配置
webmvc
@Configuration
@EnableRedisHttpSession //使用注解@EnableRedisHttpSession
public class RedisHttpSessionConfig { //考虑到分布式系统,一般使用redis存储session
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
}
//单点登录使用FindByIndexNameSessionRepository根据用户名查询session,删除其他session即可
Map<String, Session> map = findByIndexNameSessionRepository.findByPrincipalName(name);
webflux
@Configuration
@EnableRedisWebSession(maxInactiveIntervalInSeconds = 60) //使用注解@EnableRedisWebSession ,maxInactiveIntervalInSeconds设置数据过期时间,spring.session.timeout不管用
public class RedisWebSessionConfig { //考虑到分布式系统,一般使用redis存储session
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
return new LettuceConnectionFactory();
}
}
//单点登录使用ReactiveRedisSessionRepository.getSessionRedisOperations().scan方法查询相同用户名的session,删除其他session即可
public Mono<Map<String, String>> findByPrincipalName(String name) {
return reactiveSessionRepository.getSessionRedisOperations().scan(ScanOptions.scanOptions().match(ReactiveRedisSessionRepository.DEFAULT_NAMESPACE + ":sessions:*").build())
.flatMap(new Function<String, Publisher<Tuple2<String, Map.Entry<Object, Object>>>>() {
@Override
public Publisher<Tuple2<String, Map.Entry<Object, Object>>> apply(String s) {
return reactiveSessionRepository.getSessionRedisOperations().opsForHash().entries(s)
.map(new Function<Map.Entry<Object, Object>, Tuple2<String, Map.Entry<Object, Object>>>() {
@Override
public Tuple2<String, Map.Entry<Object, Object>> apply(Map.Entry<Object, Object> objectObjectEntry) {
return Tuples.of(s, objectObjectEntry);
}
});
}
})
.filter(new Predicate<Tuple2<String, Map.Entry<Object, Object>>>() {
@Override
public boolean test(Tuple2<String, Map.Entry<Object, Object>> rule) {
Map.Entry<Object, Object> t = rule.getT2();
String key = "sessionAttr:" + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
if (key.equals(t.getKey())) {
User sci = (User) ((SecurityContextImpl) t.getValue()).getAuthentication().getPrincipal();
return sci.getUsername().equals(name);
}
return false;
}
})
.collectMap(new Function<Tuple2<String, Map.Entry<Object, Object>>, String>() {
@Override
public String apply(Tuple2<String, Map.Entry<Object, Object>> rule) {
return name;
}
}, new Function<Tuple2<String, Map.Entry<Object, Object>>, String>() {
@Override
public String apply(Tuple2<String, Map.Entry<Object, Object>> rule) {
return rule.getT1().replace(ReactiveRedisSessionRepository.DEFAULT_NAMESPACE + ":sessions:", "");
}
});
}
swagger配置(参考文章)
webmvc
//依赖包
implementation 'io.springfox:springfox-swagger2:2.9.2'
implementation 'io.springfox:springfox-swagger-ui:2.9.2'
//配置
@Configuration
@EnableSwagger2 //使用注解
public class SwaggerConfig {
@Bean
public Docket docket () {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("*****")
.description("******")
.version("0.0.1")
.build())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
}
//参数上传
//定义参数bean
@Setter
@Getter
@ToString
@ApiModel
public class QueryBean{
@ApiModelProperty(value = "普通参数", required = false, example = "")
private String query;
@ApiModelProperty(value = "文件参数", required = false, example = "")
private MultipartFile image; //webmvc中使用MultipartFile作为接收文件的类型
}
//定义接口
@ApiOperation("一个接口")
@PostMapping("/path")
//这里需要使用@ApiImplicitParam显示配置【文件参数】才能使swagger界面显示上传文件按钮
@ApiImplicitParams({
@ApiImplicitParam(
paramType = "form", //表单参数
dataType = "__file", //最新版本使用__file表示文件,以前用的是file
name = "image", //和QueryBean里面的【文件参数image】同名
value = "文件") //注释
})
public Mono<Response> bannerAddOrUpdate(QueryBean q) {
}
webflux-要稍微麻烦些,多个依赖
//依赖
implementation 'io.springfox:springfox-swagger2:2.10.5'
implementation 'io.springfox:springfox-swagger-ui:2.10.5'
implementation 'io.springfox:springfox-spring-webflux:2.10.5'
//配置
@Configuration
@EnableSwagger2WebFlux //使用注解
public class SwaggerConfig {
@Bean
public Docket docket() {
Set<String> consumes = new HashSet<>();
consumes.add(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
Set<String> produces = new HashSet<>();
produces.add(MediaType.APPLICATION_JSON_VALUE);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("*****")
.description("*****")
.version("0.0.1")
.build())
.pathMapping("/context-path") //注意webflux没有context-path配置,如果不加这句话的话,接口测试时路径没有前缀
.consumes(consumes)
.produces(produces)
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
}
//参数上传
//定义参数bean
@Setter
@Getter
@ToString
@ApiModel
public class QueryBean{
@ApiModelProperty(value = "普通参数", required = false, example = "")
private String query;
@ApiModelProperty(value = "文件参数", required = false, example = "")
private FilePart image; //强调,webflux中使用FilePart作为接收文件的类型
}
//定义接口
@ApiOperation("一个接口")
@PostMapping("/path")
//这里需要使用@ApiImplicitParam显示配置【文件参数】才能使swagger界面显示上传文件按钮
@ApiImplicitParams({
@ApiImplicitParam(
paramType = "form", //表单参数
dataType = "__file", //最新版本使用__file表示文件,以前用的是file
name = "image", //和QueryBean里面的【文件参数image】同名
value = "文件") //注释
})
public Mono<Response> bannerAddOrUpdate(QueryBean q) {
}
以上就是webmvc和webflux的对比,谢谢大家。