随着不断地使用Spring,以及后续的Boot、cloud,不断的体会到这个拯救Java的生态体系的强大,也使我对于这个框架有了极大的好奇心,以至于产生了我为什么不能写一个这样的框架的思考。
通过自学及参考谭勇德(Tom)老师的《Spring 5核心原理》这本书,决定记录我手写Spring的过程,记录此系列博客 。
愿每个想探究Spring原理的人,学习道路一帆风顺

文章目录

  • 1 准备阶段——自定义配置
    • 1.1 配置application.properties
    • 1.2 配置web.xml文件
    • 1.3 自定义注解
      • 1.3.1 @Service
      • 1.3.2 @Autowired
      • 1.3.3 @Controller
      • 1.3.4 @RequestMapping
      • 1.3.5 @RequestParam
    • 1.4 配置注解
  • 2 容器初始化
    • 2.1 实现Spring 1.0版本
    • 2.2 实现Spring 2.0版本
      • 2.2.1 将init()方法中的代码进行改造
      • 2.2.2 在类的开头声明变量
      • 2.2.3 实现doLoadConfig()方法
      • 2.2.4 实现doScanner()方法
      • 2.2.5 实现doInstance()方法
      • 2.2.6 实现doAutowired()方法
      • 2.2.7 实现doInitHandlerMapping()方法
      • 2.2.8 doGet和doPost的doDispatch
    • 2.3 实现Spring 3.0版本
      • 2.3.1 HandlerMapping内部类Handler
      • 2.3.2修改doDispatch()方法
  • 3 至此,运行项目

Spring最初的时候,其功能远远不如现在强大,甚至我在看Spring最初版本的时候有种这就是所谓的Spring?的疑问,但随后便恍然大悟,我是站立在历史的下游,用后人的眼光去看前人的作品,当然有种站在制高点俯视的感觉,当我一步一步深入学习Spring的设计思想设计理念以及实现方式的时候,无不为前人那惊天地泣鬼神的思想所震撼。

话不多说进入主题:
正常的创建一个web项目就好

1 准备阶段——自定义配置

1.1 配置application.properties

为了解析方便,我们用application.properties来作为配置文件,内容很简单,如下:

scanPackage=com.gupaoedu.demo

1.2 配置web.xml文件

大家都知道,所有依赖于Web容器的项目都是从读取web.xml文件开始的。

<" />="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"version="2.4"><display-name>XiaoZhao Web Application</display-name><servlet><servlet-name>zhao mvc</servlet-name><servlet-class>com.xiaoZhao666.mvcframework.v1.servlet.DispatchServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>application.properties</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>zhao mvc</servlet-name><url-pattern>/*

其中DispatchServlet是模拟Spring实现的核心功能类

1.3 自定义注解

做就做全套,我们连注解也给他模拟了,在自己包下创建annotation包,下面用的注解都是咱们自己创建的

1.3.1 @Service

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Service {String value() default "";}

1.3.2 @Autowired

@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Autowired {String value() default "";}

1.3.3 @Controller

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Controller {String value() default "";}

1.3.4 @RequestMapping

@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RequestMapping {String value() default "";}

1.3.5 @RequestParam

@Target({ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RequestParam {String value() default "";}

1.4 配置注解

配置业务实现类,此时文件结构如下

接口:

public interface IDemoService {String get(String name);}

实现类:

/** * 核心业务逻辑 */@Servicepublic class DemoService implements IDemoService{@Overridepublic String get(String name) {return "My name is " + name + ",from service.";}}

配置请求入口DemoAction:

@Controller@RequestMapping("/demo")public class DemoAction {@Autowiredprivate IDemoService demoService;@RequestMapping("/query")public void query(HttpServletRequest req, HttpServletResponse resp,@RequestParam("name") String name){String result = demoService.get(name);try {resp.getWriter().write(result);} catch (IOException e) {e.printStackTrace();}}@RequestMapping("/add")public void add(HttpServletRequest req, HttpServletResponse resp,@RequestParam("a") Integer a, @RequestParam("b") Integer b){try {resp.getWriter().write(a + "+" + b + "=" + (a + b));} catch (IOException e) {e.printStackTrace();}}@RequestMapping("/remove")public Stringremove(@RequestParam("id") Integer id){return "" + id;}}

至此,我们的所有配置就算完成了。

2 容器初始化

2.1 实现Spring 1.0版本

1.0版本只是有了一些简单的逻辑,对于以前写Servlet的老同学来说,看着会无比亲切,这一块没啥好说的,Spring的底层就是Servlet嘛。
核心逻辑都在init方法里了,让我们迅速过度到下一阶段2.0版本

public class DispatcherServlet extends HttpServlet {private Map<String,Object> mapping = new HashMap<String, Object>();@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req,resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {try {doDispatch(req,resp);} catch (Exception e) {resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));}}private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {String url = req.getRequestURI();String contextPath = req.getContextPath();url = url.replace(contextPath, "").replaceAll("/+", "/");if(!this.mapping.containsKey(url)){resp.getWriter().write("404 Not Found!!");return;}Method method = (Method) this.mapping.get(url);Map<String,String[]> params = req.getParameterMap();method.invoke(this.mapping.get(method.getDeclaringClass().getName()),new Object[]{req,resp,params.get("name")[0]});}//当我晕车的时候,我就不去看源码了//init方法肯定干得的初始化的工作//inti首先我得初始化所有的相关的类,IOC容器、servletBean@Overridepublic void init(ServletConfig config) throws ServletException {InputStream is = null;try{Properties configContext = new Properties();is = this.getClass().getClassLoader().getResourceAsStream(config.getInitParameter("contextConfigLocation"));configContext.load(is);String scanPackage = configContext.getProperty("scanPackage");doScanner(scanPackage);for (String className : mapping.keySet()) {if(!className.contains(".")){continue;}Class<" />> clazz = Class.forName(className);if(clazz.isAnnotationPresent(Controller.class)){mapping.put(className,clazz.newInstance());String baseUrl = "";if (clazz.isAnnotationPresent(RequestMapping.class)) {RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);baseUrl = requestMapping.value();}Method[] methods = clazz.getMethods();for (Method method : methods) {if (!method.isAnnotationPresent(RequestMapping.class)) {continue; }RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);String url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");mapping.put(url, method);System.out.println("Mapped " + url + "," + method);}}else if(clazz.isAnnotationPresent(Service.class)){Service service = clazz.getAnnotation(Service.class);String beanName = service.value();if("".equals(beanName)){beanName = clazz.getName();}Object instance = clazz.newInstance();mapping.put(beanName,instance);for (Class<?> i : clazz.getInterfaces()) {mapping.put(i.getName(),instance);}}else {continue;}}for (Object object : mapping.values()) {if(object == null){continue;}Class clazz = object.getClass();if(clazz.isAnnotationPresent(Controller.class)){Field [] fields = clazz.getDeclaredFields();for (Field field : fields) {if(!field.isAnnotationPresent(Autowired.class)){continue; }Autowired autowired = field.getAnnotation(Autowired.class);String beanName = autowired.value();if("".equals(beanName)){beanName = field.getType().getName();}field.setAccessible(true);try {field.set(mapping.get(clazz.getName()),mapping.get(beanName));} catch (IllegalAccessException e) {e.printStackTrace();}}}}} catch (Exception e) {}finally {if(is != null){try {is.close();} catch (IOException e) {e.printStackTrace();}}}System.out.print("MVC Framework is init");}private void doScanner(String scanPackage) {URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));File classDir = new File(url.getFile());for (File file : classDir.listFiles()) {if(file.isDirectory()){ doScanner(scanPackage + "." +file.getName());}else {if(!file.getName().endsWith(".class")){continue;}String clazzName = (scanPackage + "." + file.getName().replace(".class",""));mapping.put(clazzName,null);}}}}

2.2 实现Spring 2.0版本

让我们迅速过度到2.0版本,改造1.0版本的DispatchServlet。
我们在1.0的版本上进行优化,加入Spring中使用的设计模式(工厂模式,单例模式,委派模式,策略模式),将init()方法中的代码进行封装。按照Spring框架的实现思路,先搭基础框架,再“填肉注血”,具体代码如下:

2.2.1 将init()方法中的代码进行改造

@Overridepublic void init(ServletConfig config) throws ServletException {//1、加载配置文件doLoadConfig(config.getInitParameter("contextConfigLocation"));//2、扫描相关的类doScanner(contextConfig.getProperty("scanPackage"));//==============IoC部分==============//3、初始化IoC容器,将扫描到的相关的类实例化,保存到IcC容器中doInstance();//AOP,新生成的代理对象//==============DI部分==============//4、完成依赖注入doAutowired();//==============MVC部分==============//5、初始化HandlerMappingdoInitHandlerMapping();System.out.println("GP Spring framework is init.");}

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

2.2.2 在类的开头声明变量

//保存application.properties配置文件中的内容private Properties contextConfig = new Properties();//享元模式,缓存//保存扫描的所有的类名private List<String> classNames = new ArrayList<String>();//这就是传说中的IOC容器//为了简化程序,先不考虑ConcurrentHashMap,主要还是关注设计思想和原理//key默认是类名首字母小写,value就是对应的实例对象private Map<String,Object> ioc = new HashMap<String,Object>();//保存url和Method的对应关系private Map<String,Method> handlerMapping = new HashMap<String, Method>();

按照init方法的步骤,实现doLoadConfig()方法:

2.2.3 实现doLoadConfig()方法

//加载配置文件private void doLoadConfig(String contextConfigLocation) {//直接通过类路径找到Spring主配置文件所在的路径,并且将其读取出来放在Properties对象中InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);try {contextConfig.load(is);} catch (IOException e) {e.printStackTrace();}finally {if(null != is){try {is.close();} catch (IOException e) {e.printStackTrace();}}}}

2.2.4 实现doScanner()方法

//扫描相关的类private void doScanner(String scanPackage) {//jar 、 war 、zip 、rar//转换为文件路径,实际上就是把 . 替换为 / URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));File classPath = new File(url.getFile());//当成是一个ClassPath文件夹for (File file : classPath.listFiles()) {if(file.isDirectory()){doScanner(scanPackage + "." + file.getName());}else {if(!file.getName().endsWith(".class")){continue;}//全类名 = 包名.类名String className = (scanPackage + "." + file.getName().replace(".class", ""));//Class.forName(className);classNames.add(className);}}}

2.2.5 实现doInstance()方法

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

private void doInstance() {//初始化,为DI做准备if(classNames.isEmpty()){return;}try {for (String className : classNames) {Class<?> clazz = Class.forName(className);//什么样的类才需要初始化呢?———// 加了注解的类才初始化>>>模拟Spring框架中的注解开发——// 只用@Controller和@Service举例if(clazz.isAnnotationPresent(Controller.class)) {//key提取出来了,把value也搞出来//Spring默认类名首字母小写String beanName = toLowerFirstCase(clazz.getSimpleName());Object instance = clazz.newInstance();ioc.put(beanName, instance);}else if(clazz.isAnnotationPresent(Service.class)){//1、在多个包下出现相同的类名,只能寄几(自己)起一个全局唯一的名字//自定义命名String beanName = clazz.getAnnotation(Service.class).value();if("".equals(beanName.trim())){beanName = toLowerFirstCase(clazz.getSimpleName());}//2、默认的类名首字母小写Object instance = clazz.newInstance();ioc.put(beanName, instance);//3、如果是接口————投机取巧一下,嘿嘿//判断有多少个实现类,如果只有一个,默认就选择这个实现类//如果有多个,只能抛异常for (Class<?> i : clazz.getInterfaces()) {if(ioc.containsKey(i.getName())){throw new Exception("The " + i.getName() + " is exists!!");}//直接把接口的类型当成keyioc.put(i.getName(),instance);}}else{continue;}}}catch (Exception e){e.printStackTrace();}}

这里为了处理方便,自己实现了toLowerFirstCase()方法,来实现类名首字母小写:

//自己写,自己用private String toLowerFirstCase(String simpleName) {char [] chars = simpleName.toCharArray();//之所以要加法,是因为大小写字母的ASCII码相差32chars[0] += 32;return String.valueOf(chars);}

2.2.6 实现doAutowired()方法

.实现依赖注入:

private void doAutowired() {if(ioc.isEmpty()){return;}for (Map.Entry<String,Object> entry : ioc.entrySet()) {//把所有的包括private/protected/default/public 修饰字段都取出来for (Field field : entry.getValue().getClass().getDeclaredFields()) {if(!field.isAnnotationPresent(Autowired.class)){ continue; }Autowired autowired = field.getAnnotation(Autowired.class);//如果用户没有自定义的beanName,就默认根据类型注入String beanName = autowired.value().trim();if("".equals(beanName)){//field.getType().getName() 获取字段的类型beanName = field.getType().getName();}//暴力访问field.setAccessible(true);try {//用反射机制动态给字段赋值field.set(entry.getValue(),ioc.get(beanName));} catch (IllegalAccessException e) {e.printStackTrace();}}}}

2.2.7 实现doInitHandlerMapping()方法

这一步其实就到了部分MVC的部分了。另外HandlerMapping是策略模式的案例

//初始化url和Method的一对一关系private void doInitHandlerMapping() {if(ioc.isEmpty()){ return;}for (Map.Entry<String,Object> entry : ioc.entrySet()) {Class<?> clazz = entry.getValue().getClass();if(!clazz.isAnnotationPresent(Controller.class)){ continue; }//相当于提取 class上配置的url//也就是@RequestMapping上的路径String baseUrl = "";if(clazz.isAnnotationPresent(RequestMapping.class)){RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);baseUrl = requestMapping.value();}//只获取public的方法for (Method method : clazz.getMethods()) {if(!method.isAnnotationPresent(RequestMapping.class)){continue;}//提取每个方法上面配置的urlRequestMapping requestMapping = method.getAnnotation(RequestMapping.class);// //demo//queryString url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/");handlerMapping.put(url,method);System.out.println("Mapped : " + url + "," + method);}}}

到这里容器初始化的部分就完成了,接下来只要完成运行时的处理逻辑就行了,一起来写doGet和doPost叭

2.2.8 doGet和doPost的doDispatch

@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req,resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//记得刚才init方法的5步吗,这是第六步//6、委派,根据URL去找到一个对应的Method并通过response返回try {doDispatch(req,resp);} catch (Exception e) {e.printStackTrace();resp.getWriter().write("500 Exception,Detail : " + Arrays.toString(e.getStackTrace()));}}

这里doDispatch用到了委派模式,代码如下:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {String url = req.getRequestURI();String contextPath = req.getContextPath();url = url.replaceAll(contextPath,"").replaceAll("/+","/");if(!this.handlerMapping.containsKey(url)){resp.getWriter().write("404 Not Found!!!");return;}Method method = this.handlerMapping.get(url);//第一个参数:方法所在的实例//第二个参数:调用时所需要的实参//保存请求的url参数列表Map<String,String[]> params = req.getParameterMap();//获取形参列表Class<?> [] parameterTypes = method.getParameterTypes();//保存赋值参数的位置Object [] paramValues = new Object[parameterTypes.length];//根据参数位置动态赋值for (int i = 0; i < parameterTypes.length; i++) {Class paramterType = parameterTypes[i];if(paramterType == HttpServletRequest.class){paramValues[i] = req;}else if(paramterType == HttpServletResponse.class){paramValues[i] = resp;}else if(paramterType == String.class){//通过运行时的状态去拿到你Annotation[] [] pa = method.getParameterAnnotations();for (int j = 0; j < pa.length ; j ++) {for(Annotation a : pa[i]){if(a instanceof RequestParam){String paramName = ((RequestParam) a).value();if(!"".equals(paramName.trim())){ String value = Arrays.toString(params.get(paramName)) .replaceAll("\\[|\\]","") .replaceAll("\\s+",",");paramValues[i] = value;}}}}}}//暂时硬编码String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());//赋值实参列表method.invoke(ioc.get(beanName),paramValues);}

2.3 实现Spring 3.0版本

在2.0版本中,基本功能已经实现,但是代码还不够优雅,比如HandlerMapping还不能像真正的Spring一样支持正则,url参数还不能支持强制类型转换,在反射调用前还需要重新获取beanName,我们来继续优化

首先改造HandlerMapping,在真实的Spring源码中,HandlerMapping其实是一个List而不是一个Map。List中的元素是自定义类型的。现在我们来仿写这一段,定义一个内部类Handler:

2.3.1 HandlerMapping内部类Handler

//保存一个url和一个Method的关系public class Handler {//必须把url放到HandlerMapping才好理解吧private Pattern pattern;//正则private Method method;//保存映射的方法private Object controller;//保存方法对应的实例private Class<?> [] paramTypes;//形参列表//参数的名字作为key,参数的顺序,位置作为值private Map<String,Integer> paramIndexMapping;public Pattern getPattern() {return pattern;}public Method getMethod() {return method;}public Object getController() {return controller;}public Class<?>[] getParamTypes() {return paramTypes;}public Handler(Pattern pattern, Object controller, Method method) {this.pattern = pattern;this.method = method;this.controller = controller;paramTypes = method.getParameterTypes();paramIndexMapping = new HashMap<String, Integer>();putParamIndexMapping(method);}private void putParamIndexMapping(Method method){//提取方法中加了注解的参数//把方法上的注解拿到,得到的是一个二维数组//因为一个参数可以有多个注解,而一个方法又有多个参数Annotation [] [] pa = method.getParameterAnnotations();for (int i = 0; i < pa.length ; i ++) {for(Annotation a : pa[i]){if(a instanceof RequestParam){String paramName = ((RequestParam) a).value();if(!"".equals(paramName.trim())){paramIndexMapping.put(paramName, i);}}}}//提取方法中的request和response参数Class<?> [] paramsTypes = method.getParameterTypes();for (int i = 0; i < paramsTypes.length ; i ++) {Class<?> type = paramsTypes[i];if(type == HttpServletRequest.class ||type == HttpServletResponse.class){paramIndexMapping.put(type.getName(),i);}}}}

然后优化HandlerMapping的结构:

//思考:为什么不用Map//你用Map的话,key,只能是url//Handler 本身的功能就是把url和method对应关系,已经具备了Map的功能//根据设计原则:冗余的感觉了,单一职责,最少知道原则,帮助我们更好的理解private List<Handler> handlerMapping = new ArrayList<Handler>();

2.3.2修改doDispatch()方法

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception { Handler handler = getHandler(req); if(handler == null){//if(!this.handlerMapping.containsKey(url)){resp.getWriter().write("404 Not Found!!!");return;}//获得方法的形参列表Class<?> [] paramTypes = handler.getParamTypes();Object [] paramValues = new Object[paramTypes.length];Map<String,String[]> params = req.getParameterMap();for (Map.Entry<String, String[]> parm : params.entrySet()) {String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]","").replaceAll("\\s",",");if(!handler.paramIndexMapping.containsKey(parm.getKey())){continue;}int index = handler.paramIndexMapping.get(parm.getKey());paramValues[index] = convert(paramTypes[index],value);}if(handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());paramValues[reqIndex] = req;}if(handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());paramValues[respIndex] = resp;}Object returnValue = handler.method.invoke(handler.controller,paramValues);if(returnValue == null || returnValue instanceof Void){ return; }resp.getWriter().write(returnValue.toString());}
private Handler getHandler(HttpServletRequest req) {if(handlerMapping.isEmpty()){return null;}//绝对路径String url = req.getRequestURI();//处理成相对路径String contextPath = req.getContextPath();url = url.replaceAll(contextPath,"").replaceAll("/+","/");for (Handler handler : this.handlerMapping) {Matcher matcher = handler.getPattern().matcher(url);if(!matcher.matches()){ continue;}return handler;}return null;}
//url传过来的参数都是String类型的,HTTP是基于字符串协议//只需要把String转换为任意类型就好private Object convert(Class<?> type,String value){//如果是intif(Integer.class == type){return Integer.valueOf(value);}else if(Double.class == type){return Double.valueOf(value);}//如果还有double或者其他类型,继续加if//这时候,我们应该想到策略模式了//在这里暂时不实现,希望小伙伴自己来实现return value;}

3 至此,运行项目

在浏览器中输入:
localhost:8080/demo/query.json?name=XiaoZhao666
就会得到:

当然真正的Spring要复杂很多,这里只是手写了解一下Spring的基本设计思路和设计模式的应用