SSM-SpringMVC(四)-拦截器、springVC执行流程、SSM整合
拦截器(Interceptor)
- 拦截器(Interceptor))的功能,跟过滤器(Filter)有点类似,但是有本质区别
- 过滤器
- 是Servlet规范的一部分
- 能拦截任意请求,在请求抵达Servlet之前、响应抵达客户端之前拦截
- 常用于:编码设置、登录校验等
- 拦截器
- 是SpringMVC的一部分
- 只能拦截DispatcherServlet拦截到的内容,一般用来拦截Controller,请求到达Controller之前。
- 常用于:抽取Controller的公共代码
拦截器使用步骤
-
自定义一个类实现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()); } }
- HandlerInterceptor方法分析
- preHandle:在controller的处理方法之前调用
- 一般在这里进行初始化、请求预处理操作
- 如果返回false,那么后序将不会再调用controller的处理方法、postHandle、afterCompletion方法
- 当有多个拦截器时,这个方法按照正序执行
- postHandle:在controller的处理方法之后、在DispatcherServlet进行视图渲染之前调用
- 一般在这里进行请求后续加工处理操作
- 当有多个拦截器时,这个方法按照逆序执行
- afterCompletion:在DispatcherServlet进行视图渲染之后调用
- 一般在这里进行资源回收操作
- 当有多个拦截器时,这个方法按照逆序执行
- preHandle:在controller的处理方法之前调用
-
容器配置(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>
-
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"; }
多个拦截器调用顺序
-
当配置多个拦截器时
<!-- 设置拦截器 --> <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>
-
调用顺序日志如下:
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
- 从源码中可以查看有的是正向遍历、有的是逆向遍历
-
注意: 多个拦截器,如果其中任意一个preHandle返回值为false,其他拦截器都不会往下执行
SpringMVC的执行流程
- 浏览器输入一个地址,如何到达Controller的呢?
- DispatcherServlet继承体系如下
- DispatcherServlet->FrameworkServlet->HttpServletBean->HttpServlet
源码跟踪
- FrameworkServlet-service
- 客户端请求来到DispatcherServlet,首先会去调用service方法(之前讲到Servelt时讲过)
- DispatcherServlet没有service,会到父类FrameworkServlet查找service,找到则调用
- HttpServlet-service
- 会发现FrameworkServlet的service方法内部会调用父类的service
- 到父类HttpServletBean查找service没发现,会到父类HttpServlet查找,找到调用
- FrameworkServlet-doGet
- HttpServlet的service方法内部会跟求请求类型选择调用doGet还是Post
- 但是首先还是从DispatcherServlet查找,没找到,则到FrameworkServlet
- 调用FrameworkServlet中的doGet、doPost,方法内部调用的是processRequest方法
- FrameworkServlet-processRequest
- DispatcherServlet没有processRequest方法,则到FrameworkServlet中查找调用
- processRequest内部调用doService
- DispatcherServlet-doService
- DispatcherServlet有doService方法,则调用
- DispatcherServlet-doDispatch
- doService方法本质调用了doDispatch
- doDispatch的核心作用就是分发控制器处理
doDispatch源码分析
- checkMultipart:检查是否是ultipart格式的请求
- getHandler:找到第一个不为空的Handler(HandlerExecutionChain类型)-作用是后期用来处理Controller跟拦截器
- getHandlerAdapter:获取HandlerAdapter
- applyPreHandle:HandlerExecutionChain遍历所有拦截器,调用所有拦截器的preHandle方法
- handle:HandlerAdapter调用handle,内部执行的是Controller的处理方法
- applyPostHandle: applyPreHandle:HandlerExecutionChain遍历所有的拦截器,调用所有拦截器的postHandle方法
- processDispatchResult:处理异常、渲染视图(转发、重定向)
- 内部调用render方法,内部InternalResourceView调用render方法
- triggerAfterCompletion:在catch出现异常时调用,内部HandlerExecutionChain调用triggerAfterCompletion,遍历所有的拦截器,调用afterCompletion方法
- applyAfterConcurrentHandlingStarted:在finally处,HandlerExecutionChain调用applyAfterConcurrentHandlingStarted,本质任然是遍历所有的拦截器,调用所有拦截器的afterConcurrentHandlingStarted方法
简要步骤
- mappedHandler=getHandler:获取Handler (用来处理controller、拦截器)
- ha =getHandlerAdapter:获取Handler Adapter
- mappedHandler.applyPreHandle:调用拦截器的preHandle方法
- mv=ha.handle:调用controller的处理方法
- 如果返回的不是视图页面(比如是JSON数据) ,在这个步骤中就已经将数据写回给客户端
- mappedHandler.applyPostHandle:调用拦截器的postHandle方法
- processDispatchResult:处理异常、渲染视图(转发、重定向)
- triggerAfterCompletion:调用拦截器的afterCompletion方法
SSM-XML整合
- 创建一个web项目(步骤略)
-
添加依赖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>
-
创建domain
public class Skill { private Integer id; private Date createdTime; private String name; private Integer level; //get/set方法略 }
-
创建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>
-
创建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); } }
-
创建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); } }
- 创建applicationContext.xml
-
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>
父子容器
- 引入:
- 前面已经学习过Spring+Mybatis的整合,就剩下整合SpringMVC
- 目前存在的问题:
- 从web.xml中的DispatcherServlet加载IoC容器(applicationContext.xml)可以看到,所有的bean都存储在同一个IoC容器中
-
无法避免service注入Controller等问题:本来service应该服务于Controller,但是有些人也可以这么写,在SkillServiceImpl里面,这样一来就乱套了
//主动注入一个Controller @Autowired private SkillController controller; //在成员方法中调用controller的方法
- 解决方案 —-采用父子容器
- 搞2个IoC容器,一个专门给SpringMVC使用(Controller)-SpringMVC容器,一个Spring默认的容器(Service、dao)-Spring容器
- 比如上面的service注入Controller成员问题解决
- 这个注入
@Autowired
是到当前Spring容器中去找这个对象,无法到SpringMVC容器中查找对象
- 这个注入
- 问题: controller要通过
@Autowired
注入一个service成员,它在SpringMVC容器中是找不到的,那么它也不能到Spring容器中去找,该怎么解决呢? - 让SpringMVC容器通过parent成为Spring容器的子容器
- 子容器通过parent能找到父容器,但是父容器是无法找到子容器的,这样以来,controller的service在SpringMVC容器中找不到时,会通过parent到父容器Spring容器中查找
- 反过来父容器Spring容器找不到bean对象时,无法通过其他方式到其他容器中找
- 父子容器
- 特点
- 父容器和子容器是相互隔离的,它们内部可以存在名称相同的bean
- 子容器可以访问父容器中的bean,而父容器不能访问子容器中的bean
- 调用子容器的getbean方法时,会沿着当前容器开始向父容器进行查找,直到找到对应的bean为止
- 子容器中可以通过注入父容器中的bean,而父容器中无法注入子容器中的bean
- 优点
- 采用父子容器可以避免依赖层次混乱(service注入controller)
- 将相互不关心的东西隔开,可以有效避免一些不必要的错误,而父子容器加载的速度也会快一些
- 特点
- 父子容器的配置文件建议
-
子容器的配置文件: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/>
-
父容器的配置文件: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>
-
- web.xml中配置父子容器
-
子容器的加载创建(将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>
-
父容器的加载创建
<!-- 父容器 --> <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>
-
添加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>
-
-
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整合补充
- 如果将Mapper文件跟dao放在相同目录下,且文件名跟dao一样,那么通过MapperScannerConfigurer(专门来扫描dao的)就可以找到Mapper文件,不需要再配置SqlSessionFactoryBean的mapperLocations属性,如下:
- 将mappers/skill.xml删除,在dao文件夹下新建一个SkillDao.xml文件,内容跟skil.xml一样
-
将applicationContext.xml中扫描mappers代码删除
<property name="mapperLocations" value="${mybatis.mapperLocations}"/>
- 注意: Maven要配置打包非resources下资源文件(配置详见maven章节)
-
可以设置SqlSessionFactoryBean的configLocation属性,指定MyBatis核心配置文件的位置,使用mybatis-config.xml配置mybatis单独相关的内容。
<property name="configLocation" value="classpath:mybatis-config.xml"/>
-
mybatis-config.xml内容如下
<!--开启驼峰标识自动抓取为下划线功能,这个是mybaits自己相关的配置,因此单独搞到mybaits自己的文件中去--> <configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>
-