0%

MINI Spring MVC

在乎的是,自己以及他所在乎的人,能不能够秉持着自己的心意与信念,在人生中大部分时间里都能自在、快乐

项目github地址:https://github.com/wmsheng/MINI-SpringMVC

项目框架搭建

搭建基于Gradle的项目

首先进行项目的”骨架”的搭建。

使用Gradle作为依赖创建项目,相比Maven,Gradle的优势在于:

  • 发扬Maven的约定大于配置的思想(为了迁移方便,其可以直接使用Maven的配置)
  • 使用DSL语言 提供函数支持
  • 方便性上:Json格式,而且免安装

先创建Gradle项目,然后创建子的Module模块.

子模块需要创建多个。这里创建这么几个:framework框架模块、test测试模块,创建好后如下图:

创建项目1.png

因为我们不需要在项目的父模块中写代码,只需要在子模块中写,所以可以删除父模块层中的src包。

删除src包.png

可以看到,右侧中有配置文件,默认使用了很多配置项。Maven中一行就能完成的配置,在Gradle中一行就可以完成。

在配置subprojects的时候,其中的所有配置项都会被子模块继承。但是可以注意,当子模块中有父模块中重复的配置项时,会以范围更近的子模块为主,会覆盖父模块的配置。

在配置好之后,可以进行打包,命令是

1
gradle build

在打包成功后,各个子模块中都会多出一个build文件夹,而在libs文件夹下就是jar包文件。如下图:

gradle_build之后.png

仿Spring搭建项目结构

我们已经创建好了framework和test这两个模块,分别对应框架模块应用模块。应用模块主要用来测试,框架可以支撑应用模块,而应用模块可以反馈框架。

重点在于框架模块。Spring包的模块划分主要可以分为两层。一层是核心层(Core),它是Spring框架的基石。其包括:

  • Beans包:负责Spring的Bean的维护和管理
  • Core包:Spring中经常用到的工具包
  • Context包:提供Spring根据不同产品的需求所完成的接口,是进入Spring的入口
  • SpEL:Spring的R式语言包,可以提供查询、操作数据的功能

再往上,一个是数据层,Data。这里主要用于对数据的增删改查。其中有JDBC、ORM的针对数据操作的模型。

还有一个是对外的Web应用包,其中有MVC模型和对Servlet的封装。

如下图:

Spring包结构.png

我们这个项目主要是MINI,所以目的是实现SpringMVC的一部分重要模块。

  • 实现Core模块,包括core、beans、context包
  • 实现Web,集成web和webmvc
  • 添加starter,实现类spring-boot的启动方式

开发中,我们将test模块和frameword模块进行了关联,重点是在test模块的gradle配置文件中进行配置。

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
plugins {
id 'java'
}

group 'wms.mooc.com'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile(project(':framework'))
}

subprojects {

}
jar{
manifest{
attributes "Main-Class":"com.mooc.wms.Application"
}
from{
configurations.compile.collect{
it.isDirectory() ? it : zipTree(it)
}
}
}

dependencies中配置了编译过程中要关联的其他子模块的名字,注意写法。这里关联的是framework模块。

集成服务器

接下来进行服务器Web服务器和Servelet的集成,可以被称作是项目的载体。

首先回顾一下JavaWeb模型。我们的程序想要对外提供服务,必须借助Web服务器。常见的web服务器有Tomcat、Netty、Nginx等。

  • 监听一个TCP端口(本地启动一般监听localhost,80,8080)
  • 负责转发请求,回收相应(Nginx转发给PHP程序、Tomcat转发给Java程序)
  • 本身没有业务逻辑,只负责连接操作系统和应用程序代码

系统Web服务模型

首先,客户端会向web服务器发送一个请求,通过网络到达web服务器所在的操作系统。

但是从网络传输过去的信息只能是一些bit流,我们无法直接读取其中的信息。这些bit流通过网卡到达了操作系统。然后操作系统的TCP/IP栈专门负责解析这些bit流,再之后操作系统会把这些信息(端口、IP、字节流)都发送给Web服务器来进行处理。

到达Web服务器之后,Web服务器会连接到应用程序,由应用程序处理其中的逻辑和进行操作。

处理完之后,会把结果原路返回,再相应回客户端。

具体流程如下图所示:

系统web服务模型.png

请求分发流程

请求分发流程.png

上图是一个抽象的比喻,发信人和收信人的关系。

邮局就好像是TCP端口,不论有没有信件往来,它都在那里等着。一旦有信件往来,它就把这个信件交给一个快递员,也就是web服务器。这个web服务器每天守着一个TCP端口,如果有消息派发给他,他就赶快把消息送出,再把回复收回,再把回复发到给发信人。

就好像快递员不能拆开快递一样,服务器只能充当操作系统和应用程序的连接着,他们不能负责具体业务,只负责高效而准确地发送任务。

那么,web服务器是怎么分发请求的呢?——这就不得不介绍Servlet

实际上,Servlet是一种复合的含义。

  • Servlet是一种规范:它约束了Java服务器与业务类的通信方式。试想不同的服务器有不同的通信方式,那么你每一台服务器都要单独写一套逻辑,那么学习成本高,迁移也会非常困难
  • Servlet是一个接口:javax.servlet.Servlet,这个是用代码来表达Servlet这种规范的方法
  • Servlet是一种Java类:实现了Servlet接口的应用程序类。每一个实现了Servlet应用接口的类都可以成为是一个Servlet。大型服务一般需要多个Servlet共同合作来完成。

到这里,可以再补充一下系统Web服务模型,里面的Servlet实际上可以更细化:

系统web服务模型细化Servlet.png

实际上,SpringBoot中通过向框架中集成Web服务器来实现的。我们这里使用Tomcat来进行集成。

使用Tomcat服务器有以下一些特点:

  • Java原生,运行在JVM上
  • 多种并发模型,高性能
  • 支持嵌入式应用程序

具体流程很简单,我们只需要在项目中:

  1. 引入Tomcat包
  2. 实例化一个Tomcat类
  3. 调用Tomcat的start方法

走这三步,就可以启动Tomcat服务器。

集成Tomcat服务器

首先,我们为tomcat引入一个tomcat包,写在gradle的配置文件中。

在Maven官网查找Tomcat Embed Core, https://mvnrepository.com/search?q=tomcat。选一个版本,这里选8.5.23,配置`framework`的gradle中。

导入成功后,在framework的web包下创建server包,然后完成相应逻辑任务。

TomcatServer.java

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
package com.mooc.wms.web.server;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;

/**
* @author Bennett_Wang on 2020/5/2
*/
public class TomcatServer {
public Tomcat tomcat;
public String[] args;

public TomcatServer(String[] args) {
this.args = args;
}

public void startServer() throws LifecycleException {
tomcat = new Tomcat();
tomcat.setPort(6699);
tomcat.start();

Thread awaitThread = new Thread("tomcat_await_thread") {
@Override
public void run() {
TomcatServer.this.tomcat.getServer().await();
}
};
awaitThread.setDaemon(false);
awaitThread.run();
}
}

TestServlet.java

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
package com.mooc.wms.web.servlet;

import javax.servlet.*;
import java.io.IOException;

/**
* @author Bennett_Wang on 2020/5/2
*/
public class TestServlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
res.getWriter().println("test");
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}

在Tomcat容器内,Servlet容器并不是直接依附在Tomcat容器里的,而是会分成四个级别,Tomcat用这四个级别进行容器的解耦。这四个级别分别是:

  1. Engine容器:最顶级容器,可以理解为Tomcat的总控中心
  2. Host容器:管理主机信息和他们的子容器
  3. Context:最接近Servlet的容器,可以通过它设置资源属性和管理骨架
  4. Wrapper:负责Servlet的加载、初始化、执行、销毁

我们这里只往Context容器内注册一个Servlet即可。

为了建立Tomcat和Servlet的关联,我们需要在TomcatServer.java类中加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//初始化Context容器
Context context = new StandardContext();
context.setPath("");
context.addLifecycleListener(new Tomcat.FixContextListener());

// 创建TestServlet实例
TestServlet servlet = new TestServlet();
//使用Tomcat的静态方法addServlet
Tomcat.addServlet(context,"testServlet",servlet)
.setAsyncSupported(true);

//将Context容器和"testServlet"实例关联起来
context.addServletMappingDecoded("/test.json","testServlet");
tomcat.getHost().addChild(context);

此时,启动之后,可以用localhost:6699/test.json访问到页面,当然,结果只有一个输出的”test”,即为TestServlet.java中的service函数:

1
2
3
4
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
res.getWriter().println("test");
}

集成服务器总结

总而言之,这里使用了Web服务模型,其中最重要的是使用了Servlet标准,它规定了Web服务器和应用程序之间的通信方式

最后,要把这个流程真正运行起来,要一个Web服务器。这里我们仿照Spring Boot,往框架里嵌入了一个Tomcat的Web服务器,实现了Servlet接口,往Tomcat中注册了一个Servlet实例,实现了真正的请求响应。即上面往TomcatServer.java中加入的代码。

控制器Controller的实现

Controller可以看做是项目的入口了。

传统项目的实现

传统项目中,为了解决请求分发,需要配置Servlet,因为Servlet经常会有很多,所以经常需要配置大量内容到web.xml中(在classpath下的web.xml中配置URI到Servlet的映射,Tomcat会在启动时自动加载这个文件),每个业务都需要添加一个Servlet,而每个Servlet都对应一个URI。

这样在小型项目中可能没太多不妥。但是随着项目发展,这样传统的配置方式的问题会逐渐暴露:

  • 配置集中,大而杂,不易管理(特别是Servlet的职责没有明确界限时,很容易把URI配置错)
  • 需要多次实现Servlet接口,不必要(Servlet接口有五个方法,虽然我们一般只需要实现service方法,但是实现了接口,其他方法哪怕是空的,也要写上啊)

Spring框架

Spring框架解决这个问题的方式,就是引入了DispatcherServlet作为”总管”,它直接在Tomcat中配置了根目录路径符,表示所有的请求都由它来受理。

当然,我们不能把所有业务都写在DispatcherServlet中。Spring做的另一件事,是把所有的Servlet都简化成了一个一个的”Mapping Handler”。在代码中,就是Controller类中的一个方法。一般用requestMapping来注解。所以实际中会有请求打到DispatcherServlet上,然后通过DispatcherServlet发送到Mapping Handler上,再由Mapping Handler处理,之后返回。

总结起来,用Spring框架进行调度的优势:

  1. 用注解,实现简单,按需实现(需要用哪个方法,就用哪个注解即可)
  2. 配置分散,不杂乱(可以通过多个Controller把业务逻辑的界限定义的很清晰)
  3. 容器内实现,易控制(真正的分发变成了框架处理,不再受web服务器的控制,修改和拓展更方便)

实战添加web组件

首先将之前定义的TestServlet该名字,改成DispatcherServlet。

然后修改Tomcat和Servlet联系的方式,名字都改成dispatcherServlet。并且修改MappingDecoded的路径,改成”/“,表示根目录符,表示根目录下所有的URI。

1
2
3
4
5
6
7
8
9
// 创建TestServlet实例
DispatcherServlet servlet = new DispatcherServlet();
//使用Tomcat的静态方法addServlet
Tomcat.addServlet(context,"dispatcherServlet",servlet)
.setAsyncSupported(true);

//将Context容器和"testServlet"实例关联起来
context.addServletMappingDecoded("/","dispatcherServlet");
tomcat.getHost().addChild(context);

接下来再在web包下面添加MVC包。MVC包中添加Controller、RequestMapping和RequestParam这三个我们常用的注解。

@Controller

1
2
3
4
5
6
7
8
9
10
11
package com.mooc.wms.web.mvc;

import java.lang.annotation.*;

@Documented
//生命周期为运行期
@Retention(RetentionPolicy.RUNTIME)
//作用目标是:类
@Target(ElementType.TYPE)
public @interface Controller {
}

@RequestMapping

1
2
3
4
5
6
7
8
9
10
11
12
package com.mooc.wms.web.mvc;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
//目标:Controller中的方法
@Target(ElementType.METHOD)
public @interface RequestMapping {
//需要添加属性用来保存映射的URI
String value();
}

@RequestParam

1
2
3
4
5
6
7
8
9
10
11
12
package com.mooc.wms.web.mvc;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
//这个注解目标是Controller注解的参数
@Target(ElementType.PARAMETER)
public @interface RequestParam {
//需要添加一个参数用来表示他要接收的param String中的key
String value();
}

然后,在测试类中创建一个controllers包,里面创建一个SalaryController类,对上面的注解进行使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.mooc.wms.controllers;

import com.mooc.wms.web.mvc.Controller;
import com.mooc.wms.web.mvc.RequestMapping;
import com.mooc.wms.web.mvc.RequestParam;

/**
* @author Bennett_Wang on 2020/5/3
*/
@Controller
public class SalaryController {
@RequestMapping("/get_salary.json")
public Integer getSalary(@RequestParam("name") String name,
@RequestParam("experience") String experience) {
return 10000;
}
}

但是如果此时将程序运行起来,访问localhost:6699/get_salary.json,还是只会显示”test”。也就是说,这些Controller并没有生效。

为什么?因为现在框架并不知道我们添加了哪些controller。想要让框架知道我们定义了哪些controller,这时候就需要使用Java的类加载器了。

类加载器,即ClassLoader。ClassLoader的作用

  • 通过类全限定名获取类的二进制字节流(全限定名是包括了包路径的类名)
  • 解析二进制字节流,获取Class类实例——实际上,所有的.java文件在被编译成.class二进制文件后,都已经成为了二进制的字节码。得到类的二进制字节码后,类加载器通过虚拟机来解析这些二进制字节码,把它初始化成类,然后就可以获取到这个类的class实例了。
  • 加载classpath下的静态资源——这个特性是我们获取类全限定名的关键,虽然我们可以使用类加载器来获取需要的类,但我们不知道项目下所有的类的全限定名。想要获取到这些全限定名,还需要从类文件入手,因为我们不可能自己定义一个类名列表,每次有新的类文件添加都往列表中添加。

需要指出,我们能够通过Java的类文件来获取到类的全限定名,这要归功于Java类文件规范。

  • 统一的Resource抽象——H5页面、类等等,都是资源
  • 每个Java类文件和类名对应——类名末尾加上.class后缀,就是文件名,这个规范也很巧妙
  • 类包名和文件夹路径对应

手写类扫描获取类定义

在core包中添加类扫描器——ClassScanner

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
58
59
60
61
62
63
64
65
package com.mooc.wms.core;

import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
* @author Bennett_Wang on 2020/5/3
* 类扫描器
*/
public class ClassScanner {
public static List<Class<?>> scanClasses(String packageName) throws IOException, ClassNotFoundException {
//存储扫描包的类
List<Class<?>> classList = new ArrayList<>();
//把包名转换为文件路径,即把包名中的"."换成"/"就可以了
String path = packageName.replace(".","/");
//使用类加载器,通过路径加载文件
//获取默认类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//调用getResource()方法,其返回的是可遍历的URL资源
Enumeration<URL> resources = classLoader.getResources(path);
while(resources.hasMoreElements()) {
//获取资源
URL resource = resources.nextElement();
//获取资源后,要判断资源的类型,可以用protocol属性获取到
//因为这个项目最后会打包成jar包,所以我们设置资源类型为jar包,当资源类型为jar包的时候
//进行处理
if(resource.getProtocol().contains("jar")) {
//如果是jar包,则尝试获取jar包路径,jar包路径可以通过jarURLConnection获取
JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
//可以用这个jarURLConnection获取到jar包的路径名
String jarFilePath = jarURLConnection.getJarFile().getName();
classList.addAll(getClassesFromJar(jarFilePath,path));

} else {
// todo
}
}
return classList;
}
/*
* 此方法通过jar包的路径来获取到jar包所有的类
* String jarFilePath为jar包的路径
* String path——一个jar包可能有多个类文件,可以用path指定哪些类文件是我们需要的,path为类的相对路径
*/
private static List<Class<?>> getClassesFromJar(String jarFilePath,String path) throws IOException, ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
JarFile jarFile = new JarFile(jarFilePath);
Enumeration<JarEntry> jarEntries = jarFile.entries();
while(jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String entryName = jarEntry.getName();// com/mooc/wms/test/Test.class
if(entryName.startsWith(path) && entryName.endsWith(".class")) {
String classFullName = entryName.replace("/",".").substring(0,entryName.length() - 6);
classes.add(Class.forName(classFullName));
}
}
return classes;
}
}

试用一下这个类扫描器,在框架的入口类MiniApplication中尝试获取所有的class

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MiniApplication {
public static void run(Class<?> cls, String[] args) {
System.out.println("Hello mini-spring!");
TomcatServer tomcatServer = new TomcatServer(args);
try {
tomcatServer.startServer();
List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName());
classList.forEach(it -> System.out.println(it.getName()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行时可以发现控制台会输出各个以我们包路径开头的类。

控制器的初始化

上一节获取了所有controller类,但是这些类并不能真正响应我们的服务,我们还需要把Controller中定义的Mapping Handler提取出来才能使用。提取的方式可以使用反射。

反射:Java中的高级特性,框架实现的关键,经常用于框架中的动态设计。

反射的特点:

  • 活跃于运行时(Runtime)——普通程序在编译前就已经确定了代码该使用哪个属性,调用哪个方法等等。但是反射在运行时可以动态地获取类的属性和方法实例。
  • 获取属性和方法实例——上面已经提到,反射可以在运行时动态地获取属性和方法实例
  • 动态实例化类——不用new关键字了。可以用反射保存Controller里面的Mapping Handler了,有请求到来时再次调用

具体实现:先定义Mapping Handler的数据结构,在框架的web包中再添加一个handler子包。在子包下定义一个MappingHandler,每个MappingHandler都是一个请求映射器。在它的属性里保存它要处理的URI、对应的Controller方法等等。

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
package com.mooc.wms.web.handler;

import java.lang.reflect.Method;

/**
* @author Bennett_Wang on 2020/5/3
*/
public class MappingHandler {
//要处理的URI
private String uri;
//对应的controller方法
private Method method;
//对应的controller类
private Class<?> controller;
//调用方法需要的参数
private String[] args;

MappingHandler(String uri, Method method,Class<?> cls,String[] args) {
this.uri = uri;
this.method = method;
this.controller = cls;
this.args = args;
}

}

接下来,添加一些管理器来管理这些handler。取名为HandlerManager

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
package com.mooc.wms.web.handler;

import com.mooc.wms.web.mvc.Controller;
import com.mooc.wms.web.mvc.RequestMapping;
import com.mooc.wms.web.mvc.RequestParam;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;

/**
* @author Bennett_Wang on 2020/5/3
*/
public class HandlerManager {
//静态属性,用于保存多个mappingHandler
public static List<MappingHandler> mappingHandlerList = new ArrayList<>();

//一个方法,把controller中的类挑选出来,然后初始化mappingHandler方法初始化成mappingHandler
public static void resolveMappingHandler(List<Class<?>> classList) {
for(Class<?> cls : classList) {
if(cls.isAnnotationPresent(Controller.class)) {
//如果Controller注解存在,我们解析这个controller类
parseHandlerFromController(cls);
}
}
}

//完善子方法
private static void parseHandlerFromController(Class<?> cls) {
//先用反射获取到这个类的所有方法
Method[] methods = cls.getDeclaredMethods();
//遍历所有方法,找到对应的requestMapping注解的方法
for(Method method : methods) {
if(!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
//找到这个方法后,从这个方法的属性中获取所有构成MappingHandler的属性
//首先是uri,可以直接从注解的属性中获取到
String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
//之后的所需要的参数,我们初始化一个容器来暂时存储一下
List<String> paramNameList = new ArrayList<>();
//遍历方法所有的参数,依次判断并找到被requestParam注解的参数
for(Parameter parameter : method.getParameters()) {
if(parameter.isAnnotationPresent(RequestParam.class)) {
paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
}
}
String[] params = paramNameList.toArray(new String[paramNameList.size()]);
//方法和类都是已知的,下面来构造MappingHandler
MappingHandler mappingHandler = new MappingHandler(uri,method,cls,params);
//最后把构造好的handler放到管理器的静态属性里
HandlerManager.mappingHandlerList.add(mappingHandler);
}
}
}

接下来,在DispatcherHandler类里面使用这个handler。在service()方法中进行定义,当一个请求发送过来之后,我们依次判断这些handler能不能处理这些请求,如果能处理,就响应结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
for(MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {
try {
if(mappingHandler.handle(req,res)) {
return ;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}

完善一下MappingHandler中的方法,添加handle方法。

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
public class MappingHandler {
//要处理的URI
private String uri;
//对应的controller方法
private Method method;
//对应的controller类
private Class<?> controller;
//调用方法需要的参数
private String[] args;

public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
String requestUri = ((HttpServletRequest) req).getRequestURI();
//判断handler中的uri和请求的uri是否相等
if(!uri.equals(requestUri)) {
return false;
}
//如果uri相等,那么我们需要调用handler里的method方法
//首先我们准备一下参数
Object[] parameters = new Object[args.length];
//通过参数名依次从ServletRequest里面获取到这个参数
for (int i = 0; i < args.length; i++) {
parameters[i] = req.getParameter(args[i]);
}
//实例化controller,将异常抛出去上层
Object ctl = controller.newInstance();
//由于controller可能会返回多种类型,我们用object来存储结果
Object response = method.invoke(ctl,parameters);
//把方法返回的结果放入到ServletResponse里面去
res.getWriter().println(response.toString());
//处理成功后返回true
return true;

}

MappingHandler(String uri, Method method,Class<?> cls,String[] args) {
this.uri = uri;
this.method = method;
this.controller = cls;
this.args = args;
}

}

最后别忘了在入口的MiniApplication中调用HandlerManager来初始化所有的MappingHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MiniApplication {
public static void run(Class<?> cls, String[] args) {
System.out.println("Hello mini-spring!");
TomcatServer tomcatServer = new TomcatServer(args);
try {
tomcatServer.startServer();
List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName());

//在框架入口类中调用HandlerManager来初始化所有的MappingHandler
HandlerManager.resolveMappingHandler(classList);

classList.forEach(it -> System.out.println(it.getName()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

控制器实现章节的重点

当servlet太多时,使用web服务器配置来维护servlet是很困难的,由此提出了DispatcherServlet。

  • DispatcherServlet有很重要的意义
  • 通过类加载机制实现类的扫描,并且从中挑选出了controller
  • 通过反射实现mappingHandler,真正实现了Spring中的DispatcherServlet

Bean管理(IOC&DI)

Bean的管理,当然是Spring框架能够运行的基石。

bean管理

之前已经实现了对controller的解析,可以使用框架来添加controller来搭建web服务了。但仅有一个controller是不够的,要实现MVC,还需要将项目复杂的逻辑抽离到Module里。在Spring中一般用Service来表示。而当service较多的时候,每个请求重复创建会有较大的性能损耗,所以Spring抽象出了Bean这么一个概念

什么是bean?

bean本质也是一种对象,但是比较特殊:

  • 生命周期较长——从JVM启动,服务的初始化开始,在服务结束,JVM初始化结束的时候结束。
  • 可见性较大——普通对象只是在创建的代码块中可见,但bean在整个虚拟机中都是可见的
  • 维护成本高,默认是单例模式

bean的优势

  • 运行期效率高——使用bean之前不需要再初始化,而且不需要每次都是用service对象生成,可以让代码更干净
  • 统一维护,便于管理和扩展——让一些比如添加类代理等的操作变得更加简单
  • 单例模式可以让每次使用bean的时候不需要做各种setProperty,也不用处理各种链式依赖

普通类创建方式和bean的对比

传统方式创建对象.png

如图所示,A和B类都含有一个Z属性,当我们实例化A和B的时候,会自动实例化Z;情况更复杂一些的话,有一个类C,里面有属性B,那么实例化C的时候也会实例化B,并且初始化了属性Z。

当我们实例化完A、B、C三个对象之后,整个虚拟机中就有了三个Z对象,造成极大的性能浪费。

Spring的实现方式

  • 包扫描并自动装配(反射),不使用显示的”new”来创建对象
  • bean在虚拟机中会通过beanFactory统一管理维护,beanFactory是接口,可以通过类型、名字获取到bean
  • 依赖注入

控制反转/依赖注入

依赖注入和控制反转是有区别的。

  • IOC(Inversion Of Control):一种思想,用来降低代码之间的耦合度
  • DI(Dependency Injection):实现IOC的方式,类A中定义对象B,初始化A的时候B也会被定义好

用控制反转创建对象如下图所示:

控制反转创建Bean.png

每创建一个对象,就把该对象放入到BeanFactory中,下次再需要这个对象的时候可以直接去BeanFactory中去找。这样每个对象都只有一个,节约了大量内存空间。

实现依赖注入的方式

具体步骤为:

  1. 扫描包,获得类定义(已完成)
  2. 初始化Bean,并实现依赖注入
  3. 解决Bean初始化顺序问题

流程示意图:

依赖注入实现顺序.png

具体代码实现

创建两个Annotation注解类:@Bean和@Autowired

@Bean:

1
2
3
4
5
6
7
8
9
package com.mooc.wms.beans;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Bean {
}

@Autowired:

1
2
3
4
5
6
7
8
9
package com.mooc.wms.beans;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}

创建一个BeanFactory用来实现Bean工厂

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
58
59
60
61
62
package com.mooc.wms.beans;

import com.mooc.wms.web.mvc.Controller;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* @author Bennett_Wang on 2020/5/4
*/
public class BeanFactory {
//映射在后期可能需要并发处理,故用ConcurrentHashMap
private static Map<Class<?>, Object> classToBean = new ConcurrentHashMap<>();

//定义一个方法用来从映射里获取Bean
public static Object getBean(Class<?> cls) {
return classToBean.get(cls);
}

//完善Bean初始化的方法
public static void initBean(List<Class<?>> classList) throws Exception {
List<Class<?>> toCreate = new ArrayList<>(classList);
while(toCreate.size() != 0) {
int remainSize = toCreate.size();
for (int i = 0; i < toCreate.size(); i++) {
if(finishCreate(toCreate.get(i))) {
toCreate.remove(i);
}
}
if(toCreate.size() == remainSize) {
throw new Exception("cycle dependency!");
}
}
}

private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
//首先判断其是否需要初始化为Bean。如果其不需要被初始化为Bean,
//则直接返回true,然后把它从初始化列表中删除
if(!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)) {
return true;
}

Object bean = cls.newInstance();

for(Field field : cls.getDeclaredFields()) {
if(field.isAnnotationPresent(Autowired.class)) {
Class<?> fieldType = field.getType();
Object reliantBean = BeanFactory.getBean(fieldType);
if(reliantBean == null) {
return false;
}
field.setAccessible(true);
field.set(bean,reliantBean);
}
}
classToBean.put(cls,bean);
return true;
}
}

然后在启动类MiniApplication入口中初始化Bean工厂,初始化Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MiniApplication {
public static void run(Class<?> cls, String[] args) {
System.out.println("Hello mini-spring!");
TomcatServer tomcatServer = new TomcatServer(args);
try {
tomcatServer.startServer();
List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName());

//使用Bean工厂初始化Bean
BeanFactory.initBean(classList);

//在框架入口类中调用HandlerManager来初始化所有的MappingHandler
HandlerManager.resolveMappingHandler(classList);

classList.forEach(it -> System.out.println(it.getName()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

然后在MappingHandler中使用Bean工厂来利用controller来初始化Bean

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
58
59
public class BeanFactory {
//映射在后期可能需要并发处理,故用ConcurrentHashMap
private static Map<Class<?>, Object> classToBean = new ConcurrentHashMap<>();

//定义一个方法用来从映射里获取Bean
public static Object getBean(Class<?> cls) {
return classToBean.get(cls);
}

//完善Bean初始化的方法
public static void initBean(List<Class<?>> classList) throws Exception {
List<Class<?>> toCreate = new ArrayList<>(classList);
//写一个循环来初始化Bean
while(toCreate.size() != 0) { //当容器中还有类定义时,我们不断遍历,看类定义能否初始化为Bean
//每初始化一个Bean,就把它从容器中删掉
int remainSize = toCreate.size();
for (int i = 0; i < toCreate.size(); i++) {
//如果完成了创建,就把它从容器中删除
if(finishCreate(toCreate.get(i))) {
toCreate.remove(i);
}
}
//如果每次遍历之后容器大小没有变化,那么就是陷入了死循环,我们要抛出异常
if(toCreate.size() == remainSize) {
throw new Exception("cycle dependency!");
}
}
}

//创建完成则返回true
private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
//首先判断其是否需要初始化为Bean。如果其不需要被初始化为Bean,
//则直接返回true,然后把它从初始化列表中删除
if(!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)) {
return true;
}

//bean的初始化
Object bean = cls.newInstance();
//遍历属性,看它有没有需要解决的依赖
for(Field field : cls.getDeclaredFields()) {
//如果这个属性被Autowired注解了,表示它需要使用依赖注入来解决这个依赖
if(field.isAnnotationPresent(Autowired.class)) {
//此时需要从Bean工厂中获取被依赖的Bean
//先拿到属性的类型
Class<?> fieldType = field.getType();
//通过类型从Bean工厂内获取Bean
Object reliantBean = BeanFactory.getBean(fieldType);
if(reliantBean == null) {
return false;
}
field.setAccessible(true);
field.set(bean,reliantBean);
}
}
classToBean.put(cls,bean);
return true;
}
}

至此,框架部分就完成了。

测试模块

测试部分是很重要的。

在测试模块中,我们再添加一个service包,在这个包中添加SalaryService类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.mooc.wms.service;

import com.mooc.wms.beans.Bean;

/**
* @author Bennett_Wang on 2020/5/4
*/
@Bean
public class SalaryService {
public Integer calSalary(Integer experience) {
return experience * 5000;
}
}

接下来在controller中使用这个service

使用流程:首先在SalaryController中声明SalaryService的依赖,也就是让两者有组合的关系,添加Autowired注解。

然后再SalaryController中使用SalaryService的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.mooc.wms.controllers;

import com.mooc.wms.beans.Autowired;
import com.mooc.wms.service.SalaryService;
import com.mooc.wms.web.mvc.Controller;
import com.mooc.wms.web.mvc.RequestMapping;
import com.mooc.wms.web.mvc.RequestParam;

/**
* @author Bennett_Wang on 2020/5/3
*/
@Controller
public class SalaryController {

@Autowired
private SalaryService salaryService;

@RequestMapping("/get_salary.json")
public Integer getSalary(@RequestParam("name") String name,
@RequestParam("experience") String experience) {
return salaryService.calSalary(Integer.parseInt(experience));
}
}

之后gradle build项目,再访问http://localhost:6699/get_salary.json?experience=3&name=listl,可以得到结果。

小结

  • Bean的介绍
  • 依赖注入和控制反转
  • 解决依赖顺序的简单策略

总结

  1. 搭建了框架,主要使用gradle进行,和Spring各个包的功能等
  2. 嵌入集成服务器Servlet
  3. 使用类加载器进行资源扫描,获取项目下的类定义,以及controller的解析以及请求映射器的初始化
  4. Bean管理,Bean的使用和控制反转,是Spring框架的核心