目录
4. DispatcherServlet的initHandlerMappings方法
一. springmvc的初始化
1. 根IOC容器初始化
在web环境中使用ioc容器,需要将ioc容器的初始化与web服务器的启动集成在一起,关键在于web.xml配置文件中的监听器ContextLoaderListener:
<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">
<!-- web应用的初始化参数 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<!-- 监听web应用中ServletContext的创建与销毁 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- servlet配置... -->
</web-app>
如图1所示,随着web服务器的启动,根IOC容器(ROOT WAC)完成了初始化:
2. DispatcherServlet的IOC容器初始化
DispatcherServlet作为一种servlet,同样遵循以下的生命周期:
① 构造阶段(调用1次构造器):load-on-startup参数<0,第一次请求servlet时调用;load-on-startup参数≥0,web应用被servlet容器加载时调用
② 初始化阶段(调用1次init方法):在创建完实例后立即被调用,用于初始化当前servlet
③ 提供服务阶段(调用多次service方法):每次响应请求都会调用service方法
④ 销毁阶段(调用1次destroy方法):当前servlet所在的web应用被卸载前调用,用于释放该servlet所占用的资源
根据图2的DispatcherServlet的UML图,它从父类FrameworkServlet继承了属性:WebApplicationContext webApplicationContext,从祖先类HttpServletBean继承了init方法。DispatcherServlet的IOC容器初始化就是在servlet初始化阶段完成的。
DispatcherServlet的初始化流程:HttpServletBean的init方法 => FrameworkServlet的initServletBean方法 => FrameworkServlet的initWebApplicationContext方法 => DispatcherServlet的initStrategies方法。initWebApplicationContext方法完成了与图1相似的流程:通过反射创建IOC容器、wac记录servletContext、wac.refresh()、servletContext将wac记录到attribute。DispatcherServlet的initStrategies方法如下:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
//重点关注initHandlerMappings方法
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
3. HandlerMapping类
在分析initHandlerMappings方法之前,先从图3了解一下什么是HandlerMapping。
ps:UML类图中不同线型表示的关系可以参考https://www.cnblogs.com/shindo/p/5579191.html
根据图3,得到如下信息:
① DispatcherServlet持有多个HandlerMapping;
② HandlerMapping有个方法getHandler(HttpServletRequest)返回HandlerExecutionChain,而HandlerExecutionChain有2个重要属性:Object handler、HandlerInterceptor[] interceptors。
书中在提到HandlerExecutionChain时,直接指出:handler属性就是业务代码中的controller。在目前主流应用场景中,handler属性应该是controller中标注@RequestMapping的方法。DispatcherServlet就是通过interceptors(拦截器链)和handler实现业务逻辑的。
4. DispatcherServlet的initHandlerMappings方法
initHandlerMappings方法是为DispatcherServlet的handlerMappings属性赋值,而值的来源:在IOC容器中搜索HandlerMapping类型的bean,如果未搜到,则根据配置文件创建默认的BeanNameUrlHandlerMapping、RequestMappingHandlerMapping。无论是从IOC容器中搜索,还是创建指定的bean,都会触发如图4所示的调用链:
图4的ApplicationContextAwareProcessor是在ioc容器refresh阶段的prepareBeanFactory方法中默认设定的一种BPP处理器。图4执行链路的最后是AbstractHandlerMapping的initApplicationContext方法:
@Override
protected void initApplicationContext() throws BeansException {
extendInterceptors(this.interceptors);
detectMappedInterceptors(this.adaptedInterceptors);
initInterceptors();
}
其中detectMappedInterceptors方法的主要逻辑是:找出所有类型是MappedInterceptor的bean,并放入List<HandlerInterceptor> adaptedInterceptors中。
如图5所示,AbstractHandlerMapping的实现类分为两大支脉,一支是AbstractUrlHandlerMapping(基于类级别的Handler,过时用法)、另一支是AbstractHandlerMethodMappping(基于方法级别的Handler,对应目前主流的@Controller、@RequestMapping注解用法),分别以不同方式完成了处理器的初始化。
AbstractHandlerMethodMapping注册handler的过程较为复杂,不展开分析了,主要是3个新出现的类:
RequestMappingInfo:负责提取、转化@RequestMapping注解的配置信息
MappingRegistry:注册中心,多个map维护多种映射关系
HandlerMethod:对@RequestMapping标注的方法的包装(属性method是@RequestMapping标注的方法、属性bean是controller类实例)
tips:注册中心采用了非公平的可重入读写锁ReentrantReadWriteLock,在注册register、注销unregister时使用写锁,而在服务运行过程中使用读锁。读写锁的互斥性保证了注册或注销时没有请求能进入处理流程;锁的可重入性保证不阻塞请求,只有处理完所有请求才能获得写锁进行注销。
二. springmvc的请求处理过程
DispatchServlet完成初始化后,可以处理http请求了。当http请求到达时,沿着Servlet类的service方法 => ... => DispatchServlet类的doDispatch方法,doDispatch方法的主要流程如下:
1. 拦截器
从上图流程和注释可以看出,对于任意http请求,普通HandlerInterceptor会直接生效,而MappedInterceptor会匹配路径决定是否拦截处理请求。
2. 处理器
上图重点分析处理器是方法级别的情况(即采用@RequestMapping注解)。列举了3种情况的HandlerMethodReturnValueHandler:
(1)异步servlet请求
参考博客1:什么是servlet异步请求 ,注意:返回Callable这种方式不复用线程,生产环境不推荐使用这种方式,应该使用线程池代替 → 返回值采用WebAsyncTask包装Callable。
参考博客2:tomcat如何实现异步servlet,ps:异步servlet请求处理过程会进入doDispatch方法2次,执行2次拦截链的preHandle,执行1次拦截链的postHandle。
从返回值处理器CallableMethodReturnValueHandler中可以看到:
① 启动异步逻辑:startCallableProcessing(...)
② 子线程完成业务逻辑后 或 异常情况,执行:setConcurrentResultAndDispatch(...),该方法存在类似参考博客2的AsyncContext.complete()的逻辑,执行ActionCode.ASYNC_COMPLETE分支
(2)@ResponseBody
该注解有专门的处理器,将controller的方法的返回对象按指定格式写到response中,通常用来返回JSON数据或者是XML
(3)ModelAndView
spring集成了第三方视图方案,可以返回多种视图格式
3. 视图解析器
在业务逻辑new ModelAndView时,设置的是viewName,视图解析器用于从viewName得到view
三. springmvc Demo
(1)dispatchServlet的ioc容器配置:
<?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"
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">
<context:annotation-config />
<context:component-scan base-package="com.webtest"/>
<!-- 将viewName作为beanName,在ioc容器中搜索得到view对象 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/outdated">outdatedController</prop>
</props>
</property>
</bean>
<bean id="outdatedController" class="com.webtest.controller.OutdatedController"/>
<bean id="requestMappingHandlerMapping"
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors">
<list>
<bean class="com.webtest.MyInterceptor"></bean>
</list>
</property>
</bean>
<bean id="myInterceptor" class="com.webtest.MyInterceptor"></bean>
</beans>
注意:敲黑板!!!由于上述配置中使用了context:annotation-config、context:component-scan标签,demo启动时会报错:
信息: Initializing Spring DispatcherServlet 'dispatcher'
ERROR DispatcherServlet Context initialization failed
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/context]
Offending resource: ServletContext resource [/WEB-INF/dispatcher-servlet.xml]
at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:72)
at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:119)
at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:111)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.error(BeanDefinitionParserDelegate.java:281)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1388)
参考博客:spring系列1-基于源码搭建普通工程&web工程的五-2-(5)引入其它spring模块的问题
(2)类级别的处理器:
package com.webtest.controller;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class OutdatedController extends AbstractController {
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
return new ModelAndView("showJsp", "testcontroller", "hello world!");
}
}
(3)方法级别的处理器:
package com.webtest.controller;
import com.webtest.view.MyExcelView;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.servlet.ModelAndView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.concurrent.Callable;
@Controller
@RequestMapping("/test")
public class TestController {
@RequestMapping("/abc")
@ResponseBody
public String abc() {
return "hello abc!";
}
@RequestMapping(value = "/async1", produces = "text/plain;charset=UTF-8")
@ResponseBody
public Callable<String> callable() {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
StringBuilder sb = new StringBuilder();
sb.append("主线程开始:").append(dateFormat.format(new Date()));
System.out.println(sb.toString());
Callable<String> result = () -> {
Thread.sleep(5 * 1000);
String s = "子线程执行:" + dateFormat.format(new Date());
System.out.println(s);
return sb.append("\n").append(s).toString();
};
System.out.println("主线程退出......");
return result;
}
@RequestMapping("/async2")
@ResponseBody
public WebAsyncTask<String> webAsyncTask() {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
StringBuilder sb = new StringBuilder();
sb.append("主线程开始:").append(dateFormat.format(new Date()));
System.out.println(sb.toString());
Callable<String> result = () -> {
Thread.sleep(5 * 1000);
String s = "子线程执行:" + dateFormat.format(new Date());
System.out.println(s);
return sb.append("\n").append(s).toString();
};
WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(10000, result);
System.out.println("主线程退出......");
return webAsyncTask;
}
@RequestMapping("/getExcel")
public ModelAndView getExcel() {
return new ModelAndView(new MyExcelView(), new HashMap<>(0));
}
@RequestMapping("/getExcel2")
public ModelAndView getExcel2() {
return new ModelAndView("myExcelView", new HashMap<>(0));
}
}
自定义的excel视图:
package com.webtest.view;
import com.webtest.ExcelUtil;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.document.AbstractXlsView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
@Component
public class MyExcelView extends AbstractXlsView {
@Override
protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception {
Set<String> names = new HashSet<>();
names.add("age");
names.add("name");
List<Person> people = new ArrayList<>();
people.add(new Person(13, "Lilei"));
people.add(new Person(17, "Hanmeimei"));
ExcelUtil.setSheet(workbook, people, Person.class, names);
}
static class Person {
private int age;
private String name;
Person(int age, String name) {
this.age = age;
this.name = name;
}
}
}
package com.webtest;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Set;
public class ExcelUtil {
private static String SUFFIX = ".xls";
/**
* 将data的录入到文件的sheet中,每调用一次新增一个sheet
*
* @param workbook 文件
* @param allowFieldNames 允许导入文件的字段名称
*/
public static <T> void setSheet(Workbook workbook, List<T> data, Class<T> clazz, Set<String> allowFieldNames) {
Sheet sheet = workbook.createSheet();
Field[] fields = clazz.getDeclaredFields();
setTableHeader(sheet, fields, allowFieldNames);
setTableData(sheet, data, fields, allowFieldNames);
}
/**
* 设置表头
*/
private static void setTableHeader(Sheet sheet, Field[] fields, Set<String> allowFieldNames) {
if (fields == null) {
return;
}
Row row = sheet.createRow(0);
int count = 0;
for (Field f : fields) {
if (allowFieldNames.contains(f.getName())) {
sheet.setColumnWidth(count, f.getName().length() * 256 * 2);
Cell cell = row.createCell(count);
cell.setCellValue(f.getName());
count++;
}
}
}
/**
* 设置表数据
*/
private static <T> void setTableData(Sheet sheet, List<T> data, Field[] fields, Set<String> allowFieldNames) {
if (CollectionUtils.isEmpty(data) || fields == null) {
return;
}
for (int i = 0; i < data.size(); i++) {
Row row = sheet.createRow(i + 1);
int count = 0;
for (Field f : fields) {
if (allowFieldNames.contains(f.getName())) {
f.setAccessible(true);
Object fieldValue = null;
try {
fieldValue = f.get(data.get(i));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
Class fieldClazz = f.getType();
String strValue = fieldClazz.equals(Date.class) ?
DateFormat.getDateInstance().format(fieldValue) : String.valueOf(fieldValue);
row.createCell(count).setCellValue(strValue);
count++;
}
}
}
}
}
(4)自定义拦截器:
package com.webtest;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("myInterceptor pre handle....");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
System.out.println("myInterceptor post handle....");
}
}
参考资料
《spring技术内幕》