一个轻量级 Web 框架的实现
前言
自从 1990 年蒂莫西・约翰・伯纳斯 - 李爵士发明了 HTTP 协议之后,近 30 年来 HTTP 服务已经成为了我们生活中必不可缺的一部分,倘若没有它世界也将缺少一分光彩。我们已经知道了 HTTP 协议是用于沟通 HTTP 服务器和客户端的一种协议,那么 HTTP 服务器作为服务的提供方其重要性自然不言而喻。在 HTTP 协议刚刚诞生不久的年代,HTTP 服务器还只能处理静态网页,后来慢慢的出现了动态网页,在 1993 年 CGI 技术诞生,它可以被认为是最早期的 Web 框架之一。后来随着时间的推移,各种 Web 框架层出不穷,直到今天 Web 框架已经多的数不胜数,例如 SpringMVC、Laravel、Rails 和 Flask 等等。我因为对 SpringMVC 的注解式代码书写方式以及 Spring 容器的依赖注入非常好奇,所以便根据 Spring 的实现来书写了这个框架,故有了这篇文章。
什么是 Web 框架
Web 框架出现的目的主要是为了加快开发效率,事实上 Web 框架可能会在一定程度的降低程序的运行效率,但是我们并不在乎这一部分性能的损失。Web 框架本质上还是依赖于 HTTP 协议,在客户端看来它的响应和我们手动书写的 HTTP 响应并没有什么不同,不过它可以提升代码的可重用性,还可以提供很多方便数据访问方式。
实现一个类似于 SpringMVC 的 Web 需要解决哪些问题
我们已经知道了 Web 框架也需要依赖于 HTTP 协议,所以我们要做到能够处理 HTTP 请求并向客户端发回 HTTP 响应。除此之外,我们还需要实现 Java 的依赖注入功能,而且因为我们的框架是通过 Java 的注解做的请求映射,所以我们还需要实现注解的处理并将特定的请求转发指定的处理方法上去。
综上我们需要解决下面这几个问题:
- 处理 HTTP 请求并生成 HTTP 响应;
- 实现 Java 的依赖注入功能;
- 可以根据注解实现请求到方法的映射;
HTTP 请求的处理
我在实现 HTTP 请求处理的时候使用了两种方式,在使用框架时可以通过配置文件的方式来选择使用哪种服务器
- 第一种是使用 JavaNIO 库手动的处理 HTTP 请求和响应,实现的比较简陋,但是足够完成基本的请求和响应;
- 使用 Jetty 这个 Web 容器来处理 HTTP 请求和响应,功能更为强大,这也是框架中默认选择的服务器;
两种实现分别对应 NioServer.java 和 JettyServer.java,具体选择哪一个服务的代码如下所示
1 | // 获取服务器配置 |
实现依赖注入
依赖注入的目的是让容器来管理 JavaBean 而不是开发者自己手动来管理,它在一定程度上降低了业务代码的复杂性。我们在 Web 框架下自己实现了依赖注入的功能,它的原理是通过 Java 的反射机制来创建用于所需要的 Bean。它的核心逻辑如下所示
- 获取到当前 ClassPath 下的所有的 Class,这一步的核心逻辑是根据文件 IO 获取到 ClassPath 下所有的 *.class 文件,之后做一定处理获取到该 class 文件的包名以及类名,最后通过
Class.forName()
方法来使用反射创建该类(该部分逻辑位于 java/com/nosuchfield/geisha/utils/PackageListUtils.java);1
List<Class> classes = PackageListUtils.getAllClass();
- 在第一步我们已经获取到了所有的 class,在第二步我们扫描所有的 class 找出加上了
Component
和Configuration
注解的类,通过反射创建这些类的对象并保存;1
2
3
4
5
6// 扫描类并且创建bean,把bean保存到内存中
for (Class clazz : classes) {
if (clazz.isAnnotationPresent(Component.class) || clazz.isAnnotationPresent(Configuration.class)) {
BeansPool.getInstance().setObject(clazz, clazz.newInstance());
}
} - 扫描所有加上了
Configuration
注解的类中加上了Bean
注解的方法,并把该方法返回的对象保存;1
2
3
4
5
6
7
8
9
10
11
12
13// 把用户自定义的Bean保存到内存中去
for (Class clazz : classes) {
if (clazz.isAnnotationPresent(Configuration.class)) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Bean.class)) {
Object classObject = BeansPool.getInstance().getObject(clazz);
Object o = method.invoke(classObject); // 获取方法的返回值对象
BeansPool.getInstance().setObject(o.getClass(), o);
}
}
}
} - 把所有加上了
Resource
注解的变量进行注入;1
2
3
4
5
6
7
8
9
10
11
12
13// 把内存中的bean注入到对象中去
for (Class clazz : classes) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Resource.class)) {
Object classObject = BeansPool.getInstance().getObject(clazz);
Object fieldObject = BeansPool.getInstance().getObject(field.getType());
field.setAccessible(true);
field.set(classObject, fieldObject);
}
}
}
通过以上这几步我们已经实现了一个简单的依赖注入功能,它可以使用注解来实现对象的创建、管理和注入。
实现 HTTP 请求映射
其实在实现了依赖注入之后,请求的映射也变得很简单了。无非就是在系统启动时对另外一些注解做处理,把注解所代表的请求和指定方法映射起来,并且把这些映射关系保存起来。之后当有请求到来时,查阅请求关系获取到请求对应的处理方法,之后执行方法即可。
系统启动时的映射关系获取如下所示:
1 | // 获取到所有的class |
通过上面的代码我们已经成功的把所有的请求和方法的映射关系保存了起来,之后我们看一看当 HTTP 请求到来我们是如何做处理的。我们以 Jetty 服务器为例(NIO 的话要稍微复杂一些,因为我们还需要自己解析 HTTp 请求),看看我们是如何根据请求从 UrlMappingPool
中取出映射关系并处理的
查看 java 代码
1 | /** |
至此一次请求就能够被成功处理了。
后记
我实现的这个 Web 框架还是非常的简单的,大神请轻喷。而且我在实现 class 获取的时候并没有能够获取到 jar 包或者 war 包中的 class 信息,这也是一个比极大的缺点,以后也许会把该功能完成。