在网上学习手写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