手写SpringMVC
框架之前呢,我觉得有必要先了解SpringMVC
的请求处理流程以及高级特性。
1、请求处理流程
流程说明:
第一步:用户发送请求至前端控制器DispatcherServlet
。
第二步:DispatcherServlet
收到请求调用HandlerMapping
处理器映射器。
第三步:处理器映射器根据请求Url
找到具体的Handler
(后端控制器),生成处理器对象及处理器拦截器(如果有则生成)一并返回DispatcherServlet
。
第四步:DispatcherServlet
调用HandlerAdapter
处理器适配器去调用Handler
。
第五步:处理器适配器执行Handler
。
第六步:Handler执行完成给处理器适配器返回ModelAndView
。
第七步:处理器适配器向前端控制器返回 ModelAndView
,ModelAndView
是SpringMVC
框架的一个 底层对 象,包括 Model
和 View
。
第八步:前端控制器请求视图解析器去进行视图解析,根据逻辑视图名来解析真正的视图。
第九步:视图解析器向前端控制器返回View
。
第十步:前端控制器进行视图渲染,就是将模型数据(在 ModelAndView
对象中)填充到 request
域。
第十一步:前端控制器向用户响应结果。
2、Spring MVC 九大组件
HandlerMapping(处理器映射器)
HandlerMapping
是用来查找 Handler
的,也就是处理器,具体的表现形式可以是类,也可以是方法。比如,标注了@RequestMapping
的每个方法都可以看成是一个Handler
。Handler
负责具体实际的请求处理,在请求到达后,HandlerMapping
的作用便是找到请求相应的处理器 Handler
和 Interceptor
。
HandlerAdapter(处理器适配器)
HandlerAdapter
是一个适配器。因为 Spring MVC
中 Handler
可以是任意形式的,只要能处理请求即可。但是把请求交给 Servlet
的时候,由于 Servlet
的方法结构都是 doService(HttpServletRequest req,HttpServletResponse resp)
形式的,要让固定的 Servlet
处理方法调用 Handler
来进行处理,便是 HandlerAdapter
的职责。
HandlerExceptionResolver
HandlerExceptionResolver
用于处理 Handler
产生的异常情况。它的作用是根据异常设置
ModelAndView
,之后交给渲染方法进行渲染,渲染方法会将 ModelAndView
渲染成⻚面。
ViewResolver
ViewResolver
即视图解析器,用于将String
类型的视图名和Locale
解析为View
类型的视图,只有一 个resolveViewName()
方法。从方法的定义可以看出,Controller
层返回的String
类型视图名 viewName
最终会在这里被解析成为View
。View
是用来渲染⻚面的,也就是说,它会将程序返回的参数和数据填入模板中,生成html
文件。ViewResolver
在这个过程主要完成两件事情: ViewResolver
找到渲染所用的模板(第一件大事)和所用的技术(第二件大事,其实也就是找到视图的类型,如JSP
)并填入参数。默认情况下,Spring MVC
会自动为我们配置一个 InternalResourceViewResolver
,是针对 JSP
类型视图的。
RequestToViewNameTranslator
RequestToViewNameTranslator
组件的作用是从请求中获取 ViewName
,因为 ViewResolver
根据 ViewName
查找 View
,但有的 Handler
处理完成之后,没有设置 View
,也没有设置 ViewName
, 便要通过这个组件从请求中查找 ViewName
。
LocaleResolver
ViewResolver
组件的 resolveViewName
方法需要两个参数,一个是视图名,一个是 Locale
。 LocaleResolver
用于从请求中解析出 Locale
,比如中国 Locale
是 zh-CN
,用来表示一个区域。这 个组件也是 i18n
的基础。
ThemeResolver
ThemeResolver
组件是用来解析主题的。主题是样式、图片及它们所形成的显示效果的集合。 Spring MVC
中一套主题对应一个 properties
文件,里面存放着与当前主题相关的所有资源,如图片、CSS
样式等。创建主题非常简单,只需准备好资源,然后新建一个“主题名.properties
”并将资源设置进去,放在classpath
下,之后便可以在⻚面中使用了。SpringMVC
中与主题相关的类有 ThemeResolver
、ThemeSource
和Theme
。ThemeResolver
负责从请求中解析出主题名, ThemeSource
根据主题名找到具体的主题,其抽象也就是Theme
,可以通过Theme
来获取主题和具体的资源。
MultipartResolver
MultipartResolver
用于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest
来实现。MultipartHttpServletRequest
可以通过 getFile()
方法 直接获得文件。如果上传多个文件,还可以调用 getFileMap()
方法得到Map
这样的结构,MultipartResolver
的作用就是封装普通的请求,使其拥有文件上传的功能。
FlashMapManager
FlashMap
用于重定向时的参数传递,比如在处理用户订单时候,为了避免重复提交,可以处理完 post
请求之后重定向到一个get
请求,这个get
请求可以用来显示订单详情之类的信息。这样做虽然可以规避用户重新提交订单的问题,但是在这个⻚面上要显示订单的信息,这些数据从哪里来获得呢?因为重定向时么有传递参数这一功能的,如果不想把参数写进URL
(不推荐),那么就可以通过FlashMap
来传递。只需要在重定向之前将要传递的数据写入请求(可以通过
ServletRequestAttributes.getRequest()
方法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE
中,这样在重定向之后的Handler
中Spring
就会自动将其设置到Model
中,在显示订单信息的⻚面 上就可以直接从Model
中获取数据。FlashMapManager
就是用来管理 FalshMap
的。
监听器、过滤器和拦截器对比
Servlet
:处理Request
请求和Response
响应。
过滤器(Filter)
:对Request请求起到过滤的作用,作用在Servlet
之前,如果配置为/*
可以对所有的资源访问(servlet、js/css
静态资源等)进行过滤处理。
监听器(Listener)
:实现了javax.servlet.ServletContextListener
接口的服务器端组件,它随 Web
应用的启动而启动,只初始化一次,然后会一直运行监视,随Web
应用的停止而销毁。
作用一:做一些初始化工作,web
应用中spring
容器启动ContextLoaderListener
。
作用二:监听web
中的特定事件,比如HttpSession,ServletRequest
的创建和销毁;变量的创建、 销毁和修改等。可以在某些动作前后增加处理,实现监控,比如统计在线人数,利用 HttpSessionLisener
等。
拦截器(Interceptor)
:是SpringMVC、Struts等
表现层框架自己的,不会拦截 jsp/html/css/image
的访问等,只会拦截访问的控制器方法(Handler)
。
从配置的⻆度也能够总结发现:serlvet、filter、listener
是配置在web.xml
中的,而interceptor
是配置在表现层框架自己的配置文件中的。
在Handler
业务逻辑执行之前拦截一次
在Handler
逻辑执行完毕但未跳转⻚面之前拦截一次
在跳转⻚面之后拦截一次
关于它们更详细的区别,可以看下这篇博文: SpringBoot项目中自定义Filter过滤器、Listener监听器、Interceptor拦截器和Servlet容器
好了,回顾完请求处理流程与一些高级特性后,我们开始来手写 SpringMVC
框架了。
我们来梳理下流程,为了更加清晰手写 SpringMVC
框架的思路,我画了下面这张图:
1、自定义注解类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
String value() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
String value() default "";
}
2、DispatcherServlet 最核心的类
public class DispatcherServlet extends HttpServlet {
private Properties properties = new Properties();
private List<String> classNames = Lists.newArrayList(); // 缓存扫描
private Map<String, Object> ioc = Maps.newHashMap(); // ioc容器
// handlerMapping
// private Map handlerMapping = Maps.newHashMap(); // 存储url和method之间的映射关系
private List<Handler> handlerMapping = Lists.newArrayList();
@Override
public void init(ServletConfig servletConfig) throws ServletException {
// 1.加载配置文件 springmvc.properties
String contextConfigLocation = servletConfig.getInitParameter("contextConfigLocation");
doLoadConfig(contextConfigLocation);
// 2.扫描相关的类,扫描注解。
doScan(properties.getProperty("scanPackage"));
// 3.初始化bean对象(实现ioc容器,基于注解)
doInstance();
// 4.实现依赖注入
doAutowired();
// 5.构造一个HandlerMapping处理器映射器,将配置好的url和Method建立映射关系
initHandlerMapping();
System.out.println("riemann mvc init success...");
// 6.等待请求进入,处理请求。
}
/**
* 构造一个HandlerMapping处理器映射器
* 最关键的步骤
* 目的:将url和method建立关联
*/
private void initHandlerMapping() {
if (ioc.isEmpty()) return;
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
// 获取ioc容器中当前遍历的对象的class类型
Class<?> clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(Controller.class)) continue;
String baseUrl = "";
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping annotation = clazz.getAnnotation(RequestMapping.class);
baseUrl = annotation.value(); // 等同于 /riemann
}
// 获取方法
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
// 方法没有标识RequestMapping,就不处理
if (!method.isAnnotationPresent(RequestMapping.class)) continue;
// 如果标识则处理
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
String methodUrl = annotation.value(); // 等同于 /query
String url = baseUrl + methodUrl; // 计算出来的url /riemann/query
// 把method所有信息及url封装为一个Handler
Handler handler = new Handler(entry.getValue(), method, Pattern.compile(url));
// 计算方法的参数位置信息 // query(HttpServletRequest request, HttpServletResponse response, String name)
Parameter[] parameters = method.getParameters();
for (int j = 0; j < parameters.length; j++) {
Parameter parameter = parameters[j];
if (parameter.getType() == HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) {
// 如果是request和response对象,那么参数名称写HttpServletRequest和HttpServletResponse
handler.getParamIndexMapping().put(parameter.getType().getSimpleName(), j);
} else {
handler.getParamIndexMapping().put(parameter.getName(), j); //
}
}
// 建立url和method之间的映射关系(map缓存起来)
// handlerMapping.put(url, method);
handlerMapping.add(handler);
}
}
}
/**
* 实现依赖注入
*/
private void doAutowired() {
if (ioc.isEmpty()) return;
// 有对象,再进行依赖注入处理
// 遍历ioc中所有对象,查看对象中的字段,是否有@Autowired注解,如果有需要维护依赖注入的关系
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
// 获取bean对象中的字段信息
Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
// 遍历判断处理
for (int i = 0; i < declaredFields.length; i++) {
Field declaredField = declaredFields[i]; // @Autowired private RiemannService riemannService;
if (!declaredField.isAnnotationPresent(Autowired.class)) continue;
// 有该注解
Autowired annotation = declaredField.getAnnotation(Autowired.class);
String beanName = annotation.value(); // 需要注入的bean的id
if ("".equals(beanName.trim())) {
// 没有配置具体的bean id,那就需要根据当前字段类型注入(接口注入)RiemannService
beanName = declaredField.getType().getName();
}
// 开启赋值
declaredField.setAccessible(true);
try {
declaredField.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
/**
* ioc容器
* 基于className缓存的类的全限定类名,以及反射技术,完成对象创建和管理。
*/
private void doInstance() {
if (classNames.size() == 0) return;
try {
for (int i = 0; i < classNames.size(); i++) {
String className = classNames.get(i); // com.riemann.controller.RiemannController
// 反射
Class<?> clazz = Class.forName(className);
// 区分controller,区分service
if (clazz.isAnnotationPresent(Controller.class)) {
// controller的id不做过多处理,不取value了,就拿类的首字母小写作为id,保存到ioc中
String simpleName = clazz.getSimpleName(); // RiemannController
String lowerLetterSimpleName = lowerLetterFirst(simpleName); // riemannController
Object o = clazz.newInstance();
ioc.put(lowerLetterSimpleName, o);
} else if (clazz.isAnnotationPresent(Service.class)) {
Service annotation = clazz.getAnnotation(Service.class);
// 获取注解的值
String beanName = annotation.value();
// 如果指定了id,就以指定的为准
if (!"".equals(beanName.trim())) {
ioc.put(beanName, clazz.newInstance());
} else {
// 如果没有指定,就以类名首字母小写
beanName = lowerLetterFirst(clazz.getSimpleName());
ioc.put(beanName, clazz.newInstance());
}
// service层往往是有接口的,面向接口开发,此时再以接口名为id,放入一份对象到ioc容器中,便于后期根据接口类型注入
Class<?>[] interfaces = clazz.getInterfaces();
for (int j = 0; j < interfaces.length; j++) {
Class<?> anInterface = interfaces[j];
// 以接口的全限定类名作为id放入
ioc.put(anInterface.getName(), clazz.newInstance());
}
} else {
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String lowerLetterFirst(String str) {
char[] chars = str.toCharArray();
if ('A' <= chars[0] && chars[0] <= 'Z') {
chars[0] += 32;
}
return String.valueOf(chars);
}
/**
* 扫描类
* scanPackage:com.riemann ---> 磁盘上的文件夹(File) com/riemann
* @param scanPackage
*/
private void doScan(String scanPackage) {
String scanPackagePath = Thread.currentThread().getContextClassLoader().getResource("").getPath() +
scanPackage.replaceAll("\\.", "/");
File packageName = new File(scanPackagePath);
for (File file : packageName.listFiles()) {
if (file.isDirectory()) { // 子package
// 递归
doScan(scanPackage + "." + file.getName()); // com.riemann.controller
} else if (file.getName().endsWith(".class")) {
String className = scanPackage + "." + file.getName().replaceAll(".class", "");
classNames.add(className);
}
}
}
/**
* 加载配置文件
* @param contextConfigLocation
*/
private void doLoadConfig(String contextConfigLocation) {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
properties.load(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 处理请求,根据url,找到对应的Method方法,进行调用。
// 获取uri
// String requestURI = req.getRequestURI();
// Method method = handlerMapping.get(requestURI); // 获取到一个反射的方法
// 反射调用,需要传入对象,需要传入参数,此处无法完成调用,没有把对象缓存起来,也没有参数!!!需要改造initHandlerMapping()
// method.invoke();
// 根据uri获取到我们能够处理当前请求的handler(从handlerMapping中(List))
Handler handler = getHandler(req);
if (handler == null) {
resp.getWriter().write("404 not found");
return;
}
// 参数绑定
// 获取所有参数类型数组,这个数组的长度就是我们最后要传入的args数组的长度
Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();
// 根据上述数组长度创建一个新的数组(参数数组,是要传入反射调用的)
Object[] paramValues = new Object[parameterTypes.length];
// 以下就是为了向参数数组中塞值,而且还得保证参数的顺序和方法中形参顺序一致
Map<String, String[]> parameterMap = req.getParameterMap();
// 遍历request中所有参数(填充除了request、response之外的)
for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
// name=1&name=2 name [1,2]
String value = StringUtils.join(param.getValue(), ","); // 如同 1,2
// 如果参数和方法中的参数匹配上了,填充数据。
if (!handler.getParamIndexMapping().containsKey(param.getKey())) continue;
// 方法形参确实有该参数,找到它的索引位置,对应的把参数值放入paramValues
Integer index = handler.getParamIndexMapping().get(param.getKey()); // name在第2个位置
paramValues[index] = value; // 把前台传递过来的参数值填充到对应的位置去
}
int requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName()); // 0
paramValues[requestIndex] = req;
int responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName()); // 1
paramValues[responseIndex] = resp;
// 最终调用handler的method属性
try {
handler.getMethod().invoke(handler.getController(), paramValues);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private Handler getHandler(HttpServletRequest req) {
if (handlerMapping.isEmpty()) return null;
String requestURI = req.getRequestURI();
for (Handler handler : handlerMapping) {
Matcher matcher = handler.getPattern().matcher(requestURI);
if (!matcher.matches()) continue;
return handler;
}
return null;
}
}
3、pojo类Handler
/**
* 封装handler方法相关的信息
*/
@Data
public class Handler {
private Object controller; // method.invoke(obj,);
private Method method;
private Pattern pattern; // spring中url是支持正则的
private Map<String, Integer> paramIndexMapping; // 参数的顺序,是为了进行参数绑定。key是参数名,value是第几个参数
public Handler(Object controller, Method method, Pattern pattern) {
this.controller = controller;
this.method = method;
this.pattern = pattern;
paramIndexMapping = Maps.newHashMap();
}
}
4、web.xml配置
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>riemannmvc</servlet-name>
<servlet-class>com.riemann.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>springmvc.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>riemannmvc</servlet-name>
<url-pattern>/*
5、RiemannController.java
@Controller
@RequestMapping("/riemann")
public class RiemannController {
@Autowired
private RiemannService riemannService;
/**
* URL: /riemann/query
* @param request
* @param response
* @param name
* @return
*/
@RequestMapping("/query")
public String query(HttpServletRequest request, HttpServletResponse response, String name) {
return riemannService.get(name);
}
}
6、测试结果
浏览器输入:http://localhost:8888/riemann/query?name=riemann
riemann mvc init success...
RiemannService 实现类中的name参数:riemann
ok,测试成功,这样就完成了手写SpringMVC框架的简易版了。
https://github.com/riemannChow/perseverance/tree/master/handwriting-framework/springmvc