SSM-SpringMVC(四)-拦截器、springVC执行流程、SSM整合

拦截器(Interceptor)

  1. 拦截器(Interceptor))的功能,跟过滤器(Filter)有点类似,但是有本质区别
  2. 过滤器
    1. 是Servlet规范的一部分
    2. 能拦截任意请求,在请求抵达Servlet之前、响应抵达客户端之前拦截
    3. 常用于:编码设置、登录校验等
  3. 拦截器
    1. 是SpringMVC的一部分
    2. 只能拦截DispatcherServlet拦截到的内容,一般用来拦截Controller,请求到达Controller之前。
    3. 常用于:抽取Controller的公共代码

拦截器使用步骤

  1. 自定义一个类实现HandlerInterceptor接口

     public class MyInterceptor1 implements HandlerInterceptor {
         @Override
         public boolean preHandle(HttpServletRequest request,
                                  HttpServletResponse response,
                                  Object handler) throws Exception {
             System.out.println("MyInterceptor1 - preHandle - " + request.getRequestURI());
             return true;
         }
        
         @Override
         public void postHandle(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                ModelAndView modelAndView) throws Exception {
             System.out.println("MyInterceptor1 - postHandle - " + request.getRequestURI());
             //针对某个控制器,做相应的请求头设置
             //if (控制器) {
                 //response.setHeader();
             //}
               //拦截器可直接修改Controller之前指定的路径
             //比如Controller中处理test2时,响应的是test1.jsp,这里可以修改响应路径资源
             //modelAndView.setViewName("/test2.jsp");
         }
        
         /*
         客户端的响应的数据(下面三个方法)已经执行完了,才会执行这个方法
         response.sendRedirect();
         request.getRequestDispatcher().forward();
         response.getWriter().write();
         * */
         @Override
         public void afterCompletion(HttpServletRequest request,
                                     HttpServletResponse response,
                                     Object handler,
                                     Exception ex) throws Exception {
             System.out.println("MyInterceptor1 - afterCompletion - " + request.getRequestURI());
         }
     }
    
  2. HandlerInterceptor方法分析
    1. preHandle:在controller的处理方法之前调用
      1. 一般在这里进行初始化、请求预处理操作
      2. 如果返回false,那么后序将不会再调用controller的处理方法、postHandle、afterCompletion方法
      3. 当有多个拦截器时,这个方法按照正序执行
    2. postHandle:在controller的处理方法之后、在DispatcherServlet进行视图渲染之前调用
      1. 一般在这里进行请求后续加工处理操作
      2. 当有多个拦截器时,这个方法按照逆序执行
    3. afterCompletion:在DispatcherServlet进行视图渲染之后调用
      1. 一般在这里进行资源回收操作
      2. 当有多个拦截器时,这个方法按照逆序执行
  3. 容器配置(applicationContext.xml中)

     <!-- 设置拦截器 -->
     <mvc:interceptors>
         <mvc:interceptor>
             <!-- 可以拦截DispatcherServlet接收到的所有路径 -->
             <!-- 需要拦截的路径可以写多个) -->
             <!-- **代表当前目录下的所有内容(包括子目录) -->
             <mvc:mapping path="/**"/>
             <!-- 排除asset目录的所有内容 -->
             <mvc:exclude-mapping path="/asset/**"/>
             <!-- 排除所有的html-->
             <mvc:exclude-mapping path="/**/*.html"/>
             <!--            <mvc:exclude-mapping path="/**/*.png"/>-->
             <!--            <mvc:exclude-mapping path="/**/*.js"/>-->
             <!-- 拦截器对象-->
             <bean class="com.zh.interceptor.MyInterceptor1"/>
         </mvc:interceptor>
     </mvc:interceptors>
    
  4. TestController

     @RequestMapping("/test1")
     @ResponseBody
     public String test1() {
         System.out.println("test1------------------");
         return "test1 success!";
     }
    
     @RequestMapping("/test2")
     public String test2() {
         System.out.println("test2------------------");
         return "/test1.jsp";
     }
    

多个拦截器调用顺序

  1. 当配置多个拦截器时

     <!-- 设置拦截器 -->
     <mvc:interceptors>
         <mvc:interceptor>
             <!-- 可以拦截DispatcherServlet接收到的所有路径 -->
             <!-- 需要拦截的路径可以写多个) -->
             <!-- **代表当前目录下的所有内容(包括子目录) -->
             <mvc:mapping path="/**"/>
             <!-- 排除asset目录的所有内容 -->
             <mvc:exclude-mapping path="/asset/**"/>
             <!-- 拦截器对象-->
             <bean class="com.zh.interceptor.MyInterceptor1"/>
         </mvc:interceptor>
    
         <mvc:interceptor>
             <mvc:mapping path="/**"/>
             <mvc:exclude-mapping path="/asset/**"/>
             <bean class="com.zh.interceptor.MyInterceptor2"/>
         </mvc:interceptor>
    
         <mvc:interceptor>
             <mvc:mapping path="/**"/>
             <mvc:exclude-mapping path="/asset/**"/>
             <bean class="com.zh.interceptor.MyInterceptor3"/>
         </mvc:interceptor>
     </mvc:interceptors>
    
  2. 调用顺序日志如下:

     MyInterceptor1 - preHandle - /mvc05/test1
     MyInterceptor2 - preHandle - /mvc05/test1
     MyInterceptor3 - preHandle - /mvc05/test1
     test1------------------
     MyInterceptor3 - postHandle - /mvc05/test1
     MyInterceptor2 - postHandle - /mvc05/test1
     MyInterceptor1 - postHandle - /mvc05/test1
     MyInterceptor3 - afterCompletion - /mvc05/test1
     MyInterceptor2 - afterCompletion - /mvc05/test1
     MyInterceptor1 - afterCompletion - /mvc05/test1
    
    1. 从源码中可以查看有的是正向遍历、有的是逆向遍历
  3. 注意: 多个拦截器,如果其中任意一个preHandle返回值为false,其他拦截器都不会往下执行

SpringMVC的执行流程

  1. 浏览器输入一个地址,如何到达Controller的呢?
  2. DispatcherServlet继承体系如下
    1. DispatcherServlet->FrameworkServlet->HttpServletBean->HttpServlet

源码跟踪

  1. FrameworkServlet-service
    1. 客户端请求来到DispatcherServlet,首先会去调用service方法(之前讲到Servelt时讲过)
    2. DispatcherServlet没有service,会到父类FrameworkServlet查找service,找到则调用
  2. HttpServlet-service
    1. 会发现FrameworkServlet的service方法内部会调用父类的service
    2. 到父类HttpServletBean查找service没发现,会到父类HttpServlet查找,找到调用
  3. FrameworkServlet-doGet
    1. HttpServlet的service方法内部会跟求请求类型选择调用doGet还是Post
    2. 但是首先还是从DispatcherServlet查找,没找到,则到FrameworkServlet
    3. 调用FrameworkServlet中的doGet、doPost,方法内部调用的是processRequest方法
  4. FrameworkServlet-processRequest
    1. DispatcherServlet没有processRequest方法,则到FrameworkServlet中查找调用
    2. processRequest内部调用doService
  5. DispatcherServlet-doService
    1. DispatcherServlet有doService方法,则调用
  6. DispatcherServlet-doDispatch
    1. doService方法本质调用了doDispatch
    2. doDispatch的核心作用就是分发控制器处理

doDispatch源码分析

  1. checkMultipart:检查是否是ultipart格式的请求
  2. getHandler:找到第一个不为空的Handler(HandlerExecutionChain类型)-作用是后期用来处理Controller跟拦截器
  3. getHandlerAdapter:获取HandlerAdapter
  4. applyPreHandle:HandlerExecutionChain遍历所有拦截器,调用所有拦截器的preHandle方法
  5. handle:HandlerAdapter调用handle,内部执行的是Controller的处理方法
  6. applyPostHandle: applyPreHandle:HandlerExecutionChain遍历所有的拦截器,调用所有拦截器的postHandle方法
  7. processDispatchResult:处理异常、渲染视图(转发、重定向)
    1. 内部调用render方法,内部InternalResourceView调用render方法
  8. triggerAfterCompletion:在catch出现异常时调用,内部HandlerExecutionChain调用triggerAfterCompletion,遍历所有的拦截器,调用afterCompletion方法
  9. applyAfterConcurrentHandlingStarted:在finally处,HandlerExecutionChain调用applyAfterConcurrentHandlingStarted,本质任然是遍历所有的拦截器,调用所有拦截器的afterConcurrentHandlingStarted方法

图1

简要步骤

  1. mappedHandler=getHandler:获取Handler (用来处理controller、拦截器)
  2. ha =getHandlerAdapter:获取Handler Adapter
  3. mappedHandler.applyPreHandle:调用拦截器的preHandle方法
  4. mv=ha.handle:调用controller的处理方法
    1. 如果返回的不是视图页面(比如是JSON数据) ,在这个步骤中就已经将数据写回给客户端
  5. mappedHandler.applyPostHandle:调用拦截器的postHandle方法
  6. processDispatchResult:处理异常、渲染视图(转发、重定向)
  7. triggerAfterCompletion:调用拦截器的afterCompletion方法

SSM-XML整合

  1. 创建一个web项目(步骤略)
  2. 添加依赖pom.xml

     <!-- 打包方式-->
     <packaging>war</packaging>
     <!-- 设置编码 -->
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
     <dependencies>
         <!-- Servlet + JSP -->
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
             <version>4.0.1</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>javax.servlet.jsp</groupId>
             <artifactId>javax.servlet.jsp-api</artifactId>
             <version>2.3.3</version>
             <scope>provided</scope>
         </dependency>
    
         <!-- Log -->
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
             <version>1.2.3</version>
         </dependency>
    
         <!-- Spring + SpringMVC -->
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-webmvc</artifactId>
             <version>5.2.8.RELEASE</version>
         </dependency>
         <dependency>
             <groupId>org.aspectj</groupId>
             <artifactId>aspectjweaver</artifactId>
             <version>1.9.6</version>
         </dependency>
         <dependency>
             <groupId>org.aspectj</groupId>
             <artifactId>aspectjrt</artifactId>
             <version>1.9.6</version>
         </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
             <version>2.11.0</version>
         </dependency>
    
         <!-- MyBatis -->
         <dependency>
             <groupId>org.mybatis</groupId>
             <artifactId>mybatis</artifactId>
             <version>3.5.5</version>
         </dependency>
         <dependency>
             <groupId>org.mybatis</groupId>
             <artifactId>mybatis-spring</artifactId>
             <version>2.0.5</version>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-jdbc</artifactId>
             <version>5.2.8.RELEASE</version>
         </dependency>
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>5.1.49</version>
         </dependency>
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>druid</artifactId>
             <version>1.1.22</version>
         </dependency>
    
         <!-- junit -->
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.13</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
    
  3. 创建domain

     public class Skill {
         private Integer id;
         private Date createdTime;
         private String name;
         private Integer level;
         //get/set方法略
     }
    
  4. 创建Dao、mapper

     public interface SkillDao {
         boolean save(Skill skill);
         boolean update(Skill skill);
         boolean remove(Integer id);
            
         @Select("SELECT * FROM skill")
         List<Skill> list();
         @Select("SELECT * FROM skill WHERE id = #{id}")
         Skill get(Integer id);
     }
     //resources/mappers/Skill.xml
     <?xml version="1.0" encoding="UTF-8" ?>
     <!DOCTYPE mapper
             PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
     <mapper namespace="com.zh.dao.SkillDao">
         <insert id="save" parameterType="Skill">
             INSERT INTO skill(name, level) VALUES (#{name}, #{level})
         </insert>
        
         <update id="update" parameterType="Skill">
             UPDATE skill SET name = #{name}, level = #{level} WHERE id = #{id}
         </update>
        
         <delete id="remove" parameterType="int">
             DELETE FROM skill WHERE id = #{id}
         </delete>
     </mapper>
    
  5. 创建service

     public interface SkillService {
         boolean save(Skill skill);
         boolean remove(Integer id);
         List<Skill> list();
         Skill get(Integer id);
     }
        
     @Service
     public class SkillServiceImpl implements SkillService, ApplicationContextAware {
         @Autowired
         private SkillDao dao;
        
         @Override
         public boolean save(Skill skill) {
             Integer id = skill.getId();
             if (id == null || id < 1) {
                 return dao.save(skill);
             }
             return dao.update(skill);
         }
        
         @Override
         public boolean remove(Integer id) {
             return dao.remove(id);
         }
        
         @Override
         public List<Skill> list() {
             return dao.list();
         }
        
         @Override
         public Skill get(Integer id) {
             return dao.get(id);
         }
            
         //查看当前容器是哪个
         @Override
         public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
             System.out.println(applicationContext);
         }
     }
    
  6. 创建Controller

     @Controller
     @RequestMapping("/skills")
     public class SkillController implements ApplicationContextAware {
         @Autowired
         private SkillService service;
         // 读:GET
         // 写:POST
        
         @GetMapping("/list")
         @ResponseBody
         public List<Skill> list() {
             return service.list();
         }
        
         @GetMapping("/get")
         @ResponseBody
         public Skill get(Integer id) {
             return service.get(id);
         }
        
         @PostMapping("/save")
         @ResponseBody
         public String save(Skill skill) {
             return service.save(skill) ? "保存成功" : "保存失败";
         }
        
         @PostMapping("/remove")
         @ResponseBody
         public String remove(Integer id) {
             return service.remove(id) ? "删除成功" : "删除失败";
         }
         //查看当前容器是哪个
         @Override
         public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
             System.out.println(applicationContext);
         }
     }
    
  7. 创建applicationContext.xml
  8. web.xml中添加DispatcherServlet

     <servlet>
         <servlet-name>DispatcherServlet</servlet-name>
         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
         <init-param>
             <param-name>contextConfigLocation</param-name>
             <!-- 加载Spring的IoC容器 -->
             <param-value>classpath:applicationContext.xml</param-value>
         </init-param>
         <load-on-startup>0</load-on-startup>
     </servlet>
        
     <servlet-mapping>
         <servlet-name>DispatcherServlet</servlet-name>
         <url-pattern>/</url-pattern>
     </servlet-mapping>
    

父子容器

  1. 引入:
    1. 前面已经学习过Spring+Mybatis的整合,就剩下整合SpringMVC
    2. 目前存在的问题
      1. 从web.xml中的DispatcherServlet加载IoC容器(applicationContext.xml)可以看到,所有的bean都存储在同一个IoC容器中
      2. 无法避免service注入Controller等问题:本来service应该服务于Controller,但是有些人也可以这么写,在SkillServiceImpl里面,这样一来就乱套了

         //主动注入一个Controller
         @Autowired
         private SkillController controller;
         //在成员方法中调用controller的方法
        
    3. 解决方案 —-采用父子容器
      1. 搞2个IoC容器,一个专门给SpringMVC使用(Controller)-SpringMVC容器,一个Spring默认的容器(Service、dao)-Spring容器
      2. 比如上面的service注入Controller成员问题解决
        1. 这个注入@Autowired是到当前Spring容器中去找这个对象,无法到SpringMVC容器中查找对象
      3. 问题: controller要通过@Autowired注入一个service成员,它在SpringMVC容器中是找不到的,那么它也不能到Spring容器中去找,该怎么解决呢?
      4. SpringMVC容器通过parent成为Spring容器的子容器
        1. 子容器通过parent能找到父容器,但是父容器是无法找到子容器的,这样以来,controller的service在SpringMVC容器中找不到时,会通过parent到父容器Spring容器中查找
        2. 反过来父容器Spring容器找不到bean对象时,无法通过其他方式到其他容器中找
  2. 父子容器 图1
    1. 特点
      1. 父容器和子容器是相互隔离的,它们内部可以存在名称相同的bean
      2. 子容器可以访问父容器中的bean,而父容器不能访问子容器中的bean
      3. 调用子容器的getbean方法时,会沿着当前容器开始向父容器进行查找,直到找到对应的bean为止
      4. 子容器中可以通过注入父容器中的bean,而父容器中无法注入子容器中的bean
    2. 优点
      1. 采用父子容器可以避免依赖层次混乱(service注入controller)
      2. 将相互不关心的东西隔开,可以有效避免一些不必要的错误,而父子容器加载的速度也会快一些
  3. 父子容器的配置文件建议
    1. 子容器的配置文件:dispatcherServlet.xml

       <context:component-scan base-package="com.zh.controller"/>
      
       <mvc:annotation-driven>
           <!-- 响应数据设置编码类型 -->
           <mvc:message-converters>
               <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                   <property name="defaultCharset" value="UTF-8"/>
               </bean>
               <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                   <property name="defaultCharset" value="UTF-8"/>
               </bean>
           </mvc:message-converters>
       </mvc:annotation-driven>
          
       <mvc:default-servlet-handler/>    
      
    2. 父容器的配置文件:applicationContext.xml

       <context:component-scan base-package="com.zh.service"/>
      
       <context:property-placeholder location="classpath:main.properties"/>
          
       <!-- 数据源(Druid) -->
       <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
           <property name="driverClassName" value="${jdbc.driverClassName}"/>
           <property name="url" value="${jdbc.url}"/>
           <property name="username" value="${jdbc.username}"/>
           <property name="password" value="${jdbc.password}"/>
       </bean>
          
       <!-- 创建SqlSessionFactory -->
       <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
           <!-- 数据源 -->
           <property name="dataSource" ref="dataSource"/>
           <!-- 这个包底下的类会自动设置别名(一般是领域模型) -->
           <property name="typeAliasesPackage" value="${mybatis.typeAliasesPackage}"/>
           <!-- 查找mappers的映射文件 -->
           <property name="mapperLocations" value="${mybatis.mapperLocations}"/>
       </bean>
          
       <!-- 扫描dao -->
       <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
           <!-- 设置SqlSessionFactoryBean的id -->
           <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
           <!-- 设置dao的包 -->
           <property name="basePackage" value="com.zh.dao"/>
       </bean>
          
       <!-- 事务管理器 -->
       <bean id="txMgr"
             class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
           <property name="dataSource" ref="dataSource"/>
       </bean>
          
       <!-- 附加代码:事务管理代码 -->
       <tx:advice id="txAdvice" transaction-manager="txMgr">
           <tx:attributes>
               <tx:method name="*"/>
               <tx:method name="list*" read-only="true"/>
               <tx:method name="get*" read-only="true"/>
           </tx:attributes>
       </tx:advice>
          
       <!-- 切面 -->
       <aop:config>
           <aop:pointcut id="pc" expression="within(com.zh.service..*)"/>
           <aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/>
       </aop:config>
      
  4. web.xml中配置父子容器
    1. 子容器的加载创建(将web.xml中的容器加载修改为子容器dispatcherServlet.xml)

       <servlet>
           <servlet-name>DispatcherServlet</servlet-name>
           <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
           <init-param>
               <param-name>contextConfigLocation</param-name>
               <!-- 子容器 -->
               <param-value>classpath:dispatcherServlet.xml</param-value>
           </init-param>
            <!-- 监听web项目的部署,部署完成加载dispatcherServlet.xml,将内容放入到子容器,并自动设置与父容器的关系 -->
           <load-on-startup>0</load-on-startup>
       </servlet>
      
    2. 父容器的加载创建

       <!-- 父容器 -->
       <context-param>
           <param-name>contextConfigLocation</param-name>
           <param-value>classpath:applicationContext.xml</param-value>
       </context-param>
              
       <!-- 监听web项目的部署,部署完成加载applicationContext.xml,将内容放入到父容器 -->
       <listener>
           <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
       </listener>
      
    3. 添加post请求乱码问题

       <!-- post请求解决乱码问题 -->
       <filter>
           <filter-name>CharacterEncodingFilter</filter-name>
           <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
           <init-param>
               <param-name>encoding</param-name>
               <param-value>UTF-8</param-value>
           </init-param>
       </filter>
          
       <filter-mapping>
           <filter-name>CharacterEncodingFilter</filter-name>
           <url-pattern>/*</url-pattern>
       </filter-mapping>
      
  5. main.properties

     # jdbc
     jdbc.driverClassName=com.mysql.jdbc.Driver
     jdbc.url=jdbc:mysql://localhost:3306/test-mybatis
     jdbc.username=root
     jdbc.password=root
        
     # mybatis
     mybatis.typeAliasesPackage=com.zh.domain
     mybatis.mapperLocations=classpath:mappers/*.xml
     #mybatis.mapperScan=com.zb.dao
     mybatis.configLocation=classpath:mybatis-config.xml
    

MyBatis整合补充

  1. 如果将Mapper文件跟dao放在相同目录下,且文件名跟dao一样,那么通过MapperScannerConfigurer(专门来扫描dao的)就可以找到Mapper文件,不需要再配置SqlSessionFactoryBean的mapperLocations属性,如下:
    1. 将mappers/skill.xml删除,在dao文件夹下新建一个SkillDao.xml文件,内容跟skil.xml一样
    2. 将applicationContext.xml中扫描mappers代码删除

        <property name="mapperLocations" value="${mybatis.mapperLocations}"/>
      
    3. 注意: Maven要配置打包非resources下资源文件(配置详见maven章节)
  2. 可以设置SqlSessionFactoryBean的configLocation属性,指定MyBatis核心配置文件的位置,使用mybatis-config.xml配置mybatis单独相关的内容。

     <property name="configLocation" value="classpath:mybatis-config.xml"/>
    
    1. mybatis-config.xml内容如下

       <!--开启驼峰标识自动抓取为下划线功能,这个是mybaits自己相关的配置,因此单独搞到mybaits自己的文件中去-->
       <configuration>
           <settings>
               <setting name="mapUnderscoreToCamelCase" value="true"/>
           </settings>
       </configuration>
      
Table of Contents