浅尝Spring注解开发_Servlet3.0与SpringMVC
阅读原文时间:2022年05月11日阅读:1

浅尝Spring注解开发_Servlet 3.0 与 SpringMVC

浅尝Spring注解开发,基于Spring 4.3.12

Servlet3.0新增了注解支持、异步处理,可以省去web.xml文件,异步处理更高效。

浅尝Spring注解开发_自定义注册组件、属性赋值、自动装配

浅尝Spring注解开发_Bean生命周期及执行过程

浅尝Spring注解开发_AOP原理及完整过程分析(源码)

浅尝Spring注解开发_声明式事务及原理

浅尝Spring注解开发_简单理解BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor、ApplicationListener

Spring注解开发_Spring容器创建概述

浅尝Spring注解开发_Servlet3.0与SpringMVC

创建Web动态工程,Servlet3.0需要Tomcat7.0及以上

注解包括@WebServlet()、@WebListener、@WebFilter

  • 发送请求

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
        <a href="hello">hello</a>
    </body>
    </html>
  • 处理请求,利用@WebServlet()注解标注

    @WebServlet("/hello")
    public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // TODO Auto-generated method stub
        //super.doGet(req, resp);
        resp.getWriter().write("hello...");
    }
    }

Shared libraries(共享库) / runtimes pluggability(运行时插件能力)

容器在启动应用的时候,会扫描当前应用每一个jar包里面META-INF/services/javax.servlet.ServletContainerInitializer指定的实现类,启动并运行这个实现类的方法,并传入感兴趣的类型

原理

  1. Servlet容器启动会扫描,当前应用里面每一个jar包的ServletContainerInitializer的实现
  2. 提供ServletContainerInitializer的实现类:

应用

  1. 创建一个实现ServletContainerInitializer的类

    public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set&lt;Class&lt;?&gt;&gt; arg0, ServletContext sc) throws ServletException {
        // TODO Auto-generated method stub
    
    }
    }
  2. 在src目录(或idea在resources目录下)创建META-INF/services/目录和javax.servlet.ServletContainerInitializer文件,内容为实现类全类名

    com.atguigu.servlet.MyServletContainerInitializer
  3. 实现接口方法,两个参数,一个注解

    //容器启动的时候会将@HandlesTypes指定的这个类型下面的子类(实现类,子接口等)传递过来;
    //传入感兴趣的类型;
    @HandlesTypes(value={HelloService.class})
    public class MyServletContainerInitializer implements ServletContainerInitializer {
    /**
     * 应用启动的时候,会运行onStartup方法;
     *
     * Set&lt;Class&lt;?&gt;&gt; arg0:感兴趣的类型的所有子类型;
     * ServletContext arg1:代表当前Web应用的ServletContext;一个Web应用一个ServletContext;
     *
     */
    @Override
    public void onStartup(Set&lt;Class&lt;?&gt;&gt; arg0, ServletContext sc) throws ServletException {
        // TODO Auto-generated method stub
        System.out.println("感兴趣的类型:");
        for (Class&lt;?&gt; claz : arg0) {
            System.out.println(claz);
        }
    
    }
    }
  4. @HandlesTypes的值是感兴趣的类,参数Set> arg0的值是感兴趣的类的子类、子接口、实现类等

    • 创建接口HelloService,并创建多个继承或实现此接口的子类
  5. 应用启动就会获得指定的类型

编码方式注册Servlet、Filter、Listener

  1. 使用ServletContext注册Web组件(Servlet、Filter、Listener)
  2. 使用编码的方式,在项目启动的时候给ServletContext里面添加组件
  3. 必须在项目启动的时候来添加

应用

  1. 自定义Servlet,继承HttpServlet

    public class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // TODO Auto-generated method stub
        resp.getWriter().write("tomcat...");
    }
    }
  2. 自定义Filter,实现Filter

    public class UserFilter implements Filter {
    @Override
    public void destroy() {
        // TODO Auto-generated method stub
    
    }
    
    @Override
    public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
            throws IOException, ServletException {
        // 过滤请求
        System.out.println("UserFilter...doFilter...");
        //放行
        arg2.doFilter(arg0, arg1);
    
    }
    
    @Override
    public void init(FilterConfig arg0) throws ServletException {
        // TODO Auto-generated method stub
    
    }
    }
  3. 自定义Listener,实现ServletContextListener

    /**
     * 监听项目的启动和停止
     */
    public class UserListener implements ServletContextListener {
    //监听ServletContext销毁
    @Override
    public void contextDestroyed(ServletContextEvent arg0) {
        // TODO Auto-generated method stub
        System.out.println("UserListener...contextDestroyed...");
    }
    
    //监听ServletContext启动初始化
    @Override
    public void contextInitialized(ServletContextEvent arg0) {
        // TODO Auto-generated method stub
    
        //在这里项目启动前也可以得到的ServletContext
        ServletContext servletContext = arg0.getServletContext();
        System.out.println("UserListener...contextInitialized...");
    }
    }
  4. 注册

    • ServletContainerInitializer的一个参数是ServletContext

    • 另外在上面Listener监听时也能得到ServletContext

      //容器启动的时候会将@HandlesTypes指定的这个类型下面的子类(实现类,子接口等)传递过来;
      //传入感兴趣的类型;
      @HandlesTypes(value={HelloService.class})
      public class MyServletContainerInitializer implements ServletContainerInitializer {

      /**
       * 应用启动的时候,会运行onStartup方法;
       *
       * Set<Class<?>> arg0:感兴趣的类型的所有子类型;
       * ServletContext arg1:代表当前Web应用的ServletContext;一个Web应用一个ServletContext;
       *
       * 1)、使用ServletContext注册Web组件(Servlet、Filter、Listener)
       * 2)、使用编码的方式,在项目启动的时候给ServletContext里面添加组件;
       *      必须在项目启动的时候来添加;
       *      1)、ServletContainerInitializer得到的ServletContext;
       *      2)、ServletContextListener得到的ServletContext;
       */
      @Override
      public void onStartup(Set<Class<?>> arg0, ServletContext sc) throws ServletException {
          // TODO Auto-generated method stub
          System.out.println("感兴趣的类型:");
          for (Class<?> claz : arg0) {
              System.out.println(claz);
          }
      //注册组件  ServletRegistration
      ServletRegistration.Dynamic servlet = sc.addServlet("userServlet", new UserServlet());
      //配置servlet的映射信息
      servlet.addMapping("/user");
      
      //注册Listener
      sc.addListener(UserListener.class);
      
      //注册Filter  FilterRegistration
      FilterRegistration.Dynamic filter = sc.addFilter("userFilter", UserFilter.class);
      //配置Filter的映射信息,另一个方法参数是直接使用Servlet拦截
      filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
      }

      }

原理分析

web容器启动,扫描指定位置类名,发现感兴趣的类,然后从这个类的子类中选择创建组件对象

  1. 导入jar包

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
                <version>4.3.11.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.0.1</version>
                <scope>provided</scope>
            </dependency>
  2. web容器在启动的时候,会扫描每个jar包下的META-INF/services/javax.servlet.ServletContainerInitializer,打开spring-web.jar目录下的文件

    org.springframework.web.SpringServletContainerInitializer
  3. 加载这个文件指定的类SpringServletContainerInitializer,发现@HandlesTypes注解中感兴趣的类是WebApplicationInitializer

  4. Spring的应用一启动会加载感兴趣的WebApplicationInitializer接口的下的所有组件,并且为WebApplicationInitializer组件创建对象(组件不是接口,不是抽象类)

    @HandlesTypes(WebApplicationInitializer.class)
    public class SpringServletContainerInitializer implements ServletContainerInitializer {
        @Override
        public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
                throws ServletException {
                    //...
                    //防御:一些 servlet 容器为我们提供了无效的类,不管@HandlesTypes 说什么......
                    if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                            WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                            initializers.add((WebApplicationInitializer) waiClass.newInstance());
                    }
                    //...
    }
  5. 关于感兴趣的类WebApplicationInitializer的子类(子接口、抽象)三个抽象类:

  6. 总结:以注解方式来启动SpringMVC,可以选择其中一个抽象类,例如继承AbstractAnnotationConfigDispatcherServletInitializer,实现抽象方法指定DispatcherServlet的配置信息

整合

SpringMVC官方文档Web on Servlet Stack (spring.io)

Spring关于DispatcherServlet的解释,将DispatcherServlet分为具有上下继承关系的两部分,其中可在多个实例之间共享的Root WebApplicationContext包含中间服务层Service、数据源Repositories,和可在特定于Servlet的子项被覆盖的控制器Controllers、视图解析器ViewResolver、处理器映射器HandlerMapping其他与web相关的bean)

  讲过上节分析,选择感兴趣的类WebApplicationInitializer的其中一个子接口AbstractAnnotationConfigDispatcherServletInitializer来实现

  1. 实现AbstractAnnotationConfigDispatcherServletInitializer接口并配置

    //web容器启动的时候创建对象;调用方法来初始化容器以前前端控制器
    public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    //获取根容器的配置类;(Spring的配置文件)   父容器;
    @Override
    protected Class&lt;?&gt;[] getRootConfigClasses() {
        // TODO Auto-generated method stub
        return new Class&lt;?&gt;[]{RootConfig.class};
    }
    
    //获取web容器的配置类(SpringMVC配置文件)  子容器;
    @Override
    protected Class&lt;?&gt;[] getServletConfigClasses() {
        // TODO Auto-generated method stub
        return new Class&lt;?&gt;[]{AppConfig.class};
    }
    
    //获取DispatcherServlet的映射信息
    //  /:拦截所有请求(包括静态资源(xx.js,xx.png)),但是不包括*.jsp;
    //  /*:拦截所有请求;连*.jsp页面都拦截;jsp页面是tomcat的jsp引擎解析的;
    @Override
    protected String[] getServletMappings() {
        // TODO Auto-generated method stub
        return new String[]{"/"};
    }
    }
  2. 创建两个配置

  3. 创建业务

与之前使用xml文件相似,可以配置视图解析器、静态资源访问、拦截器

可参考Spring文档Web on Servlet Stack (spring.io)

  1. @EnableWebMvc:开启SpringMVC定制配置功能,与xml中的<mvc:annotation-driven>相同

  2. 配置组件,实现WebMvcConfigurer(Spring4中用WebMvcConfigurerAdapter)

    //SpringMVC只扫描Controller;子容器
    //useDefaultFilters=false 禁用默认的过滤规则;
    @ComponentScan(value="com.atguigu",includeFilters={
            @Filter(type=FilterType.ANNOTATION,classes={Controller.class})
    },useDefaultFilters=false)
    @EnableWebMvc
    public class AppConfig  extends WebMvcConfigurerAdapter  {
    //定制
    
    //视图解析器
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // TODO Auto-generated method stub
        //默认所有的页面都从 /WEB-INF/ xxx .jsp
        //registry.jsp();
        registry.jsp("/WEB-INF/views/", ".jsp");
    }
    
    //静态资源访问
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        // TODO Auto-generated method stub
        configurer.enable();
    }
    
    //拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // TODO Auto-generated method stub
        //super.addInterceptors(registry);
        registry.addInterceptor(new MyFirstInterceptor()).addPathPatterns("/**");
    }
    }

    拦截器

    public class MyFirstInterceptor implements HandlerInterceptor {
    //目标方法运行之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // TODO Auto-generated method stub
        System.out.println("preHandle..."+request.getRequestURI());
        return true;
    }
    
    //目标方法执行正确以后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        // TODO Auto-generated method stub
        System.out.println("postHandle...");
    
    }
    
    //页面响应以后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        // TODO Auto-generated method stub
        System.out.println("afterCompletion...");
    }
    }

  在Servlet3.0之前,Servlet才采用Thread—Per—Request的方式处理请求。即每一次http请求都由一个线程从头到尾负责处理。

  如果一个请求需要进行IO操作,比如访问数据库,调用第三方服务接口等,那么其所对应的线程将同步地等待IO操作完成,而IO操作是非常慢的,所以此时的线程并不能及时的释放回线程池以供后续使用,在并发量越来越大的情况下,这将带来严重的新能问题。即便像Spring、struts这样的高层框架也脱离不了这样的问题,因为他们都是建立在Servlet之上的。为了解决这样的问题,Servlet3.0引入了一步处理,然后在Servlet3.1中又引入了非阻塞IO来进行增加一步处理的性能。

同步请求

每次请求时从线程池获取一个线程,然后处理请求,直到处理完成,仍由这个线程响应。如果一个线程等待时间过长,其他请求就会占满线程池

验证

  1. 发送一个请求,获取处理请求前后的线程

    @WebServlet("/hello")
    public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // TODO Auto-generated method stub
        //super.doGet(req, resp);
        System.out.println(Thread.currentThread()+" start...");
        try {
            sayHello();
        } catch (Exception e) {
            e.printStackTrace();
        }
        resp.getWriter().write("hello...");
        System.out.println(Thread.currentThread()+" end...");
    }
    
    public void sayHello() throws Exception{
        System.out.println(Thread.currentThread()+" processing...");
        Thread.sleep(3000);
    }
    }
  2. 输出

    • 整个请求都是由一个线程处理

      Thread[http-nio-8888-exec-10,5,main] start…
      Thread[http-nio-8888-exec-10,5,main] processing…
      Thread[http-nio-8888-exec-10,5,main] end…

异步请求

当请求过来时,由主线程池中一条线程处理,当开始异步处理时,主线程就结束返回,等待其他请求,异步处理线程的一条线程同时开始处理请求。

验证

  1. 异步对象AsyncContext可以开始和完成异步处理,同时开启注解@WebServlet(value="/xxx",asyncSupported=true)

    @WebServlet(value="/async",asyncSupported=true)
    public class HelloAsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1、支持异步处理asyncSupported=true
        //2、开启异步模式
        //主线程开始
        System.out.println("main thread start..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
        AsyncContext startAsync = req.startAsync();
    
        //3、业务逻辑进行异步处理;开始异步处理
        startAsync.start(new Runnable() {
            @Override
            public void run() {
                try {
                    //副线程开始
                    System.out.println("vice thread start..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
                    sayHello();
                    startAsync.complete();
                    //获取到异步上下文
                    //AsyncContext asyncContext = req.getAsyncContext();
                    //4、获取响应
                    //使用asyncContext.getResponse()副线程不能结束,仍使用startAsync.getResponse()获取响应
                    //ServletResponse response = asyncContext.getResponse();
                    ServletResponse response = startAsync.getResponse();
                    response.getWriter().write("hello async...");
                    //副线程结束
                    System.out.println("vice thread end..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
                } catch (Exception e) {
                }
            }
        });
        //主线程结束
        System.out.println("main thread end..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
    }
    
    //处理过程
    public void sayHello() throws Exception{
        System.out.println(Thread.currentThread()+" processing...");
        Thread.sleep(3000);
    }
    }
  2. 输出

    • 主线程开启后遇到异步处理立刻结束

    • 主线程结束同时副线程开始处理异步请求

      //主线程
      main thread start…Thread[http-nio-8888-exec-5,5,main]==>1651997065639
      main thread end…Thread[http-nio-8888-exec-5,5,main]==>1651997065648
      //副线程
      vice thread start…Thread[http-nio-8888-exec-7,5,main]==>1651997065648
      Thread[http-nio-8888-exec-7,5,main] processing…
      vice thread end…Thread[http-nio-8888-exec-7,5,main]==>1651997068656

参考SpringMVC官方文档Web on Servlet Stack (spring.io)

返回Callable

Controller返回Callable类型的返回值,异步处理在Callable类中进行

过程

Callable处理过程

  1. 控制器返回Callable
  2. SpringMVC异步处理,将Callable 提交到 TaskExecutor 使用一个隔离的线程进行执行
  3. DispatcherServlet和所有的Filter退出web容器的线程,但是response 保持打开状态
  4. Callable返回结果,SpringMVC将请求重新派发给容器,恢复之前的处理
  5. 根据Callable返回的结果。SpringMVC继续进行视图渲染流程等(从收请求-视图渲染)
  6. 对于异步处理,普通拦截器不能拦截异步中的代码,使用异步拦截器:

应用

  1. 异步处理

    @Controller
    public class AsyncController {
    @ResponseBody
    @RequestMapping("/async01")
    public Callable&lt;String&gt; async01(){
        //主线程开始
        System.out.println("main thread start..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
    
        Callable&lt;String&gt; callable = new Callable&lt;String&gt;() {
            @Override
            public String call() throws Exception {
                //副线程开始
                System.out.println("vice thread start..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
    
                //处理业务
                Thread.sleep(2000);
                //副线程开始
                System.out.println("vice thread end..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
    
                return "Callable&lt;String&gt; async01()";
            }
        };
    
        //主线程结束
        System.out.println("main thread end..."+Thread.currentThread()+"==&gt;"+System.currentTimeMillis());
    
        return callable;
    }
    }
  2. 输出

    • 主线程和副线程不是同一线程

    • 拦截器拦截了两次同一个请求

    • 上面两处都是Callable的原因,Callable会另起一个线程,并且结束前端控制器,等异步处理完在恢复前端控制器继续执行,所以有两次拦截器,并且要使用异步拦截器

      //拦截器
      preHandle…/springmvc_annotation_war/async01

      //主线程,执行完DispatcherServlet及所有的Filter退出线程
      main thread start…Thread[http-nio-8888-exec-10,5,main]==>1652003199856
      main thread end…Thread[http-nio-8888-exec-10,5,main]==>1652003199860

      //副线程,等待Callable执行
      vice thread start…Thread[MvcAsync1,5,main]==>1652003199873
      vice thread end…Thread[MvcAsync1,5,main]==>1652003201875

      //拦截器,再次收到之前重发过来的请求
      preHandle…/springmvc_annotation_war/async01
      postHandle…
      afterCompletion…

DeferredResult

上面的Callable使用与简单场景,可以处理当前简单业务,对于多应用复杂业务可以使用DeferredResult类型,在当前应用处理请求,然后继续向下执行,当其他应用完成请求之后,通知DeferredResult就可以响应结果结束请求。对应下图:

应用1是当前应用,负责接受请求,中间隔着其他组件,应用2负责异步处理请求,然后将结果返回给当前应用1。

官方文档

  在 Servlet 容器中启用异步请求处理功能后,控制器方法可以使用 DeferredResult 包装任何支持的控制器方法返回值,如以下示例所示:

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // Save the deferredResult somewhere..
    // 将deferredResult保存到某个地方。
    return deferredResult;
}

// From some other thread...
// 从其他线程...
deferredResult.setResult(result);

  控制器可以从不同的线程异步生成返回值 — 例如,响应外部事件(JMS 消息)、计划任务或其他事件。

应用

  1. 请求

    • 在当前应用接收请求,然后将DeferredResult保存起来

    • 然后返回

      @Controller
      public class AsyncController {

      @ResponseBody
      @RequestMapping("/createOrder")
      public DeferredResult<Object> createOrder(){
          //使用超时值和要使用的默认结果创建DeferredResult
          DeferredResult<Object> deferredResult = new DeferredResult<>((long)3000, "create fail...");
      DeferredResultQueue.save(deferredResult);
      
      return deferredResult;
      }

      }

  2. 保存DeferredResult队列

    • 有存取功能

      public class DeferredResultQueue {

      private static Queue<DeferredResult<Object>> queue = new ConcurrentLinkedQueue<DeferredResult<Object>>();
      
      public static void save(DeferredResult<Object> deferredResult){
          queue.add(deferredResult);
      }
      
      public static DeferredResult<Object> get( ){
          return queue.poll();
      }

      }

  3. 模拟另一个应用,异步处理业务

    • 真实请情况可以是另一个应用或线程

      @ResponseBody
      @RequestMapping("/create")
      public String create(){
          //创建订单
          String order = UUID.randomUUID().toString();
          //取出DeferredResult
          DeferredResult<Object> deferredResult = DeferredResultQueue.get();
          //放入结果,同时应用1完成异步处理
          deferredResult.setResult(order);
          return "success===>"+order;
      }
  4. 结果

    成功
    52917142-3a12-4df5-9c89-258cbf0d0c3a
    success===>52917142-3a12-4df5-9c89-258cbf0d0c3a
    失败
    create fail...
  5. DeferredResult部分文档

    DeferredResult 提供了使用 Callable 进行异步请求处理的替代方法。 虽然代表应用程序同时执行 Callable,但使用 DeferredResult 应用程序可以从其选择的线程中生成结果。
    子类可以扩展此类以轻松地将附加数据或行为与 DeferredResult 关联。 例如,可能希望通过扩展类并为用户添加一个附加属性来关联用于创建 DeferredResult 的用户。 这样,以后可以很容易地访问用户,而无需使用数据结构来进行映射。
    
    将附加行为与此类关联的示例可以通过扩展类以实现附加接口来实现。 例如,您可能希望实现 Comparable 以便在将 DeferredResult 添加到 PriorityQueue 时以正确的顺序对其进行处理。
    
    onCompletion
    public void onCompletion(Runnable callback)
    注册代码在异步请求完成时调用。
    当异步请求因任何原因(包括超时和网络错误)完成时,从容器线程调用此方法。
    这对于检测延迟实例不再可用很有用。
    
    public boolean setResult(T result)
    设置DeferredResult的值并处理它。

更详细的文章推荐:

DeferredResult的使用场景及用法 - 胡桃同学的个人空间 - OSCHINA - 中文开源技术交流社区