此篇文章面向有spring、反射机制有基础的人。
昨天看了个大佬的视屏,1小时手写了个简单的Spring框架,感触极深。今天特地花了一天时间,也写了个mini版的Spring框架,基本功能完整,不过离真正的Spring还差十万八千里。
首先,先来介绍一下Spring的三个阶段,配置阶段、初始化阶段和运行阶段:
- 配置阶段:主要是完成application.xml配置和Annotation配置。
- 初始化阶段:主要是加载并解析配置信息,然后,初始化IOC容器,完成容器的DI操作,已经完成HandlerMapping的初始化。
- 运行阶段:主要是完成Spring容器启动以后,完成用户请求的内部调度,并返回响应结果。
项目结构(如下图):
准备
pom.xml需要的依赖:
1 |
|
没错只需要一个servlet-api。
controller
1 |
|
注解为自定义注解,可暂时不加
service
1 | public interface IDemoService { |
1 |
|
注解为自定义注解,可暂时不加
application.properties
1 | scanPackage=com.felix.demo |
web.xml
1 | <!DOCTYPE web-app PUBLIC |
编写自定义注解
FFController、FFAutowired、FFRequestMapping、FFRequestParam、FFService
1 | /** |
自定义注解方面配置具体功能百度
初始化
创建FFDispatcherServlet,继承HttpServlet,重写init(),doGet(),doPost()
声明几个成员变量
1 | /** |
当Servlet容器启动时,会调用FFDispatcherServlet的init()方法,从init方法的参数中,我们可以拿到主配置文件的路径,从能够读取到配置文件中的信息。前面我们已经介绍了Spring的三个阶段,现在来完成初始化阶段的代码。在init()方法中,定义好执行步骤,如下:
1 | /** |
doLoadConfig()方法的实现,将文件读取到Properties对象中
1 | //将文件读取到Properties对象中 |
doScanner()方法,递归扫描出所有的Class文件
1 | //递归扫描出所有的Class文件 |
doInstance()方法,初始化所有相关的类,并放入到IOC容器之中。
IOC容器的key默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。因此,要先写一个针对类名首字母处理的工具方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class StringUtil {
/**
* 首字母小写
* @param str
* @return
*/
public static String lowerFirstCase(String str) {
char[] chars = str.toCharArray();
if (chars[0] >= 'A' && chars[0] <= 'Z') {
chars[0] += 32;
}
return String.valueOf(chars);
}
}
然后,再处理相关的类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42// 初始化所有相关的类,并放入到IOC容器之中。
// IOC容器的key默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。
private void doInstance() {
if (classNames.size() == 0) {
return;
}
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
// isAnnotationPresent:如果指定类型的注解存在于此元素上,则返回 true,否则返回 false
if (clazz.isAnnotationPresent(FFController.class)) {
//默认将首字母小写座位beanName
String beanName = StringUtil.lowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, clazz.newInstance());
} else if (clazz.isAnnotationPresent(FFService.class)) {
//getAnnotation:该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。
FFService service = clazz.getAnnotation(FFService.class);
String beanName = service.value();
//若用户设置了名字,用用户设置的
if (!"".equals(beanName.trim())) {
ioc.put(beanName, clazz.newInstance());
continue;
}
//用户没设置,就按照接口类型创建一个实例
//getInterfaces返回该类所实现的接口的一个数组
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> anInterface : interfaces) {
ioc.put(anInterface.getName(), clazz.newInstance());
}
} else {
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
doAutowired()方法,将初始化到IOC容器中的类,需要赋值的字段进行赋值
1 | //将初始化到IOC容器中的类,需要赋值的字段进行赋值 |
initHandlerMapping()方法,将FFRequestMapping中配置的信息和Method进行关联,并保存这些关系。
1 |
|
到此,初始化阶段的所有代码全部写完。
运行阶段
来到运行阶段,当用户发送请求被Servlet接受时,都会统一调用doPost方法,我先在doPost方法中再调用doDispach()方法,代码如下:
1 |
|
doDispatch()
1 |
|
到此,我们完成了一个mini版本的Spring,麻雀虽小,五脏俱全。我们把服务发布到web容器中,然后,在浏览器输入:http://localhost:8080/ffmvc/demo/query.json?name=felix,就会得到下面的结果:
当然,真正的Spring要复杂很多,但核心设计思路基本如此。
代码已上传至我的github: https://github.com/542869246/felix-mvcframework