Servlet3.0开始支持Request的异步处理,所谓异步处理就是服务端在收到请求之后,并不是直接开始调用业务代码开始存取数据等耗时操作,而是交给后端的线程池来处理,这样请求接收线程就可以继续接收新进来的请求。等线程池处理完之后,通过AsyncContext回调来返回数据给客户端。
异步请求使用场景:
- 文件上传等耗时请求,可以采用异步处理,不至于因为同时多个人在上传文件而耗光web容器的线程,影响其它快速请求
-
高并发服务器,对于高并发服务器,异步化必然选择,不但请求进来异步处理。在业务线程池调用其它服务的时候也需要异步,防止耗时操作耗光线程数,我们借用一张图来看一下:
Spring MVC异步支持
Spring MVC中已经对异步请求做了封装,只要在controller的方法中返回一个Callable
或者DeferredResult
就可以了,比如下面的例子:
@RestController
public class AsyncRequestController {
@GetMapping("/async")
public Callable<UserDto> doAsync(){
return ()->{
Thread.sleep(5000);
return (UserDto)SecurityUtils.getSubject().getPrincipal();
};
}
}
对于异步请求的线程池初始化,可以在重写WebMvcConfigurationSupport
的configureAsyncSupport()
方法。
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport{
@Override
protected void configureAsyncSupport(AsyncSupportConfigurer configurer) {
//设置一个3个线程的线程池来处理异步请求
configurer.setTaskExecutor(new ConcurrentTaskExecutor(Executors.newFixedThreadPool(3)));
//异步请求处理超时为30秒
configurer.setDefaultTimeout(30000);
}
}
Shiro针对异步请求的配置
还是以上次的shiro实现jwt认证授权的项目为例(传送门:https://www.jianshu.com/p/0b1131be7ace),如果我们按照原来的配置,请求上面的异步controller会出现下面的错误:
2018-09-24 00:26:31.659 [ERROR][http-nio-8080-exec-2]:o.a.c.c.C.[.[localhost].[/].[dispatcherServlet] [log:182] Servlet.service() for servlet [dispatcherServlet] threw exception
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123)
at org.apache.shiro.subject.Subject$Builder.<init>(Subject.java:626)
at org.apache.shiro.SecurityUtils.getSubject(SecurityUtils.java:56)
at org.apache.shiro.web.servlet.ShiroHttpServletRequest.getSubject(ShiroHttpServletRequest.java:89)
at org.apache.shiro.web.servlet.ShiroHttpServletRequest.getSubjectPrincipal(ShiroHttpServletRequest.java:94)
at org.apache.shiro.web.servlet.ShiroHttpServletRequest.getUserPrincipal(ShiroHttpServletRequest.java:112)
at org.springframework.web.servlet.FrameworkServlet.getUsernameForRequest(FrameworkServlet.java:1093)
at org.springframework.web.servlet.FrameworkServlet.publishRequestHandledEvent(FrameworkServlet.java:1078)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728)
at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:649)
at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:612)
at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:567)
at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:353)
上面错误的原因是我们的shiro filter没有对AsyncContext中的Request没有做拦截,造成SecurityManager为空。解决这个问题,只要在注册shiro Filter的地方做如下配置:
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean(SecurityManager securityManager,UserService userService) throws Exception{
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<Filter>();
filterRegistration.setFilter((Filter)shiroFilter(securityManager, userService).getObject());
filterRegistration.addInitParameter("targetFilterLifecycle", "true");
filterRegistration.setAsyncSupported(true);
filterRegistration.setEnabled(true);
//这里添加一下对DispatcherType.ASYNC的支持就可以了
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST,DispatcherType.ASYNC);
return filterRegistration;
}
Filter配置
在SpringMVC的实现中,对于Filter在进入异步请求之前会过一遍,异步请求之后又会过一遍。但是对于shiro这种鉴权的Filter,其实第二遍是没有必要的。所以我们需要在Filter中针对第二次过Filter的情况跳过。实现方式就是第一次进Filter的时候在request的Attribute中加一个属性,这样第二次进来的时候就会发现这个属性不为空,直接跳过,这也是servlet中OncePerRequestFilter
的实现逻辑。以JwtAuthFilter
为例:
public class JwtAuthFilter extends AuthenticatingFilter {
@Override
protected void postHandle(ServletRequest request, ServletResponse response){
//设置一个标记位
request.setAttribute("jwtShiroFilter.FILTERED", true);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断是不是第二次进入,是则直接返回
Boolean afterFiltered = (Boolean)(request.getAttribute("jwtShiroFilter.FILTERED"));
if( BooleanUtils.isTrue(afterFiltered))
return true;
...
}
}
同样的RolesFilter也添加类似的逻辑,具体请看源代码:https://github.com/chilexun/springboot-demo/tree/master/shiro-jwt-demo
[参考资料]
异步化,高并发大杀器 作者: 拿铁咖啡