Spring基于注解整合SpringMvc
文章简介
- servlet3.0简介
- 注解版注册web三大组件
- 运行时插件
- spring整合springmvc
- 异步处理
1.Servlet-3.0的基本使用
1.1 基于注解注册三大组件
以前我们使用springmvc做web开发时,都是将三大组件 Servlet
, Filter
, Listener
配置到web.xml
文件中,在servlet3.0规范中,提供了更加简单基于注解的方式来注册三大组件,进而完成web功能的开发。
servlet3.0是JSR-315的规范,里面有关于servlet3.0的详细阐述,其中包括了注解以及插件能力。
注意:servlet3.0
必须基于tomcat7及以上才可以。
在 servlet3.0文档 中的8.1
章节提供了相关注解的说明和使用样例,那我们就先看下。
- @WebServlet
- @WebFilter
- @WebListener
我们简单以@WebServlet
为例看下:
/**
* 必须要继承 javax.servlet.http.HttpServlet
*/
//@MultipartConfig 关于附件的一些配置
@WebServlet(
name = "myServlet",
urlPatterns = {"/hello", "/world"},
initParams = {
@WebInitParam(name = "name", value = "zhangsan"),
@WebInitParam(name = "age", value = "23")
}
)
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello @WebServlet");
}
@Override
public void init(ServletConfig config) throws ServletException {
String name = config.getInitParameter("name");
String age = config.getInitParameter("age");
//name = zhangsan, age = 23
System.out.println("name = " + name + ", age = " + age);
}
}
和之前的web.xml配置方式对比下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>com.qiuguan.servlet.MyServlet</servlet-class>
<init-param>
<param-name>name</param-name>
<param-value>zhangsan</param-value>
</init-param>
<init-param>
<param-name>age</param-name>
<param-value>23</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/hello</url-pattern>
<url-pattern>/world</url-pattern>
</servlet-mapping>
</web-app>
其实就是将web.xml中配置的标签放到了注解中。
这样部署好tomcat后(注意:tomcat版本至少是7.0.x),请求 /hello
或者 /world
(http://localhost:8080/world) 就会调用 MyServlet的 doGet(..)方法,然后将数据写回到浏览器中。
现在我们都是基于springboot开发,这些内容也逐渐交给框架去做,开发者也不是很关注这些内容,所以了解下即可。
1.2 运行时插件的功能
在 servlet3.0文档 中的8.2.4
章节中引入了一个运行时插件的功能
翻译过来就是说:当tomcat容器启动时,他会去扫描每个jar包类路径下的META-INF/services 目录下的一个叫做javax.servlet.ServletContainerInitializer
的文件(也可以放在web应用的WEB-INF/lib
目录下包含的jar
包的META-INF/services/javax.servlet.ServletContainerInitializer
)文件中,文件的内容就是ServletContainerInitializer
的实现类(注意是全类名)。
而且还可以在
ServletContainerInitializer
的实现类上添加@HandlesTypes()
注解,用于给容器导入一些感兴趣的类。
那就根据文档来配置看下:
- 定义一个实现了
ServletContainerInitializer
接口的类
@HandlesTypes(value = { UserService.class })
public class MyServletContainerInitializer implements ServletContainerInitializer {
/**
* tomcat 容器启动时,会调用这个方法
* @param c:{@link javax.servlet.annotation.HandlesTypes } 注解导入类型的子类(子接口,抽象类,实现类)
* @param ctx:servlet 上下文,一个web应用对一个 ServletContext
* @throws ServletException
*/
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
for (Class<?> aClass : c) {
System.out.println("@HandlesTypes 导入的子类 = " + aClass);
}
}
}
- 将实现类的
全类名
放置到类路径下目录名为 META-INF/services, 文件名为javax.servlet.ServletContainerInitializer
的文件中。
如果有多个实现类,换行写就可以了
- 看下
@HandlesTypes
注解导入的一些感兴趣的类
//TODO:根接口
public interface UserService {
}
//TODO:子接口
public interface UserServiceExt extends UserService{
}
//TODO:抽象类
public abstract class AbstractUserService implements UserService {
}
//TODO:继承抽象类的子类
public class UserServiceImpl extends AbstractUserService {
}
//TODO:继承普通类的子类
public class UserServiceImplExt extends UserServiceImpl{
}
- 如果
@HandlesTypes(value = { UserService.class })
,那么除了UserService接口本身所有的子类都将导入到Set集合中- 如果
@HandlesTypes(value = { UserServiceImpl.class })
,那么只会
导入UserServiceImplExt类,也就是 UserServiceImpl 的子类- 如果
@HandlesTypes(value = { UserServiceImplExt.class })
,那么将会报错,因为 UserServiceImplExt 没有子类。- 所以
@HandlesTypes()
注解指定的类一定要有子类,否则将报错
我们还可以在tomcat容器启动时,通过ServletContext
注册三大组件:
@HandlesTypes(value = {AbstractUserService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {
/**
* tomcat 容器启动时,会调用这个方法
*
* @param c:{@link javax.servlet.annotation.HandlesTypes } 注解导入类型的子类(子接口,抽象类,实现类)
* @param ctx:servlet 上下文,一个web应用对一个 ServletContext
* @throws ServletException
*/
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
for (Class<?> aClass : c) {
System.out.println("@HandlesTypes 导入的子类 = " + aClass);
}
//TODO: 在tomcat启动时,利用 ServletContext 注册组件
//TODO: 注册Servlet
ServletRegistration.Dynamic myServlet = ctx.addServlet("myServlet", new MyServlet());
myServlet.addMapping("/hello", "/world");
myServlet.setInitParameter("name", "zhangsan");
//TODO: 注册Listener
ctx.addListener(MyListener.class);
//TODO: 注册Filter
FilterRegistration.Dynamic myFilter = ctx.addFilter("myFilter", MyFilter.class);
//拦截request, forward 的请求
myFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
}
servlet 类:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("hello ServletContext.addServlet().....");
}
@Override
public void init(ServletConfig config) throws ServletException {
String name = config.getInitParameter("name");
System.out.println("name = " + name);
}
}
Listener 类:
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("tomcat 容器初始化.....");
ServletContext servletContext = sce.getServletContext();
//TODO:或者在这里也可以完成Servlet, Filter, Listener 组件的注册
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("tomcat 容器销毁");
}
}
这个运行时插件的功能在稍后整合springmvc时会看到,在springboot中也有使用。
2. 整合SpringMvc
2.1 整合过程
首先导入spring mvc 的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
然后在spring-web的jar包下可以看到这个文件:
前面我们有说过,tomcat在启动的时候会扫描每个jar包META-INF/services目录下一个叫javax.servlet.ServletContainerInitializer
的文件,这样就会加载这个文件中指定的容器初始化类。
类名是:
org.springframework.web.SpringServletContainerInitializer
那我们就看下这个类都做了什么?
//TODO:导入了感兴趣的类:WebApplicationInitializer 的子类
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
//webAppInitializerClasses: 是 WebApplicationInitializer 接口的所有子类
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
//TODO:如果不是接口,不是抽象类,且是WebApplicationInitializer的子类,则创建对象
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
接下来我们就看下 @HandlesTypes(WebApplicationInitializer.class)
注解导入的子类:
大致看下每个实现类主要都做了什么?
AbstractContextLoaderInitializer
: 定义了一个创建根容器的方法createRootApplicationContext()
,等待子类去实现;这个根容器就是Spring的IOC容器,比如以前基于xml配置开发时,根容器可以理解为ClassPathXmlApplicationContext
AbstractDispatcherServletInitializer
:从名字上看它是和DispatcherServlet
相关的初始化器,它定义了一个创建web容器的方法createServletApplicationContext()
, 等待子类去实现;还创建了DispatcherServlet
对象,通过ServletContext
注册到容器中。
以前我们在做spring+springmvc开发时,一般有2个配置文件,一个是
spring的配置文件
,比如是applicationContext.xml
,用于配置数据源,扫描@Servcie
,@Repository
等内容,这个就是根容器;另一个是springmvc的配置文件
,用于配置试图解析器,拦截器,以及扫描@Controller
等内容,这个就是web容器。
AbstractAnnotationConfigDispatcherServletInitializer
:从名字上看和上面差不多,只不过它是注解版的初始化器, 它要做的内容很简单,其实就是真正去实现前面提供的创建根容器以及web容器的方法,不过它的具体实现还是要交给我们开发者去实现。参考官方文档
- 所以我们要完成基于注解的方式启动springmvc, 只需要继承
AbstractAnnotationConfigDispatcherServletInitializer
,然后指定根容器和web容器的配置类即可。这一点从官方的图片上也有说明:
那我们就按照文档的提示,去继承 AbstractAnnotationConfigDispatcherServletInitializer
类,然后完成整合。
/**
* @author qiuguan
*/
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 指定spring根容器配置类:
* 1. 就是spring xml版的 applicationContext.xml 配置文件
--- 容器:ClassPathXmlApplicationContext
* 2. 就是spring 注解版的 @Configuration 注解标注的类
--- 容器:AnnotationConfigApplicationContext
*
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ MyRootConfig.class };
}
/**
* 指定springmvc web 容器配置类
* 1. 就是spring xml版的 spring-mvc.xml; 配置试图解析器,拦截器,扫描@Controller的 的配置文件
* 2. 就是spring 注解版的 @Configuration 注解标注的类
*
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{ MyWebConfig.class };
}
/**
* 配置 {@link org.springframework.web.servlet.DispatcherServlet} 的映射
* / : 拦截所有请求,包括静态资源,*.css, *.js, *.jpg (注意:不包括 *.jsp)
* /* : 拦截所有请求,也包括 *.jsp (*.jsp 不应该被springmvc拦截,因为jsp是由tomcat的JSP引擎负责解析的)
*/
@Override
protected String[] getServletMappings() {
return new String[]{ "/" };
}
}
然后我们看下根配置类 MyRootConfig
和web配置类 MyWebConfig
:
- 根配置类
MyRootConfig
:
/**
* @author qiuguan
* 根容器配置类,不扫描@Controller, 将其交给springmvc容器管理
*/
@ComponentScan(basePackages = "com.qiuguan",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = RestController.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
}
)
public class MyRootConfig {
}
1.配置数据源 2.配置事务 3.整合其他框架配置(比如:mybatis) 4.扫描除了@Controller以外的组件 ......
- web配置类
MyWebConfig
:
@ComponentScan(basePackages = "com.qiuguan",
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = RestController.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
},
useDefaultFilters = false
)
public class MyWebConfig {
}
1.配置试图解析器 2.配置静态资源 3.配置拦截器 4.只扫描@Controller组件 ......
到这里基于注解就已经完成了spring整合springmvc, 然后接下来可以写一个业务Controller和业务Service 并完成注入,然后启动tomcat, 通过浏览器去访问测试下。
上面我们虽然整合了springmvc, 但是我们并没有配置试图解析器,拦截器,静态资源等等,所以接下来我们就看下如何定制springmvc.
2.2 定制化SpringMvc
以前使用xml配置springmvc的时候,一般是这样配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 只扫描 @Controller注解标注的 组件-->
<context:component-scan base-package="com.qiuguan.controller"/>
<!-- 将springmvc 无法处理的请求交给tomcat 进行处理,这样可以通过DefaultServletHttpRequestHandler来处理静态资源了 -->
<mvc:default-servlet-handler/>
<!--我们一定会开启的标签,会使用springmvc提供的高级功能 -->
<mvc:annotation-driven/>
<!-- 配置试图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="WEB-INF/page/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- 和上面一样 -->
<mvc:view-resolvers>
<mvc:jsp prefix="WEB-INF/pages/" suffix=".jsp"/>
</mvc:view-resolvers>
</beans>
而现在使用注解的方式实现自定义springmvc的配置,也非常简单,只需要实现org.springframework.web.servlet.config.annotation.WebMvcConfigurer
接口,实现特定的方法,完成相对应的配置。并在实现类上添加 @EnableWebMvc
注解,它等价于以前xml配置的<mvc:annotation-driven/>
,表示使用springmvc的高级功能。那么我们就看下代码:
/**
* @author qiuguan
*/
//等价于:<mvc:annotation-driven/>
@EnableWebMvc
@ComponentScan(basePackages = "com.qiuguan",
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = RestController.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
},
useDefaultFilters = false
)
public class MyWebConfig implements WebMvcConfigurer {
/**
* 等价于:<mvc:default-servlet-handler/>
* 配置静态资源访问,比如请求一个图片,springmvc 是无法找到mapping 映射的,这样
* 它就会交给 DefaultServletHttpRequestHandler 去处理静态资源
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
/**
* 等价于:
* <mvc:interceptors>
* <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
* <mvc:interceptor>
* <mvc:mapping path="/**"/>
* <mvc:exclude-mapping path="/admin/**"/>
* <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
* </mvc:interceptor>
* <mvc:interceptor>
* <mvc:mapping path="/secure/*"/>
* <bean class="org.example.SecurityInterceptor"/>
* </mvc:interceptor>
* </mvc:interceptors>
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
//registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
}
/**
* 等价于:
* <mvc:view-resolvers>
* <mvc:jsp prefix="WEB-INF/pages/" suffix=".jsp"/>
* </mvc:view-resolvers>
*/
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
//默认从 WEB-INF/ 目录下查找 jsp 页面
//registry.jsp();
//自定义规则
registry.jsp("WEB-INF/pages/", ".jsp");
}
/**
* 等价于:<mvc:view-controller path="/" view-name="home"/>
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/hello").setViewName("hello");
}
}
更多配置请参考官方文档
文档中也有提到,可以使用更先进的方式,就是不用实现 WebMvcConfigurer
接口,直接去继承 DelegatingWebMvcConfiguration
, 如果继承了它,就可以移除掉 @EnableWebMvc
注解。但是如果继承了DelegatingWebMvcConfiguration
,那么springmvc默认的自动配置类将不会生效,也就是全面接管springmvc, 很多东西需要自己去配置。但是在实际开发中,并不建议全面接管springmvc, 而是搭配着使用。
@EnableWebMvc
注解实际上也是导入了DelegatingWebMvcConfiguration
类
@ComponentScan(basePackages = "com.qiuguan",
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = RestController.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
},
useDefaultFilters = false
)
public class MyWebConfig extends DelegatingWebMvcConfiguration {
/**
* 等价于:<mvc:default-servlet-handler/>
* 配置静态资源访问,比如请求一个图片,springmvc 是无法找到mapping 映射的,这样
* 它就会交给 DefaultServletHttpRequestHandler 去处理静态资源
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
使用方式不变。
好了,基于全注解的方式完成spring和springmvc的整合就到这里了,现在我们开发中,基本上使用的都是springboot, 这些配置框架已经帮我们做好了,所以这些了解即可,不过熟悉了这些对于springboot的学习也是有帮助的。
3.异步处理
3.1 servlet3.0中的异步处理
在servlet3.0之前,servlet采用 Thread-Per-Request的方式来处理请求,即每一次Http请求都由一个线程从头到尾负责处理。如果一个请求需要进行IO操作,比如访问数据库,或者调用第三方接口,那么其对应的线程将同步等待IO完成,而IO操作是耗时的,这样线程就不能及时释放归还到线程池中供后续使用,在并发量大的情况下,就会有严重的性能阻碍。springmvc框架是建立在servlet之上的,所以他也无法摆脱这样的困境,所以在servlet3.0提供了异步处理。
spring5之后提供了基于servelt3.1的新框架spring-webflux,一个响应式的Web开发框架
请求过来之后,交给tomcat中的线程去处理,处理完成后在将线程归回到线程池中,留着下次请求过来再使用。但是如果线程执行了长时间的IO耗时操作,那么线程就无法及时归还到线程池中,下次请求过来可能就没有线程可用,将拒绝请求。
我们可以通过代码验证下:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(Thread.currentThread().getName() + " start handle.....");
try {
task();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end handle.....");
}
public void task() throws Exception {
System.out.println(Thread.currentThread().getName() + " handling.....");
TimeUnit.SECONDS.sleep(2);
}
}
可以使用
@WebServlet
注册Servlet组件,也可以使用ServletContext
注册Servlet组件,这个不是重点,我们看下控制台打印结果:
不难发现,它从头到尾都是由 http-nio-exec-7 线程去执行的。
那么如何使用servlet3.0提供的异步是怎么做的呢?
在主线程中开启异步处理,主线程将请求交给其他线程去处理,主线程就结束了,被放回了主线程池,由其他线程继续处理请求
那么我们通过代码来演示下:
@WebServlet(name = "myAsyncServlet", urlPatterns = "/sync", asyncSupported = true)
public class MyAsyncServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
long start = System.currentTimeMillis();
System.out.println("主线程---" + Thread.currentThread().getName() + " start....");
AsyncContext asyncContext = req.startAsync(req, resp);
//TODO:业务逻辑异步处理
asyncContext.start(() -> {
try {
long asyncStart = System.currentTimeMillis();
System.out.println("子线程---" + Thread.currentThread().getName() + " start....");
task();
//TODO:异步任务处理完成了
asyncContext.complete();
//TODO:获取响应
asyncContext.getResponse().getWriter().write("hello async....");
System.out.println("子线程---" + Thread.currentThread().getName() + " end.... 耗时: " + (System.currentTimeMillis() - asyncStart));
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println("主线程---" + Thread.currentThread().getName() + " end.... 耗时:" + (System.currentTimeMillis() - start));
}
private void task() throws Exception {
TimeUnit.SECONDS.sleep(4);
}
}
然后看下控制台的打印结果:
可以看到,主线程立马就结束了,将任务交给子线程去执行,这样主线程就可以去处理新过来的请求。不过从控制台可以看出,主线程和异步线程用的是同一个线程池,这里可以自定义异步线程池。
上面你的代码实例中时通过asyncContext.complete()
来结束异步请求的;结束请求还有另外一种方式,就是子线程处理完业务之后,将结果放在 request 中,然后调用asyncContext.dispatch()
转发请求,此时请求又会进入当前 servlet,此时需在代码中判断请求是不是异步转发过来的,如果是的,则从 request 中获取结果,然后输出,这种方式就是 springmvc 处理异步的方式,我们简单看下:
@WebServlet(name = "myAsyncServlet", urlPatterns = "/sync", asyncSupported = true)
public class MyAsyncServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (DispatcherType.ASYNC == req.getDispatcherType()) {
//TODO:说明是异步转发过来的,所以直接获取结果
Object asyncResult = req.getAttribute("asyncResult");
resp.getWriter().write("hello async result: " + asyncResult);
} else {
long start = System.currentTimeMillis();
System.out.println("主线程---" + Thread.currentThread().getName() + " start....");
AsyncContext asyncContext = req.startAsync(req, resp);
//TODO:业务逻辑异步处理
asyncContext.start(() -> {
try {
long asyncStart = System.currentTimeMillis();
System.out.println("子线程---" + Thread.currentThread().getName() + " start....");
task();
//模拟
asyncContext.getRequest().setAttribute("asyncResult", "aysnc task success");
//转发请求,调用这个方法之后,请求又会被转发到当前的servlet,又会进入当前servlet的service方法
//此时请求的类型(request.getDispatcherType())是DispatcherType.ASYNC,所以通过这个值可以判断请求是异步转发过来的
//然后在request中将结果取出
asyncContext.dispatch();
//asyncContext.dispatch("/world"); //还可以根据path 跳转到其他Servlet
System.out.println("子线程---" + Thread.currentThread().getName() + " end.... 耗时: " + (System.currentTimeMillis() - asyncStart));
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println("主线程---" + Thread.currentThread().getName() + " end.... 耗时:" + (System.currentTimeMillis() - start));
}
}
private void task() throws Exception {
TimeUnit.SECONDS.sleep(4);
}
}
使用起来还是很简单,还可以绑定监听器AsyncListener
, 因为现在开发基本上不会使用原生的Servlet, 所以这些了解即可,不用太关注。SpringMVC 基于Servlet之上的上层框架,我们可以看下它的异步是如何处理的。
3.2 SpringMvc的异步处理
SpringMvc使用异步的方式也非常简单,可以参考官方文档 , 通过返回值是 java.util.concurrent.Callable
或者是 DeferredResult
来完成异步的功能。文档中对于处理流程也有说明,以DeferredResult
为例:
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
//TODO:指定超时时间是3s,超时后将返回错误信息
DeferredResult<String> deferredResult = new DeferredResult<>(3000L, "DeferredResult Timeout");
// Save the deferredResult somewhere..
return deferredResult;
}
//TODO:等待其他线程调用setrResult方法
deferredResult.setResult(data);
- 如果返回值是
DeferredResult
,将DeferredResult
对象保存在队列中 - SpringMvc 调用
request.startAsync()
(前面讲servlet的异步时有看到) - 同时
DispatcherServlet
将退出请求处理线程 - 如果其他线程一旦设置了
DeferredResult
结果,则SpringMvc将调用dispatch()
方法将请求重新交给Servlet容器 - 再次调用
DispatcherServlet
,并使用异步生成的返回值恢复处理。
我们可以模拟下 DeferredResult
的使用场景:
@Controller
public class MemberController {
//TODO:模拟一下中间件
private final Queue<DeferredResult<Object>> memberQueue = new LinkedBlockingQueue<>(100);
@ResponseBody
@GetMapping("/register")
public DeferredResult<Object> test(){
long start = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "start");
//TODO:指定超时时间是3s,超时后将返回错误信息
DeferredResult<Object> deferredResult = new DeferredResult<>(5000L, "DeferredResult Timeout");
memberQueue.add(deferredResult);
System.out.println(Thread.currentThread().getName() + "end, 耗时:" + (System.currentTimeMillis() - start));
return deferredResult;
}
//TODO:模拟另一个线程
@ResponseBody
@GetMapping("/completeRegister")
public String start(){
String memberId = UUID.randomUUID().toString();
DeferredResult<Object> result = memberQueue.poll();
result.setResult(memberId);
return memberId;
}
}
当请求
/register
时,控制台的耗时是0ms, 说明主线程很快就释放了,然后紧接着调用/completeRegister
,/register
将立刻返回。
如果想探究源码也比较容易
- 入口:
DispatcherServlet#doDispatch(..)
RequestMappingHandlerAdapter#handleInternal(...)
RequestMappingHandlerAdapter#invokeHandlerMethod(...)
ServletInvocableHandlerMethod#invokeAndHandle(...)
HandlerMethodReturnValueHandlerComposite#handleReturnValue(....)
- DeferredResultMethodReturnValueHandler#handleReturnValue(....)
处理返回值
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(..)
- WebAsyncManager#startAsyncProcessing(.)
按照这个代码顺序,就可以看到熟悉的
request.startAsync(request, response)
和asyncContext.dispatch()
;也就是Springmvc基于servlet3.0做的异步功能。
好了,Spring基于注解整合SpringMvc就记录到这里吧 限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢
转载自:https://juejin.cn/post/7135383767757094926