0


手写迷你Spring框架

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

文章目录

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

话不多说进入主题:
正常的创建一个web项目就好
在这里插入图片描述

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

1.1 配置application.properties

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

scanPackage=com.gupaoedu.demo

1.2 配置web.xml文件

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

<?xml version="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>XiaoZhaoWebApplication</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>/*</url-pattern>
    </servlet-mapping>
</web-app>

其中

DispatchServlet

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

1.3 自定义注解

做就做全套,我们连注解也给他模拟了,在自己包下创建annotation包,下面用的注解都是咱们自己创建的
在这里插入图片描述

1.3.1 @Service

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceService{Stringvalue()default"";}

1.3.2 @Autowired

@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceAutowired{Stringvalue()default"";}

1.3.3 @Controller

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceController{Stringvalue()default"";}

1.3.4 @RequestMapping

@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceRequestMapping{Stringvalue()default"";}

1.3.5 @RequestParam

@Target({ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceRequestParam{Stringvalue()default"";}

1.4 配置注解

配置业务实现类,此时文件结构如下
在这里插入图片描述
接口:

publicinterfaceIDemoService{Stringget(String name);}

实现类:

/**
 * 核心业务逻辑
 */@ServicepublicclassDemoServiceimplementsIDemoService{@OverridepublicStringget(String name){return"My name is "+ name +",from service.";}}

配置请求入口DemoAction:

@Controller@RequestMapping("/demo")publicclassDemoAction{@AutowiredprivateIDemoService demoService;@RequestMapping("/query")publicvoidquery(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")publicvoidadd(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")publicStringremove(@RequestParam("id")Integer id){return""+ id;}}

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

2 容器初始化

2.1 实现Spring 1.0版本

1.0版本只是有了一些简单的逻辑,对于以前写Servlet的老同学来说,看着会无比亲切,这一块没啥好说的,Spring的底层就是Servlet嘛。
核心逻辑都在

init

方法里了,让我们迅速过度到下一阶段2.0版本

publicclassDispatcherServletextendsHttpServlet{privateMap<String,Object> mapping =newHashMap<String,Object>();@OverrideprotectedvoiddoGet(HttpServletRequest req,HttpServletResponse resp)throwsServletException,IOException{this.doPost(req,resp);}@OverrideprotectedvoiddoPost(HttpServletRequest req,HttpServletResponse resp)throwsServletException,IOException{try{doDispatch(req,resp);}catch(Exception e){
            resp.getWriter().write("500 Exception "+Arrays.toString(e.getStackTrace()));}}privatevoiddoDispatch(HttpServletRequest req,HttpServletResponse resp)throwsException{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()),newObject[]{req,resp,params.get("name")[0]});}//当我晕车的时候,我就不去看源码了//init方法肯定干得的初始化的工作//inti首先我得初始化所有的相关的类,IOC容器、servletBean@Overridepublicvoidinit(ServletConfig config)throwsServletException{InputStream is =null;try{Properties configContext =newProperties();
            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);}}elseif(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");}privatevoiddoScanner(String scanPackage){URL url =this.getClass().getClassLoader().getResource("/"+ scanPackage.replaceAll("\\.","/"));File classDir =newFile(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()方法中的代码进行改造

@Overridepublicvoidinit(ServletConfig config)throwsServletException{//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配置文件中的内容privateProperties contextConfig =newProperties();//享元模式,缓存//保存扫描的所有的类名privateList<String> classNames =newArrayList<String>();//这就是传说中的IOC容器//为了简化程序,先不考虑ConcurrentHashMap,主要还是关注设计思想和原理//key默认是类名首字母小写,value就是对应的实例对象privateMap<String,Object> ioc =newHashMap<String,Object>();//保存url和Method的对应关系privateMap<String,Method> handlerMapping =newHashMap<String,Method>();

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

2.2.3 实现doLoadConfig()方法

//加载配置文件privatevoiddoLoadConfig(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()方法

//扫描相关的类privatevoiddoScanner(String scanPackage){//jar 、 war 、zip 、rar//转换为文件路径,实际上就是把 . 替换为 / URL url =this.getClass().getClassLoader().getResource("/"+ scanPackage.replaceAll("\\.","/"));File classPath =newFile(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()方法就是工厂模式的具体实现:

privatevoiddoInstance(){//初始化,为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);}elseif(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())){thrownewException("The "+ i.getName()+" is exists!!");}//直接把接口的类型当成key
                        ioc.put(i.getName(),instance);}}else{continue;}}}catch(Exception e){
            e.printStackTrace();}}

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

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

2.2.6 实现doAutowired()方法

.实现依赖注入:

privatevoiddoAutowired(){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的一对一关系privatevoiddoInitHandlerMapping(){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

@OverrideprotectedvoiddoGet(HttpServletRequest req,HttpServletResponse resp)throwsServletException,IOException{this.doPost(req,resp);}@OverrideprotectedvoiddoPost(HttpServletRequest req,HttpServletResponse resp)throwsServletException,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用到了委派模式,代码如下:

privatevoiddoDispatch(HttpServletRequest req,HttpServletResponse resp)throwsException{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 =newObject[parameterTypes.length];//根据参数位置动态赋值for(int i =0; i < parameterTypes.length; i++){Class paramterType = parameterTypes[i];if(paramterType ==HttpServletRequest.class){
                paramValues[i]= req;}elseif(paramterType ==HttpServletResponse.class){
                paramValues[i]= resp;}elseif(paramterType ==String.class){//通过运行时的状态去拿到你Annotation[][] pa = method.getParameterAnnotations();for(int j =0; j < pa.length ; j ++){for(Annotation a : pa[i]){if(a instanceofRequestParam){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的关系publicclassHandler{//必须把url放到HandlerMapping才好理解吧privatePattern pattern;//正则privateMethod method;//保存映射的方法privateObject controller;//保存方法对应的实例privateClass<?>[] paramTypes;//形参列表//参数的名字作为key,参数的顺序,位置作为值privateMap<String,Integer> paramIndexMapping;publicPatterngetPattern(){return pattern;}publicMethodgetMethod(){return method;}publicObjectgetController(){return controller;}publicClass<?>[]getParamTypes(){return paramTypes;}publicHandler(Pattern pattern,Object controller,Method method){this.pattern = pattern;this.method = method;this.controller = controller;

            paramTypes = method.getParameterTypes();

            paramIndexMapping =newHashMap<String,Integer>();putParamIndexMapping(method);}privatevoidputParamIndexMapping(Method method){//提取方法中加了注解的参数//把方法上的注解拿到,得到的是一个二维数组//因为一个参数可以有多个注解,而一个方法又有多个参数Annotation[][] pa = method.getParameterAnnotations();for(int i =0; i < pa.length ; i ++){for(Annotation a : pa[i]){if(a instanceofRequestParam){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的功能//根据设计原则:冗余的感觉了,单一职责,最少知道原则,帮助我们更好的理解privateList<Handler> handlerMapping =newArrayList<Handler>();

2.3.2修改doDispatch()方法

privatevoiddoDispatch(HttpServletRequest req,HttpServletResponse resp)throwsException{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 =newObject[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 instanceofVoid){return;}
        resp.getWriter().write(returnValue.toString());}
privateHandlergetHandler(HttpServletRequest req){if(handlerMapping.isEmpty()){returnnull;}//绝对路径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;}returnnull;}
//url传过来的参数都是String类型的,HTTP是基于字符串协议//只需要把String转换为任意类型就好privateObjectconvert(Class<?> type,String value){//如果是intif(Integer.class== type){returnInteger.valueOf(value);}elseif(Double.class== type){returnDouble.valueOf(value);}//如果还有double或者其他类型,继续加if//这时候,我们应该想到策略模式了//在这里暂时不实现,希望小伙伴自己来实现return value;}

3 至此,运行项目

在浏览器中输入:
localhost:8080/demo/query.json?name=XiaoZhao666
就会得到:
在这里插入图片描述
当然真正的Spring要复杂很多,这里只是手写了解一下Spring的基本设计思路和设计模式的应用

标签: spring java servlet

本文转载自: https://blog.csdn.net/weixin_44688973/article/details/125899117
版权归原作者 向着百万年薪努力的小赵 所有, 如有侵权,请联系我们删除。

“手写迷你Spring框架”的评论:

还没有评论