
手写SpringMVC框架之前呢,我觉得有必要先了解SpringMVC的请求处理流程以及高级特性。

流程说明:
第一步:用户发送请求至前端控制器DispatcherServlet。
第二步:DispatcherServlet收到请求调用HandlerMapping处理器映射器。
第三步:处理器映射器根据请求Url找到具体的Handler(后端控制器),生成处理器对象及处理器拦截器(如果有则生成)一并返回DispatcherServlet。
第四步:DispatcherServlet调用HandlerAdapter处理器适配器去调用Handler。
第五步:处理器适配器执行Handler。
第六步:Handler执行完成给处理器适配器返回ModelAndView。
第七步:处理器适配器向前端控制器返回 ModelAndView,ModelAndView 是SpringMVC 框架的一个 底层对 象,包括 Model 和 View。
第八步:前端控制器请求视图解析器去进行视图解析,根据逻辑视图名来解析真正的视图。
第九步:视图解析器向前端控制器返回View。
第十步:前端控制器进行视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域。
第十一步:前端控制器向用户响应结果。
2、Spring MVC 九大组件
HandlerMapping 是用来查找 Handler 的,也就是处理器,具体的表现形式可以是类,也可以是方法。比如,标注了@RequestMapping的每个方法都可以看成是一个Handler。Handler负责具体实际的请求处理,在请求到达后,HandlerMapping 的作用便是找到请求相应的处理器 Handler 和 Interceptor。
HandlerAdapter 是一个适配器。因为 Spring MVC 中 Handler 可以是任意形式的,只要能处理请求即可。但是把请求交给 Servlet 的时候,由于 Servlet 的方法结构都是 doService(HttpServletRequest req,HttpServletResponse resp)形式的,要让固定的 Servlet 处理方法调用 Handler 来进行处理,便是 HandlerAdapter 的职责。
HandlerExceptionResolver 用于处理 Handler 产生的异常情况。它的作用是根据异常设置
ModelAndView,之后交给渲染方法进行渲染,渲染方法会将 ModelAndView 渲染成⻚面。
ViewResolver即视图解析器,用于将String类型的视图名和Locale解析为View类型的视图,只有一 个resolveViewName()方法。从方法的定义可以看出,Controller层返回的String类型视图名 viewName 最终会在这里被解析成为View。View是用来渲染⻚面的,也就是说,它会将程序返回的参数和数据填入模板中,生成html文件。ViewResolver 在这个过程主要完成两件事情: ViewResolver 找到渲染所用的模板(第一件大事)和所用的技术(第二件大事,其实也就是找到视图的类型,如JSP)并填入参数。默认情况下,Spring MVC会自动为我们配置一个 InternalResourceViewResolver,是针对 JSP 类型视图的。
RequestToViewNameTranslator 组件的作用是从请求中获取 ViewName,因为 ViewResolver 根据 ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName, 便要通过这个组件从请求中查找 ViewName。
ViewResolver 组件的 resolveViewName 方法需要两个参数,一个是视图名,一个是 Locale。 LocaleResolver 用于从请求中解析出 Locale,比如中国 Locale 是 zh-CN,用来表示一个区域。这 个组件也是 i18n 的基础。
ThemeResolver 组件是用来解析主题的。主题是样式、图片及它们所形成的显示效果的集合。 Spring MVC 中一套主题对应一个 properties文件,里面存放着与当前主题相关的所有资源,如图片、CSS样式等。创建主题非常简单,只需准备好资源,然后新建一个“主题名.properties”并将资源设置进去,放在classpath下,之后便可以在⻚面中使用了。SpringMVC中与主题相关的类有 ThemeResolver、ThemeSource和Theme。ThemeResolver负责从请求中解析出主题名, ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源。
MultipartResolver 用于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实现。MultipartHttpServletRequest 可以通过 getFile() 方法 直接获得文件。如果上传多个文件,还可以调用 getFileMap()方法得到Map<FileName,File>这样的结构,MultipartResolver 的作用就是封装普通的请求,使其拥有文件上传的功能。
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逻辑执行完毕但未跳转⻚面之前拦截一次在跳转⻚面之后拦截一次
好了,回顾完请求处理流程与一些高级特性后,我们开始来手写 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<String, Method> 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); // <name, 2>
}
}
// 建立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>/*</url-pattern>
</servlet-mapping>
</web-app>
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框架的简易版了。
欢迎大家关注我的公众号【老周聊架构】,AI、大数据、云原生、物联网等相关领域的技术知识分享。