前言

自从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的注解做的请求映射,所以我们还需要实现注解的处理并将特定的请求转发指定的处理方法上去。

综上我们需要解决下面这几个问题:

  1. 处理HTTP请求并生成HTTP响应;
  2. 实现Java的依赖注入功能;
  3. 可以根据注解实现请求到方法的映射;

HTTP请求的处理

我在实现HTTP请求处理的时候使用了两种方式,在使用框架时可以通过配置文件的方式来选择使用哪种服务器

  1. 第一种是使用JavaNIO库手动的处理HTTP请求和响应,实现的比较简陋,但是足够完成基本的请求和响应;
  2. 使用Jetty这个Web容器来处理HTTP请求和响应,功能更为强大,这也是框架中默认选择的服务器;

两种实现分别对应NioServer.javaJettyServer.java,具体选择哪一个服务的代码如下所示

1
2
3
4
5
6
7
8
9
10
11
// 获取服务器配置
Map<String, Object> config = ConfigUtil.getConfig();
String server = (String) config.get("server");
int port = (Integer) config.get("port");
// 启动HTTP服务器
if ("jetty".equals(server))
JettyServer.start(port);
else if ("nio".equals(server))
NioServer.start(port);
else
throw new RuntimeException("Unknown server type [" + server + "]");

实现依赖注入

依赖注入的目的是让容器来管理JavaBean而不是开发者自己手动来管理,它在一定程度上降低了业务代码的复杂性。我们在Web框架下自己实现了依赖注入的功能,它的原理是通过Java的反射机制来创建用于所需要的Bean。它的核心逻辑如下所示

  1. 获取到当前ClassPath下的所有的Class,这一步的核心逻辑是根据文件IO获取到ClassPath下所有的*.class文件,之后做一定处理获取到该class文件的包名以及类名,最后通过 Class.forName() 方法来使用反射创建该类(该部分逻辑位于 java/com/nosuchfield/geisha/utils/PackageListUtils.java);

    1
    List<Class> classes = PackageListUtils.getAllClass();
  2. 在第一步我们已经获取到了所有的class,在第二步我们扫描所有的class找出加上了 ComponentConfiguration 注解的类,通过反射创建这些类的对象并保存;

    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());
    }
    }
  3. 扫描所有加上了 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);
    }
    }
    }
    }
  4. 把所有加上了 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取到所有的class
List<Class> classes = PackageListUtils.getAllClass();

for (Class clazz : classes) {
String classUrl = null;
// 判断当前类是否有 RequestMapping 注解,如果有则获取注解的值
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = (RequestMapping) clazz.getAnnotation(RequestMapping.class);
classUrl = requestMapping.value();
}

// 遍历该类的所有的方法
Method[] methods = clazz.getMethods();
for (Method method : methods) {
// 如果方法上有 RequestMapping 注解,则把注解的值取出来做处理然后保存到 UrlMappingPool 中
if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String methodUrl = requestMapping.value();
RequestMethod requestMethod = requestMapping.method(); // 只看方法上的HTTP METHOD
// 把类上的URL和方法上的URL连接起来
methodUrl = classUrl == null ? methodUrl : classUrl + methodUrl;
UrlMappingPool.getInstance().setMap(methodUrl, clazz, method, requestMethod);
}
}
}

通过上面的代码我们已经成功的把所有的请求和方法的映射关系保存了起来,之后我们看一看当HTTP请求到来我们是如何做处理的。我们以Jetty服务器为例(NIO的话要稍微复杂一些,因为我们还需要自己解析HTTp请求),看看我们是如何根据请求从 UrlMappingPool 中取出映射关系并处理的

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 解析请求并返回响应
*/

private static void doResponse(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取请求url
String url = request.getRequestURI();
// 获取请求method
RequestMethod requestMethod = RequestMethod.getEnum(request.getMethod());
// 从UrlMappingPool中根据请求url和method获取到对应的请求处理方法
MethodDetail methodDetail = UrlMappingPool.getInstance().getMap(url, requestMethod);

// 如果找不到对应的匹配规则则返回404
if (methodDetail == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().print(Constants.NOT_FOUND);
return;
}

// 从BeansPool中获取到该处理方法所在的类的对象
Class clazz = methodDetail.getClazz();
Object object = BeansPool.getInstance().getObject(clazz);
if (object == null)
throw new RuntimeException("can't find bean for " + clazz);

// 获取参数并保存到requestParam中
Map<String, String> requestParam = new HashMap<>();
request.getParameterMap().forEach((k, v) -> {
requestParam.put(k, v[0]);
});

List<String> params = new ArrayList<>(); // 最终的处理方法的参数
Method method = methodDetail.getMethod();

// 获取处理方法的所有的参数
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
String name = null;
// 获取参数上所有的注解
Annotation[] annotations = parameter.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == Param.class) {
Param param = (Param) annotation;
name = param.value();
break;
}
}
// 如果请求参数中存在这个方法参数就把该值赋给方法参数,否则赋值null
params.add(requestParam.getOrDefault(name, null));
}

// 调用该方法并获取返回值
Object result = method.invoke(object, params.toArray());

// 写回响应
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print(result);
}

至此一次请求就能够被成功处理了。

后记

我实现的这个Web框架还是非常的简单的,大神请轻喷。而且我在实现class获取的时候并没有能够获取到jar包或者war包中的class信息,这也是一个比极大的缺点,以后也许会把该功能完成。

项目源码:https://github.com/RitterHou/Geisha