手写spring-附源码

在网上学习手写spring时有些细节想再细细琢磨下,但是没有源码,于是自己手工敲了一份,并且再把一些稍微复杂点的地方加上断点细细分解一下,主要框架还是按照参考网页的内容,源码部分由图片改成自己手写的代码,中间一些稍微复杂部分会断点展示结果做一些更细的分解,并且最后附上工程完整源码。

下面用不到400行代码来描述SpringIOC、DI、MVC的精华设计思想,并保证基本功能完整。

首先,我们先来介绍一下Spring的三个阶段,配置阶段、初始化阶段和运行阶段(如图):

配置阶段:主要是完成application.xml配置和Annotation配置。

初始化阶段:主要是加载并解析配置信息,然后,初始化IOC容器,完成容器的DI操作,已经完成HandlerMapping的初始化。

运行阶段:主要是完成Spring容器启动以后,完成用户请求的内部调度,并返回响应结果。

先来看看我们的项目结构(如下图)

 

一、配置阶段

采用的是maven管理项目。先来看pom.xml文件中的配置,引用了servlet-api的依赖。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>paogu</groupId>
  <artifactId>SpringByHand</artifactId>
  <version>0.0.1-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.4</version>
		</dependency>
	</dependencies>

	<build>
		<finalName>SpringByHand</finalName>
		<plugins>
			<plugin>
				<groupId>org.eclipse.jetty</groupId>
				<artifactId>jetty-maven-plugin</artifactId>
				<version>9.2.1.v20140609</version>
			</plugin>
		</plugins>
	</build>

</project>

另外pom里引用了jetty,jetty和tomcat一样也是servlet容器,调试的时候用它比较方便,关于jetty具体的可以网上了解下,文章后面会用它进行调试。

然后,创建GPDispatcherServlet类并继承HttpServlet,重写init()、doGet()和doPost()方法。

/**
 * 此类作为启动入口
 */
public class GPDispatcherServlet extends HttpServlet
{

   
    public GPDispatcherServlet()
    {
        super();
    }


    @Override
    public void init(ServletConfig config)
        throws ServletException
    {
        
    }
    
   
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        this.doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        
    }
    
   
}

在web.xml文件中配置以下信息:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
         <display-name>Gupao Web Application</display-name>
         
         <servlet>
             <servlet-name>gpmvc</servlet-name>
             <servlet-class>com.gupaoedu.mvcframework.servlet.GPDispatcherServlet</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>gpmvc</servlet-name>
              <url-pattern>/*</url-pattern>
         </servlet-mapping>
</web-app>

IntelliJ IDEA 有时候会在 <param-value>application.properties</param-value> 行报错,不用理会

在<init-param>中,我们配置了一个初始化加载的Spring主配置文件路径,在原生框架中,我们应该配置的是classpath:application.xml。在这里,我们为了简化操作,用properties文件代替xml文件。以下是properties文件中的内容:

接下来,我们要配置注解。现在,我们不使用Spring的一针一线,所有注解全部自己手写。

创建GPController注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPController
{
    String value() default "";
}

创建GPRequestMapping注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

 

创建GPService注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPService
{
    String value() default "";
}

 

创建GPAutowired注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPAutowired
{
    String value() default "";
}

 

创建GPRequestParam注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPRequestParam
{
    String value() default "";
}

 

关于注解需要的话可以去网上搜索学习下,这边只做一个简单的枚举说明

@Target说明了Annotation所修饰的对象范围,其中,ElementType 是一个枚举类型,有以下一些值:

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD:允许作用在属性字段上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造器上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上

@Rentention 表示注解的生命周期,简单点讲表示什么阶段这个注解就消失没用了,有以下一些值:

  • RententionPolicy.SOURCE:只能在源代码中可见,在编译成的class中看不到,主要帮助提示或校验代码,如@Override 
  • RententionPolicy.CLASS:编译成的class仍然可见,但是运行阶段不可见,不起任何作用
  • RententionPolicy.RUNTIME:运行阶段仍然可见

本例中的注解都是要在运行阶段根据注解进行操作,所以值都是RententionPolicy.RUNTIME

@Documented注解表明这个注释是由 javadoc记录的,在默认情况下也有类似的记录工具。 如果一个类型声明被注释了文档化,它的注释成为公共API的一部分。

使用自定义注解进行配置DemoAction:

package com.gupaoedu.demo.mvc.action;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.gupaoedu.demo.service.IDemoService;
import com.gupaoedu.mvcframework.annotation.GPAutowired;
import com.gupaoedu.mvcframework.annotation.GPController;
import com.gupaoedu.mvcframework.annotation.GPRequestMapping;
import com.gupaoedu.mvcframework.annotation.GPRequestParam;

@GPController
@GPRequestMapping("/demo")
public class DemoAction
{
    @GPAutowired
    private IDemoService demoService;
    
    @GPRequestMapping("/query")
    public void query(HttpServletRequest req, HttpServletResponse resp, @GPRequestParam("name") String name)
    {
        String result = demoService.get(name);
        try
        {
            resp.getWriter().write(result);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
    
}

到此,我们把配置阶段的代码全部手写完成。

二、初始化阶段

先在GPDispatcherServlet中声明几个成员变量:

public class GPDispatcherServlet extends HttpServlet
{

    private static final long serialVersionUID = 1L;

    //跟web.xml中param-name的值一致
    private static final String LOCATION = "contextConfigLocation";

    //保存所有的配置信息
    private Properties contextConfig = new Properties();

    //保存所有被扫描到的相关类名
    private List<String> classNames = new ArrayList<String>();

    //核心IOC容器,保存所有初始化的Bean
    private Map<String, Object> ioc = new HashMap<String, Object>();

    //保存所有的Url和方法的映射关系
    private Map<String, Method> handlerMapping = new HashMap<String, Method>();
    
    public GPDispatcherServlet()
    {
        super();
    }

当Servlet容器启动时,会调用GPDispatcherServlet的init()方法,从init方法的参数中,我们可以拿到主配置文件的路径,从能够读取到配置文件中的信息。前面我们已经介绍了Spring的三个阶段,现在来完成初始化阶段的代码。在init()方法中,定义好执行步骤,如下:

 @Override
    public void init(ServletConfig config)
        throws ServletException
    {
        //1.加载配置文件
        doLoadConfig(config.getInitParameter(LOCATION));
        
        //2.解析配置文件,扫描所有相关类
        doScanner(contextConfig.getProperty("scanPackage"));
        
        //3.初始化所有相关的类,并保存到IOC容器中
        doInstance();
        
        //4. 完成自动化的依赖注入,DI
        doAutoWired();
        
        //5. 创建HandlerMapping 将url和method建立对应关系
        initHandlerMapping();
        
        System.out.println("GP Spring MVC is init");
        
    }

doLoadConfig()方法的实现,将文件读取到Properties对象中:

private void  doLoadConfig(String contextConfigLocation)
    {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        //InputStream is2 = DemoAction.class.getClassLoader().getResourceAsStream(contextConfigLocation);
        // TODO Auto-generated method stub
        try
        {
            contextConfig.load(is);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (null != is)
            {
                try
                {
                    is.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

这里的contextConfigLocation就是web.xml里面配置的contextConfigLocation,读出来的是application.properties,最终将application.properties的内容装载到前面定义的contextConfig变量里

另外这里的.getClassLoader()意思是获取到ClassLoader对象,ClassLoader对象就是把相应的.class文件加载到JVM内存中变成Class对象,要详细了解可以参考https://blog.csdn.net/mrwanzh/article/details/82786498

同一个工程里面自定义的类获取的ClassLoader通常是同一个,因为getClassLoader()方法是单例的,测试断点结果如下:

首先进入getClassLoader方法

先判断ClassLoader是否存在

发现存在后返回存在的(之所以存在是因为服务器刚开始启动装载工程的时候就赋值了,感兴趣的可以源码下载下来细细断点看下)

通过断点和查看源码可以确认这个工程下不同的自定义类(如GPDispatcherServlet和DemoAction)获取的ClassLoader是同一个

这里的.getClassLoader().getResourceAsStream通常用来获取properties文件的,读的目录是classpath目录,即classes目录

再简单点理解就是打成jar包时的根目录

至此第一步的加载配置文件doLoadConfig已经完成,主要目的就是把properties文件里面的内容装载到contextConfig对象里,contextConfig对象目前里面的内容是前面讲到的

第二步

doScanner()方法,递归扫描出所有的Class文件,也就是把com.gupaoedu.demo下面的class全部装载到classNames对象里

private void doScanner(String scanPackage)
    {
        //将所有的包路径转换为文件路径
        // TODO Auto-generated method stub
        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().contains(".class"))
                {
                    continue;
                }
                String className = (scanPackage + "." + file.getName().replace(".class", "")).trim();
                classNames.add(className);
            }
        }

    }

这里的replaceAll后面是正则表达式,\\.是因为.是特殊字符,代表的含义是一个字符,想要真正替换点前面要加\\

doscanner方法跑完后classNames里面的值就是com.gupaoedu.demo下面的所有类和对应的包名

然后进入doInstance()方法,初始化所有相关的类,并放入到IOC容器之中。IOC容器的key默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。因此,要先写一个针对类名首字母处理的工具方法。

   /**
     *将首字母小写
     */
    private String lowerFirstCase(String simpleName)
    {
        char[] chars = simpleName.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

这个方法就是利用大写的字母ASC码加32变成小写字母如果

然后,再处理相关的类。

 private void doInstance()
    {
        if (classNames.isEmpty())
        {
            return;
        }
        try
        {
            for (String className : classNames)
            {
                Class<?> clazz = Class.forName(className);
                if (clazz.isAnnotationPresent(GPController.class))
                {
                    //默认将首字母小写作为beanName
                    String beanName = lowerFirstCase(clazz.getSimpleName());
                    ioc.put(beanName, clazz.newInstance());
                }
                else if (clazz.isAnnotationPresent(GPService.class))
                {
                    GPService service = clazz.getAnnotation(GPService.class);
                    String beanName = service.value();
                    //如果用户设置了名字,就用用户自己的设置
                    if ("".equals(beanName.trim()))
                    {
                        beanName = lowerFirstCase(clazz.getSimpleName());
                    }
                    Object instance = clazz.newInstance();
                    ioc.put(beanName, instance);

                    //如果自己没设,就按接口类型创建一个实例
                    Class<?>[] interfaces = clazz.getInterfaces();
                    for (Class<?> i : interfaces)
                    {
                        if (ioc.containsKey(i.getName()))
                        {
                            throw new Exception("The beanName " + i.getName() + "already exists!");
                        }
                        ioc.put(i.getName(), instance);
                    }
                }
                else
                {
                    continue;
                }
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

关于代码里的Class.forName()简单解释如下:

类加载概念

当使用一个类的时候(比如 new 一个类的实例),会检查此类是否被加载到内存,如果没有,则会执行加载操作。

读取类对应的 class 文件数据,解析此数据,构造一个此类对应的 Class 类的实例。此时JVM就可以使用该类了,比如实例化此类,或者调用此类的静态方法。

Java 也提供了手动加载类的接口,class.forName()方法就是其中之一。(说来说去,其实就是生成这个类的 Class)

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

关于clazz.newInstance()表示将class实例化成一个具体的类,简单点理解就是new A() = class.forName(A) .newInstance()

区别

newInstance: 弱类型。低效率。只能调用无参构造。 
new: 强类型。相对高效。能调用任何public构造。

关于其他更细微的区别感兴趣的可以网上学一下。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

clazz.getInterfaces()表示获取这个对象所实现的所有接口,在这个例子中这个DemoService类实现了IdemoService和IDemoService2两个接口

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

doInstance()方法结束后ioc容器里的值为

这个时候demoAction里面的demoService字段是没有赋值的

然后进入doAutowired()方法给各个类里面相应字段赋值

private void doAutoWired()
    {
        // TODO Auto-generated method stub
        if (ioc.isEmpty())
        {
            return;
        }
        
        for (Map.Entry<String, Object> entry : ioc.entrySet())
        {
            //拿到实例对象中的所有属性
            Field[] fileds = entry.getValue().getClass().getDeclaredFields();
            
            for (Field field : fileds)
            {
                if (!field.isAnnotationPresent(GPAutowired.class))
                {
                    continue;
                }
                GPAutowired autowired = field.getAnnotation(GPAutowired.class);
                String beanName = autowired.value();
                if ("".equals(beanName.trim()))
                {
                    beanName = field.getType().getName();
                }
                
                field.setAccessible(true);//将私有属性设置为可访问
                
                try
                {
                    field.set(entry.getValue(), ioc.get(beanName));
                }
                catch (IllegalArgumentException e)
                {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                    continue;
                }
                catch (IllegalAccessException e)
                {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                    continue;
                }
            }
        }
    }

IOC容器字段赋值后的结果

initHandlerMapping()方法,将GPRequestMapping中配置的信息和Method进行关联,并保存这些关系。

private void initHandlerMapping()
    {
        // TODO Auto-generated method stub
        if (ioc.isEmpty())
        {
            return;
        }
        for (Map.Entry<String, Object> entry : ioc.entrySet())
        {
            Class<?> clazz = entry.getValue().getClass();
            
            if (!clazz.isAnnotationPresent(GPController.class))
            {
                continue;
            }
            
            String baseUrl = "";
            //获取Controller的url配置
            if (clazz.isAnnotationPresent(GPRequestMapping.class))
            {
                GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
                baseUrl = requestMapping.value();
            }
            //h获取Method的url配置
            Method[] methods = clazz.getMethods();
            for (Method method : methods)
            {
                //没有加RequestMapping注解的直接忽略
                if (!method.isAnnotationPresent(GPRequestMapping.class))
                {
                    continue;
                }
                //映射URL
                GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
                String url = ("/" + baseUrl + "/" + requestMapping.value().replaceAll("/+", "/"));
                url = url.replaceAll("/+", "/");
                handlerMapping.put(url, method);
                
                System.out.println("Mapped:" + url +"," +method);
            }
        }
    }

执行完initHandlerMapping()方法后handlerMapping的值如下

到此,初始化阶段的所有代码全部写完。

三、运行阶段

来到运行阶段,当用户发送请求被Servlet接受时,先调doGet方法,doGet调用doPost方法,doPost再调用doDispach()方法,代码如下:

 

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        try
        {
            doDispatch(req,resp);
        }
        catch (Exception e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
            resp.getWriter().write("500 Exception" + Arrays.toString(e.getStackTrace()));
        }
    }

doDispatch()方法是这样写的:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception
    {
        // TODO Auto-generated method stub
        if(this.handlerMapping.isEmpty()){return;}
        String url = req.getRequestURI();
        String contextPath = req.getContextPath();
        url = url.replace(contextPath, "").replaceAll("/+", "/");
        if(!this.handlerMapping.containsKey(url)){
            resp.getWriter().write("404 Not Found!!!");
            return;
        }
        Method method = this.handlerMapping.get(url);
        //获取方法的参数列表
        Map<String,String[]> params = req.getParameterMap();
        String beanName = lowerFirstCase(method.getDeclaringClass().getSimpleName());
        String name = null;
        if(params.containsKey("name")){
            name = params.get("name")[0];
        }
        //利用反射机制调用
        method.invoke(ioc.get(beanName),new Object[] {req,resp,name});
    }

到此,我们完成了一个mini版本的Spring,麻雀虽小,五脏俱全。我们把服务发布到web容器中,

在IDEA工具里用jetty启动

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

当输入地址时代码流程为

先到GPDispatcherServlet的doGet

再到doPost()

再到doDispatch

通过反射进入DemoAction的query方法

最终将结果输出到界面上。

当然,真正的Spring要复杂很多,但核心设计思路基本如此,有理解不对的地方欢迎大家指正交流。

源码链接   https://pan.baidu.com/s/1dsP2IgfIjmU9efUtqpTBLQ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值