Spring基于注解整合SpringMvc

983 阅读13分钟

drew-beamer-Vc1pJfvoQvY-unsplash.jpg


文章简介

  1. servlet3.0简介
  2. 注解版注册web三大组件
  3. 运行时插件
  4. spring整合springmvc
  5. 异步处理

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及以上才可以。

image.png

servlet3.0文档 中的8.1章节提供了相关注解的说明和使用样例,那我们就先看下。

  1. @WebServlet
  2. @WebFilter
  3. @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章节中引入了一个运行时插件的功能 image.png

翻译过来就是说:当tomcat容器启动时,他会去扫描每个jar包类路径下的META-INF/services 目录下的一个叫做javax.servlet.ServletContainerInitializer的文件(也可以放在web应用的WEB-INF/lib目录下包含的jar包的META-INF/services/javax.servlet.ServletContainerInitializer)文件中,文件的内容就是ServletContainerInitializer的实现类(注意是全类名)。

而且还可以在ServletContainerInitializer的实现类上添加@HandlesTypes()注解,用于给容器导入一些感兴趣的类。

那就根据文档来配置看下:

  1. 定义一个实现了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);
        }
    }
}
  1. 将实现类的全类名放置到类路径下目录名为 META-INF/services, 文件名为javax.servlet.ServletContainerInitializer 的文件中。

如果有多个实现类,换行写就可以了

image.png

  1. 看下 @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{
}
  1. 如果 @HandlesTypes(value = { UserService.class }),那么除了UserService接口本身所有的子类都将导入到Set集合中
  2. 如果 @HandlesTypes(value = { UserServiceImpl.class }),那么只会导入UserServiceImplExt类,也就是 UserServiceImpl 的子类
  3. 如果 @HandlesTypes(value = { UserServiceImplExt.class }),那么将会报错,因为 UserServiceImplExt 没有子类。
  4. 所以 @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包下可以看到这个文件:

image.png

前面我们有说过,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) 注解导入的子类:

image.png

大致看下每个实现类主要都做了什么?

  1. AbstractContextLoaderInitializer: 定义了一个创建根容器的方法createRootApplicationContext(),等待子类去实现;这个根容器就是Spring的IOC容器,比如以前基于xml配置开发时,根容器可以理解为 ClassPathXmlApplicationContext
  2. AbstractDispatcherServletInitializer:从名字上看它是和DispatcherServlet相关的初始化器,它定义了一个创建web容器的方法 createServletApplicationContext(), 等待子类去实现;还创建了 DispatcherServlet 对象,通过ServletContext 注册到容器中。

以前我们在做spring+springmvc开发时,一般有2个配置文件,一个是spring的配置文件,比如是applicationContext.xml,用于配置数据源,扫描@Servcie, @Repository等内容,这个就是根容器;另一个是springmvc的配置文件,用于配置试图解析器,拦截器,以及扫描 @Controller等内容,这个就是web容器

  1. AbstractAnnotationConfigDispatcherServletInitializer:从名字上看和上面差不多,只不过它是注解版的初始化器, 它要做的内容很简单,其实就是真正去实现前面提供的创建根容器以及web容器的方法,不过它的具体实现还是要交给我们开发者去实现。参考官方文档

image.png

  1. 所以我们要完成基于注解的方式启动springmvc, 只需要继承AbstractAnnotationConfigDispatcherServletInitializer,然后指定根容器和web容器的配置类即可。这一点从官方的图片上也有说明:

image.png

那我们就按照文档的提示,去继承 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

  1. 根配置类 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以外的组件
......

  1. 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开发框架

image.png

请求过来之后,交给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组件,这个不是重点,我们看下控制台打印结果:

image.png

不难发现,它从头到尾都是由 http-nio-exec-7 线程去执行的。

那么如何使用servlet3.0提供的异步是怎么做的呢?

在主线程中开启异步处理,主线程将请求交给其他线程去处理,主线程就结束了,被放回了主线程池,由其他线程继续处理请求

image.png

那么我们通过代码来演示下:

@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);
    }
}

然后看下控制台的打印结果:

image.png

可以看到,主线程立马就结束了,将任务交给子线程去执行,这样主线程就可以去处理新过来的请求。不过从控制台可以看出,主线程和异步线程用的是同一个线程池,这里可以自定义异步线程池。

上面你的代码实例中时通过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);
  1. 如果返回值是DeferredResult,将DeferredResult对象保存在队列中
  2. SpringMvc 调用 request.startAsync() (前面讲servlet的异步时有看到)
  3. 同时 DispatcherServlet 将退出请求处理线程
  4. 如果其他线程一旦设置了 DeferredResult 结果,则SpringMvc将调用 dispatch() 方法将请求重新交给Servlet容器
  5. 再次调用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 将立刻返回。

如果想探究源码也比较容易

  1. 入口:DispatcherServlet#doDispatch(..)
  2. RequestMappingHandlerAdapter#handleInternal(...)
  3. RequestMappingHandlerAdapter#invokeHandlerMethod(...)
  4. ServletInvocableHandlerMethod#invokeAndHandle(...)
  5. HandlerMethodReturnValueHandlerComposite#handleReturnValue(....)
  6. DeferredResultMethodReturnValueHandler#handleReturnValue(....)

处理返回值

  1. WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(..)
  2. WebAsyncManager#startAsyncProcessing(.)

按照这个代码顺序,就可以看到熟悉的request.startAsync(request, response)asyncContext.dispatch();也就是Springmvc基于servlet3.0做的异步功能。

好了,Spring基于注解整合SpringMvc就记录到这里吧
限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢