课程目标

1、了解看源码最有效的方式,先猜测后验证,不要一开始就去调试代码。
2、浓缩就是精华,用 300 行最简洁的代码提炼 Spring 的基本设计思想。
3、结合设计模式,掌握 Spring 框架的基本脉络。

内容定位

1、具有 1 年以上的 SpringMVC 使用经验。
2、希望深入了解 Spring 源码的人群,对 Spring 有一个整体的宏观感受。
3、全程手写实现 SpringMVC 的核心功能,帮助大家更深刻地理解设计模式。从最简单的 v1 版本一步
一步优化为 v2 版本,最后到 v3 版本。

实现思路

先来介绍一下 Mini 版本的 Spring 基本实现思路,如下图所示:

自定义配置

配置 application.properties 文件

为了解析方便,我们用 application.properties 来代替 application.xml 文件,具体配置内容如下:

  1. scanPackage=com.gupaoedu.demo

配置 web.xml 文件

大家都知道,所有依赖于 web 容器的项目,都是从读取 web.xml 文件开始的。我们先配置好 web.xml
中的内容。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
  4. xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  5. xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
  6. http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
  7. version="2.4">
  8. <display-name>Gupao Web Application</display-name>
  9. <servlet>
  10. <servlet-name>gpmvc</servlet-name>
  11. <servlet-class>com.gupaoedu.mvcframework.v1.servlet.GPDispatcherServlet</servlet-class>
  12. <init-param>
  13. <param-name>contextConfigLocation</param-name>
  14. <param-value>application.properties</param-value>
  15. </init-param>
  16. <load-on-startup>1</load-on-startup>
  17. </servlet>
  18. <servlet-mapping>
  19. <servlet-name>gpmvc</servlet-name>
  20. <url-pattern>/*</url-pattern>
  21. </servlet-mapping>
  22. </web-app>

其中 GPDispatcherServlet 是有自己模拟 Spring 实现的核心功能类。

自定义 Annotation

@GPService 注解:

  1. package com.gupaoedu.mvcframework.annotation;
  2. import java.lang.annotation.*;
  3. @Target({ElementType.TYPE})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface GPService {
  7. String value() default "";
  8. }

@GPAutowired 注解:

  1. package com.gupaoedu.mvcframework.annotation;
  2. import java.lang.annotation.*;
  3. @Target({ElementType.FIELD})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface GPAutowired {
  7. String value() default "";
  8. }

@GPController 注解:

  1. package com.gupaoedu.mvcframework.annotation;
  2. import java.lang.annotation.*;
  3. @Target({ElementType.TYPE})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface GPController {
  7. String value() default "";
  8. }

@GPRequestMapping 注解:

  1. package com.gupaoedu.mvcframework.annotation;
  2. import java.lang.annotation.*;
  3. @Target({ElementType.TYPE,ElementType.METHOD})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface GPRequestMapping {
  7. String value() default "";
  8. }

@GPRequestParam 注解:

  1. package com.gupaoedu.mvcframework.annotation;
  2. import java.lang.annotation.*;
  3. @Target({ElementType.PARAMETER})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface GPRequestParam {
  7. String value() default "";
  8. }

配置 Annotation

配置业务实现类 DemoService:

  1. package com.gupaoedu.demo.service.impl;
  2. import com.gupaoedu.demo.service.IDemoService;
  3. import com.gupaoedu.mvcframework.annotation.GPService;
  4. /**
  5. * 核心业务逻辑
  6. */
  7. @GPService
  8. public class DemoService implements IDemoService{
  9. public String get(String name) {
  10. return "My name is " + name;
  11. }
  12. }

配置请求入口类 DemoAction:

  1. package com.gupaoedu.demo.mvc.action;
  2. import java.io.IOException;
  3. import javax.servlet.http.HttpServletRequest;
  4. import javax.servlet.http.HttpServletResponse;
  5. import com.gupaoedu.demo.service.IDemoService;
  6. import com.gupaoedu.mvcframework.annotation.GPAutowired;
  7. import com.gupaoedu.mvcframework.annotation.GPController;
  8. import com.gupaoedu.mvcframework.annotation.GPRequestMapping;
  9. import com.gupaoedu.mvcframework.annotation.GPRequestParam;
  10. @GPController
  11. @GPRequestMapping("/demo")
  12. public class DemoAction {
  13. @GPAutowired private IDemoService demoService;
  14. @GPRequestMapping("/query")
  15. public void query(HttpServletRequest req, HttpServletResponse resp,
  16. @GPRequestParam("name") String name){
  17. String result = demoService.get(name);
  18. try {
  19. resp.getWriter().write(result);
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. @GPRequestMapping("/add")
  25. public void add(HttpServletRequest req, HttpServletResponse resp,
  26. @GPRequestParam("a") Integer a, @GPRequestParam("b") Integer b){
  27. try {
  28. resp.getWriter().write(a + "+" + b + "=" + (a + b));
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. @GPRequestMapping("/remove")
  34. public void remove(HttpServletRequest req,HttpServletResponse resp,
  35. @GPRequestParam("id") Integer id){
  36. }
  37. }

至此,配置阶段就已经完成。

容器初始化

实现 V1版本

所有的核心逻辑全部写在一个 init()方法中。

  1. package com.gupaoedu.mvcframework.v1.servlet;
  2. import com.gupaoedu.mvcframework.annotation.GPAutowired;
  3. import com.gupaoedu.mvcframework.annotation.GPController;
  4. import com.gupaoedu.mvcframework.annotation.GPRequestMapping;
  5. import com.gupaoedu.mvcframework.annotation.GPService;
  6. import javax.servlet.ServletConfig;
  7. import javax.servlet.ServletException;
  8. import javax.servlet.http.HttpServlet;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. import java.io.File;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.lang.reflect.Field;
  15. import java.lang.reflect.Method;
  16. import java.net.URL;
  17. import java.util.*;
  18. public class GPDispatcherServlet extends HttpServlet {
  19. private Map<String,Object> mapping = new HashMap<String, Object>();
  20. @Override
  21. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
  22. IOException {this.doPost(req,resp);}
  23. @Override
  24. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
  25. IOException {
  26. try {
  27. doDispatch(req,resp);
  28. } catch (Exception e) {
  29. resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
  30. }
  31. }
  32. private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
  33. String url = req.getRequestURI();
  34. String contextPath = req.getContextPath();
  35. url = url.replace(contextPath, "").replaceAll("/+", "/");
  36. if(!this.mapping.containsKey(url)){resp.getWriter().write("404 Not Found!!");return;}
  37. Method method = (Method) this.mapping.get(url);
  38. Map<String,String[]> params = req.getParameterMap();
  39. method.invoke(this.mapping.get(method.getDeclaringClass().getName()),new
  40. Object[]{req,resp,params.get("name")[0]});
  41. }
  42. @Override
  43. public void init(ServletConfig config) throws ServletException {
  44. InputStream is = null;
  45. try{
  46. Properties configContext = new Properties();
  47. is =
  48. this.getClass().getClassLoader().getResourceAsStream(config.getInitParameter("contextConfigLocat
  49. ion"));
  50. configContext.load(is);
  51. String scanPackage = configContext.getProperty("scanPackage");
  52. doScanner(scanPackage);
  53. for (String className : mapping.keySet()) {
  54. if(!className.contains(".")){continue;}
  55. Class<?> clazz = Class.forName(className);
  56. if(clazz.isAnnotationPresent(GPController.class)){
  57. mapping.put(className,clazz.newInstance());
  58. String baseUrl = "";
  59. if (clazz.isAnnotationPresent(GPRequestMapping.class)) {
  60. GPRequestMapping requestMapping =
  61. clazz.getAnnotation(GPRequestMapping.class);
  62. baseUrl = requestMapping.value();
  63. }
  64. Method[] methods = clazz.getMethods();
  65. for (Method method : methods) {
  66. if (!method.isAnnotationPresent(GPRequestMapping.class)) { continue; }
  67. GPRequestMapping requestMapping =
  68. method.getAnnotation(GPRequestMapping.class);
  69. String url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
  70. mapping.put(url, method);
  71. System.out.println("Mapped " + url + "," + method);
  72. }
  73. }else if(clazz.isAnnotationPresent(GPService.class)){
  74. GPService service = clazz.getAnnotation(GPService.class);
  75. String beanName = service.value();
  76. if("".equals(beanName)){beanName = clazz.getName();}
  77. Object instance = clazz.newInstance();
  78. mapping.put(beanName,instance);
  79. for (Class<?> i : clazz.getInterfaces()) {
  80. mapping.put(i.getName(),instance);
  81. }
  82. }else {continue;}
  83. }
  84. for (Object object : mapping.values()) {
  85. if(object == null){continue;}
  86. Class clazz = object.getClass();
  87. if(clazz.isAnnotationPresent(GPController.class)){
  88. Field [] fields = clazz.getDeclaredFields();
  89. for (Field field : fields) {
  90. if(!field.isAnnotationPresent(GPAutowired.class)){continue; }
  91. GPAutowired autowired = field.getAnnotation(GPAutowired.class);
  92. String beanName = autowired.value();
  93. if("".equals(beanName)){beanName = field.getType().getName();}
  94. field.setAccessible(true);
  95. try {
  96. field.set(mapping.get(clazz.getName()),mapping.get(beanName));
  97. } catch (IllegalAccessException e) {
  98. e.printStackTrace();
  99. }
  100. }
  101. }
  102. }
  103. } catch (Exception e) {
  104. }finally {
  105. if(is != null){
  106. try {is.close();} catch (IOException e) {
  107. e.printStackTrace();
  108. }
  109. }
  110. }
  111. System.out.print("GP MVC Framework is init");
  112. }
  113. private void doScanner(String scanPackage) {
  114. URL url = this.getClass().getClassLoader().getResource("/" +
  115. scanPackage.replaceAll("\\.","/"));
  116. File classDir = new File(url.getFile());
  117. for (File file : classDir.listFiles()) {
  118. if(file.isDirectory()){ doScanner(scanPackage + "." + file.getName());}else {
  119. if(!file.getName().endsWith(".class")){continue;}
  120. String clazzName = (scanPackage + "." + file.getName().replace(".class",""));
  121. mapping.put(clazzName,null);
  122. }
  123. }
  124. }
  125. }

实现 V2版本

在 V1 版本上进了优化,采用了常用的设计模式(工厂模式、单例模式、委派模式、策略模式),将 init()方法中的代码进行封装。按照之前的实现思路,先搭基础框架,再填肉注血,具体代码如下:

  1. //初始化阶段
  2. @Override
  3. public void init(ServletConfig config) throws ServletException {
  4. //1、加载配置文件
  5. doLoadConfig(config.getInitParameter("contextConfigLocation"));
  6. //2、扫描相关的类
  7. doScanner(contextConfig.getProperty("scanPackage"));
  8. //3、初始化扫描到的类,并且将它们放入到 ICO 容器之中
  9. doInstance();
  10. //4、完成依赖注入
  11. doAutowired();
  12. //5、初始化 HandlerMapping
  13. initHandlerMapping();
  14. System.out.println("GP Spring framework is init.");
  15. }

声明全局的成员变量,其中 IOC 容器就是注册时单例的具体案例:

  1. //保存 application.properties 配置文件中的内容
  2. private Properties contextConfig = new Properties();
  3. //保存扫描的所有的类名
  4. private List<String> classNames = new ArrayList<String>();
  5. //传说中的 IOC 容器,我们来揭开它的神秘面纱
  6. //为了简化程序,暂时不考虑 ConcurrentHashMap
  7. // 主要还是关注设计思想和原理
  8. private Map<String,Object> ioc = new HashMap<String,Object>();
  9. //保存 url 和 Method 的对应关系
  10. private Map<String,Method> handlerMapping = new HashMap<String,Method>();

实现 doLoadConfig()方法:

  1. //加载配置文件
  2. private void doLoadConfig(String contextConfigLocation) {
  3. //直接从类路径下找到 Spring 主配置文件所在的路径
  4. //并且将其读取出来放到 Properties 对象中
  5. //相对于 scanPackage=com.gupaoedu.demo 从文件中保存到了内存中
  6. InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
  7. try {
  8. contextConfig.load(fis);
  9. } catch (IOException e) {
  10. e.printStackTrace();
  11. }finally {
  12. if(null != fis){
  13. try {
  14. fis.close();
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. }

实现 doScanner()方法:

  1. //扫描出相关的类
  2. private void doScanner(String scanPackage) {
  3. //scanPackage = com.gupaoedu.demo ,存储的是包路径
  4. //转换为文件路径,实际上就是把.替换为/就 OK 了
  5. //classpath
  6. URL url = this.getClass().getClassLoader().getResource("/" +
  7. scanPackage.replaceAll("\\.","/"));
  8. File classPath = new File(url.getFile());
  9. for (File file : classPath.listFiles()) {
  10. if(file.isDirectory()){
  11. doScanner(scanPackage + "." + file.getName());
  12. }else{
  13. if(!file.getName().endsWith(".class")){ continue;}
  14. String className = (scanPackage + "." + file.getName().replace(".class",""));
  15. classNames.add(className);
  16. }
  17. }
  18. }

实现 doInstance()方法,doInstance()方法就是工厂模式的具体实现:

  1. private void doInstance() {
  2. //初始化,为 DI 做准备
  3. if(classNames.isEmpty()){return;}
  4. try {
  5. for (String className : classNames) {
  6. Class<?> clazz = Class.forName(className);
  7. //什么样的类才需要初始化呢?
  8. //加了注解的类,才初始化,怎么判断?
  9. //为了简化代码逻辑,主要体会设计思想,只举例 @Controller 和@Service,
  10. // @Componment...就一一举例了
  11. if(clazz.isAnnotationPresent(GPController.class)){
  12. Object instance = clazz.newInstance();
  13. //Spring 默认类名首字母小写
  14. String beanName = toLowerFirstCase(clazz.getSimpleName());
  15. ioc.put(beanName,instance);
  16. }else if(clazz.isAnnotationPresent(GPService.class)){
  17. //1、自定义的 beanName
  18. GPService service = clazz.getAnnotation(GPService.class);
  19. String beanName = service.value();
  20. //2、默认类名首字母小写
  21. if("".equals(beanName.trim())){
  22. beanName = toLowerFirstCase(clazz.getSimpleName());
  23. }
  24. Object instance = clazz.newInstance();
  25. ioc.put(beanName,instance);
  26. //3、根据类型自动赋值,投机取巧的方式
  27. for (Class<?> i : clazz.getInterfaces()) {
  28. if(ioc.containsKey(i.getName())){
  29. throw new Exception("The “" + i.getName() + "” is exists!!");
  30. }
  31. //把接口的类型直接当成 key 了
  32. ioc.put(i.getName(),instance);
  33. }
  34. }else {
  35. continue;
  36. }
  37. }
  38. }catch (Exception e){
  39. e.printStackTrace();
  40. }
  41. }

为了处理方便,自己实现了 toLowerFirstCase 方法,来实现类名首字母小写,具体代码如下:

  1. //如果类名本身是小写字母,确实会出问题
  2. //但是我要说明的是:这个方法是我自己用,private 的
  3. //传值也是自己传,类也都遵循了驼峰命名法
  4. //默认传入的值,存在首字母小写的情况,也不可能出现非字母的情况
  5. //为了简化程序逻辑,就不做其他判断了,大家了解就 OK
  6. //其实用写注释的时间都能够把逻辑写完了
  7. private String toLowerFirstCase(String simpleName) {
  8. char [] chars = simpleName.toCharArray();
  9. //之所以加,是因为大小写字母的 ASCII 码相差 32,
  10. // 而且大写字母的 ASCII 码要小于小写字母的 ASCII 码
  11. //在 Java 中,对 char 做算学运算,实际上就是对 ASCII 码做算学运算
  12. chars[0] += 32;
  13. return String.valueOf(chars);
  14. }

实现 doAutowired()方法:

  1. //自动依赖注入
  2. private void doAutowired() {
  3. if(ioc.isEmpty()){return;}
  4. for (Map.Entry<String, Object> entry : ioc.entrySet()) {
  5. //Declared 所有的,特定的 字段,包括 private/protected/default
  6. //正常来说,普通的 OOP 编程只能拿到 public 的属性
  7. Field[] fields = entry.getValue().getClass().getDeclaredFields();
  8. for (Field field : fields) {
  9. if(!field.isAnnotationPresent(GPAutowired.class)){continue;}
  10. GPAutowired autowired = field.getAnnotation(GPAutowired.class);
  11. //如果用户没有自定义 beanName,默认就根据类型注入
  12. //这个地方省去了对类名首字母小写的情况的判断,这个作为课后作业
  13. //小伙伴们自己去完善
  14. String beanName = autowired.value().trim();
  15. if("".equals(beanName)){
  16. //获得接口的类型,作为 key 待会拿这个 key 到 ioc 容器中去取值
  17. beanName = field.getType().getName();
  18. }
  19. //如果是 public 以外的修饰符,只要加了@Autowired 注解,都要强制赋值
  20. //反射中叫做暴力访问, 强吻
  21. field.setAccessible(true);
  22. try {
  23. //用反射机制,动态给字段赋值
  24. field.set(entry.getValue(),ioc.get(beanName));
  25. } catch (IllegalAccessException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. }

实现 initHandlerMapping()方法,handlerMapping 就是策略模式的应用案例:

  1. //初始化 url 和 Method 的一对一对应关系
  2. private void initHandlerMapping() {
  3. if(ioc.isEmpty()){ return; }
  4. for (Map.Entry<String, Object> entry : ioc.entrySet()) {
  5. Class<?> clazz = entry.getValue().getClass();
  6. if(!clazz.isAnnotationPresent(GPController.class)){continue;}
  7. //保存写在类上面的@GPRequestMapping("/demo")
  8. String baseUrl = "";
  9. if(clazz.isAnnotationPresent(GPRequestMapping.class)){
  10. GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
  11. baseUrl = requestMapping.value();
  12. }
  13. //默认获取所有的 public 方法
  14. for (Method method : clazz.getMethods()) {
  15. if(!method.isAnnotationPresent(GPRequestMapping.class)){continue;}
  16. GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
  17. //优化
  18. // //demo///query
  19. String url = ("/" + baseUrl + "/" + requestMapping.value())
  20. .replaceAll("/+","/");
  21. handlerMapping.put(url,method);
  22. System.out.println("Mapped :" + url + "," + method);
  23. }
  24. }
  25. }

到这里位置初始化阶段就已经完成,接下实现运行阶段的逻辑,来看 doPost/doGet 的代码:

  1. @Override
  2. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
  3. IOException {
  4. this.doPost(req,resp);
  5. }
  6. @Override
  7. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
  8. IOException {
  9. //6、调用,运行阶段
  10. try {
  11. doDispatch(req,resp);
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. resp.getWriter().write("500 Exection,Detail : " + Arrays.toString(e.getStackTrace()));
  15. }
  16. }

doPost()方法中,用了委派模式,委派模式的具体逻辑在 doDispatch()方法中:

  1. private void doDispatch(HttpServletRequest req, HttpServletResponse resp)throws Exception {
  2. String url = req.getRequestURI();
  3. String contextPath = req.getContextPath();
  4. url = url.replaceAll(contextPath,"").replaceAll("/+","/");
  5. if(!this.handlerMapping.containsKey(url)){
  6. resp.getWriter().write("404 Not Found!!");
  7. return;
  8. }
  9. Method method = this.handlerMapping.get(url);
  10. //第一个参数:方法所在的实例
  11. //第二个参数:调用时所需要的实参
  12. Map<String,String[]> params = req.getParameterMap();
  13. //投机取巧的方式
  14. String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
  15. method.invoke(ioc.get(beanName),new Object[]{req,resp,params.get("name")[0]});
  16. //System.out.println(method);
  17. }

在以上代码中,doDispatch()虽然完成了动态委派并反射调用,但对 url 参数处理还是静态代码。要实现 url 参数的动态获取,其实还稍微有些复杂。我们可以优化 doDispatch()方法的实现逻辑,代码如下:

  1. private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
  2. //绝对路径
  3. String url = req.getRequestURI();
  4. //处理成相对路径
  5. String contextPath = req.getContextPath();
  6. url = url.replaceAll(contextPath,"").replaceAll("/+","/");
  7. if(!this.handlerMapping.containsKey(url)){
  8. resp.getWriter().write("404 Not Found!!!");
  9. return;
  10. }
  11. Method method = this.handlerMapping.get(url);
  12. //从 reqest 中拿到 url 传过来的参数
  13. Map<String,String[]> params = req.getParameterMap();
  14. //获取方法的形参列表
  15. Class<?> [] parameterTypes = method.getParameterTypes();
  16. Object [] paramValues = new Object[parameterTypes.length];
  17. for (int i = 0; i < parameterTypes.length; i ++) {
  18. Class parameterType = parameterTypes[i];
  19. //不能用 instanceof,parameterType 它不是实参,而是形参
  20. if(parameterType == HttpServletRequest.class){
  21. paramValues[i] = req;
  22. continue;
  23. }else if(parameterType == HttpServletResponse.class){
  24. paramValues[i] = resp;
  25. continue;
  26. }else if(parameterType == String.class){
  27. GPRequestParam requestParam =
  28. (GPRequestParam)parameterType.getAnnotation(GPRequestParam.class);
  29. if(params.containsKey(requestParam.value())) {
  30. for (Map.Entry<String,String[]> param : params.entrySet()){
  31. String value = Arrays.toString(param.getValue())
  32. .replaceAll("\\[|\\]","")
  33. .replaceAll("\\s",",");
  34. paramValues[i] = value;
  35. }
  36. }
  37. }
  38. }
  39. //投机取巧的方式
  40. //通过反射拿到 method 所在 class,拿到 class 之后还是拿到 class 的名称
  41. //再调用 toLowerFirstCase 获得 beanName
  42. String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
  43. method.invoke(ioc.get(beanName),paramValues);
  44. }
  45. //url 传过来的参数都是 String 类型的,HTTP 是基于字符串协议
  46. //只需要把 String 转换为任意类型就好
  47. private Object convert(Class<?> type,String value){
  48. //如果是 int
  49. if(Integer.class == type){
  50. return Integer.valueOf(value);
  51. }
  52. //如果还有 double 或者其他类型,继续加 if
  53. //这时候,我们应该想到策略模式了
  54. //在这里暂时不实现,希望小伙伴自己来实现
  55. return value;
  56. }

实现 V3版本

在 V2 版本中,基本功能以及完全实现,但代码的优雅程度还不如人意。譬如HandlerMapping 还不能像 SpringMVC一样支持正则,url 参数还不支持强制类型转换,在反射调用前还需要重新获取 beanName,在 V3 版本中,下面我们继续优化。
首先,改造 HandlerMapping,在真实的 Spring 源码中,HandlerMapping 其实是一个 List 而非 Map。List 中的元素是一个自定义的类型。现在我们来仿真写一段代码,先定义一个内部类 Handler 类:

  1. /**
  2. * Handler 记录 Controller 中的 RequestMapping 和 Method 的对应关系
  3. * @author Tom
  4. * 内部类
  5. */
  6. private class Handler{
  7. protected Object controller; //保存方法对应的实例
  8. protected Method method; //保存映射的方法
  9. protected Pattern pattern;
  10. protected Map<String,Integer> paramIndexMapping; //参数顺序
  11. /**
  12. * 构造一个 Handler 基本的参数
  13. * @param controller
  14. * @param method
  15. */
  16. protected Handler(Pattern pattern,Object controller,Method method){
  17. this.controller = controller;
  18. this.method = method;
  19. this.pattern = pattern;
  20. paramIndexMapping = new HashMap<String,Integer>();
  21. putParamIndexMapping(method);
  22. }
  23. private void putParamIndexMapping(Method method){
  24. //提取方法中加了注解的参数
  25. Annotation [] [] pa = method.getParameterAnnotations();
  26. for (int i = 0; i < pa.length ; i ++) {
  27. for(Annotation a : pa[i]){
  28. if(a instanceof GPRequestParam){
  29. String paramName = ((GPRequestParam) a).value();
  30. if(!"".equals(paramName.trim())){
  31. paramIndexMapping.put(paramName, i);
  32. }
  33. }
  34. }
  35. }
  36. //提取方法中的 request 和 response 参数
  37. Class<?> [] paramsTypes = method.getParameterTypes();
  38. for (int i = 0; i < paramsTypes.length ; i ++) {
  39. Class<?> type = paramsTypes[i];
  40. if(type == HttpServletRequest.class ||
  41. type == HttpServletResponse.class){
  42. paramIndexMapping.put(type.getName(),i);
  43. }
  44. }
  45. }
  46. }
  47. }

然后,优化 HandlerMapping 的结构,代码如下:

  1. //保存所有的 Url 和方法的映射关系
  2. private List<Handler> handlerMapping = new ArrayList<Handler>();

修改 initHandlerMapping()方法:

  1. private void initHandlerMapping(){
  2. if(ioc.isEmpty()){ return; }
  3. for (Entry<String, Object> entry : ioc.entrySet()) {
  4. Class<?> clazz = entry.getValue().getClass();
  5. if(!clazz.isAnnotationPresent(GPController.class)){ continue; }
  6. String url = "";
  7. //获取 Controller 的 url 配置
  8. if(clazz.isAnnotationPresent(GPRequestMapping.class)){
  9. GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
  10. url = requestMapping.value();
  11. }
  12. //获取 Method 的 url 配置
  13. Method [] methods = clazz.getMethods();
  14. for (Method method : methods) {
  15. //没有加 RequestMapping 注解的直接忽略
  16. if(!method.isAnnotationPresent(GPRequestMapping.class)){ continue; }
  17. //映射 URL
  18. GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
  19. String regex = ("/" + url + requestMapping.value()).replaceAll("/+", "/");
  20. Pattern pattern = Pattern.compile(regex);
  21. handlerMapping.add(new Handler(pattern,entry.getValue(),method));
  22. System.out.println("mapping " + regex + "," + method);
  23. }
  24. }
  25. }

修改 doDispatch()方法:

  1. /**
  2. * 匹配 URL
  3. * @param req
  4. * @param resp
  5. * @return
  6. * @throws Exception
  7. */
  8. private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
  9. Handler handler = getHandler(req);
  10. if(handler == null){
  11. // if(!this.handlerMapping.containsKey(url)){
  12. resp.getWriter().write("404 Not Found!!!");
  13. return;
  14. }
  15. //获得方法的形参列表
  16. Class<?> [] paramTypes = handler.getParamTypes();
  17. Object [] paramValues = new Object[paramTypes.length];
  18. Map<String,String[]> params = req.getParameterMap();
  19. for (Map.Entry<String, String[]> parm : params.entrySet()) {
  20. String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]","")
  21. .replaceAll("\\s",",");
  22. if(!handler.paramIndexMapping.containsKey(parm.getKey())){continue;}
  23. int index = handler.paramIndexMapping.get(parm.getKey());
  24. paramValues[index] = convert(paramTypes[index],value);
  25. }
  26. if(handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
  27. int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
  28. paramValues[reqIndex] = req;
  29. }
  30. if(handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
  31. int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
  32. paramValues[respIndex] = resp;
  33. }
  34. Object returnValue = handler.method.invoke(handler.controller,paramValues);
  35. if(returnValue == null || returnValue instanceof Void){ return; }
  36. resp.getWriter().write(returnValue.toString());
  37. }
  38. private Handler getHandler(HttpServletRequest req) throws Exception{
  39. if(handlerMapping.isEmpty()){ return null; }
  40. String url = req.getRequestURI();
  41. String contextPath = req.getContextPath();
  42. url = url.replace(contextPath, "").replaceAll("/+", "/");
  43. for (Handler handler : handlerMapping) {
  44. try{
  45. Matcher matcher = handler.pattern.matcher(url);
  46. //如果没有匹配上继续下一个匹配
  47. if(!matcher.matches()){ continue; }
  48. return handler;
  49. }catch(Exception e){
  50. throw e;
  51. }
  52. }
  53. return null;
  54. }
  55. private Object convert(Class<?> type,String value){
  56. if(Integer.class == type){
  57. return Integer.valueOf(value);
  58. }
  59. return value;
  60. }

在以上代码中,增加了两个方法,一个是 getHandler()方法,主要负责处理 url 的正则匹配;一个是convert()方法,主要负责 url 参数的强制类型转换。
至此,手写 Mini 版 SpringMVC 框架就已全部完成。

运行效果演示

在浏览器输入:http://localhost:8080/demo/query.json?name=Tom,就会得到下面的结果:

当然,真正的Spring 要复杂很多,本课中主要通过手写的形式,了解 Spring 的基本设计思路以及设计模式如何应用,在以后的课程中,我们还会继续手写更加高仿真版本的 Spring2.0。