通过单一的Servlet拦截请求分派任务

这里我们的目标是通过一个 Controller 来拦截用户请求,找到相应的处理类进行逻辑处理,然后将处理的结果发送给客户端。

通过单一的Servlet拦截请求并分派任务,通常被称为前端控制器模式,也就是在一个Servlet中集中处理所有的请求,并根据请求的内容将任务分发给相应的处理逻辑。

  1. 创建前端控制器Servlet: 首先,你需要创建一个Servlet,该Servlet将作为前端控制器,拦截所有的请求并进行任务分派。在下面的配置文件中,这个Servlet的类是 com.minis.web.DispatcherServlet

  2. 配置Servlet映射: 在下面配置文件中,有 <servlet><servlet-mapping> 部分。<servlet> 部分定义了前端控制器Servlet的配置,而 <servlet-mapping> 部分将请求路径映射到该前端控制器Servlet。

    在下面的配置文件中,前端控制器Servlet的名称是 “minisMVC”,它会拦截根路径 “/“ 的请求。这意味着所有的请求都会被这个Servlet处理。

  3. 任务分派: 在前端控制器Servlet中,你需要根据请求的内容,将任务分派给不同的处理逻辑。这通常涉及到根据请求的URL或其他参数来确定要执行的操作。这里通过 Servlet 拦截所有请求,处理映射关系,调用业务逻辑代码,处理返回值回递给浏览器。

  4. 初始化参数和配置: 在下面的配置文件中,使用了 <init-param> 来为前端控制器Servlet提供初始化参数。这些参数可以在Servlet中读取,用于配置或传递信息。

配置文件

首先是minisMVC-servlet.xml

一开始的minisMVC-servlet.xml ,是一个Bean 配置,只是把 id 设置成了一个 URL 的形式,来匹配后端的程序,访问 /helloworld 的时候,对应调用 HelloWorldBean 类里的 doGet() 方法。属性有 id、class 与 value :

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<bean id="/helloworld" class="com.minis.test.HelloWorldBean" value="doGet"/>
</beans>

后来的minisMVC-servlet.xml可以简化成如下样式,即在 minisMVC-servlet.xml 里新增 <components><component-scan 两个标签,分别表示组件配置以及组件的扫描配置。也就是说,扫描一个包,自动配置包内满足条件的类,省去手工配置过程。

下述文件将扫描 com.minis.test 里所有的类文件,加载并实例化它们(需要配合引入的 @RequestMapping使用)。

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<components>
<component-scan base-package="com.minis.test"/>
</components>

然后是web.xml ,下面的配置文件的意思就是当 Servlet 容器启动的时候,先读取 web.xml 配置,加载配置文件中的 servlet,也就是 DispatcherServlet,并规定它拦截所有的 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
26
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:web="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID">
<!-- 这个元素用于配置一个Servlet。 -->
<servlet>
<!-- 定义了Servlet的名称,这里命名为 "minisMVC" -->
<servlet-name>minisMVC</servlet-name>
<!-- 指定servlet的核心类,也就是web程序的核心代码 -->
<servlet-class>com.minis.web.DispatcherServlet</servlet-class>
<!-- 这个元素用于配置Servlet的初始化参数。-->
<init-param>
<!-- 定义初始化参数的名称,这里是 "contextConfigLocation"。-->
<param-name>contextConfigLocation</param-name>
<!-- 指定初始化参数的值,这里是 "/WEB-INF/minisMVC-servlet.xml",表示配置文件的路径。-->
<param-value> /WEB-INF/minisMVC-servlet.xml </param-value>
</init-param>
<!-- 指定Servlet在Web应用程序启动时加载的顺序,这里是 "1",表示在启动时加载。-->
<load-on-startup>1</load-on-startup>
</servlet>
<!-- 这个元素用于配置Servlet的映射。-->
<servlet-mapping>
<!-- 指定要映射的Servlet的名称,这里是 "minisMVC"。-->
<servlet-name>minisMVC</servlet-name>
<!-- 指定URL模式,当请求的URL匹配这个模式时,该Servlet将被调用。这里是 "/",表示根路径。-->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

我们注意到这个控制器 DispatcherServlet 有一个参数 contextConfigLocation,它配置了控制器要找的逻辑处理类的文件

1
2
3
4
5
6
7
<!--  这个元素用于配置Servlet的初始化参数。-->
<init-param>
<!-- 定义初始化参数的名称,这里是 "contextConfigLocation"。-->
<param-name>contextConfigLocation</param-name>
<!-- 指定初始化参数的值,这里是 "/WEB-INF/minisMVC-servlet.xml",表示配置文件的路径。-->
<param-value> /WEB-INF/minisMVC-servlet.xml </param-value>
</init-param>

因此,为了启动这个 servlet,我们要提前解析 minisMVC-servlet.xml 文件。

即类似于在解析JavaBean时的ClassPathXmlResource,这里也定义一个ClassPathXmlResource,使用SaxReader依次解析xml文件,读取 minisMVC-servlet.xml中的属性 id、class 与 value,并存入定义实体类 MappingValue 里的三个属性:uri、clz 与 method

引入@RequestMapping

引入@RequestMapping是为了手工地将映射关系写到 XML 配置文件里,省去我们的手工配置工作。

即使用如下简化的minisMVC-servlet.xml扫描一个包,自动配置包内满足条件的类,省去手工配置过程。

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<components>
<component-scan base-package="com.minis.test"/>
</components>

@RequestMapping的实现目的是将 URL 和业务处理类中的某个方法对应起来, 在 Spring 框架里, @RequestMapping 注解可支持定义在类上,但我们这里暂时不支持该注解定义在类上,只定义在方法上。

我们看一下注解定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.minis.web;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

@Target(value={ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
//现在只有 value 一个字段,用来接收配置的 URL。
String value() default "";
}

我们提供一个新类 XmlScanComponentHelper,专门用来解析简化的minisMVC-servlet.xml的标签结构。

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
package com.minis.web;

import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

/**
* 用于处理minisMVC-servlet.xml结构
*
*/
public class XmlScanComponentHelper {
public static List<String> getNodeValue(URL xmlPath) {
//把扫描到的 package 存储在packages 这个结构里。
List<String> packages = new ArrayList<>();
SAXReader saxReader=new SAXReader();
Document document = null;
try {
//加载配置文件
document = saxReader.read(xmlPath);
} catch (DocumentException e) {
e.printStackTrace();
}
Element root = document.getRootElement();
Iterator it = root.elementIterator();

while (it.hasNext()) {
//得到XML中所有的base-package节点
Element element = (Element) it.next();
packages.add(element.attributeValue("base-package"));
}

return packages;
}

}

实现DispatcherSevlet

前面引入的配置文件和 @RequestMapping注解已经完成了配置文件的加载和自动扫描,接下来就可以实现MVC 的核心启动类DispatcherSevlet 。

在 Spring Framework 中, DispatcherServlet 是一个关键类,用于处理Web应用程序中的请求分发和处理。它可以看作是一个前端控制器,根据请求的内容将任务分发给不同的处理器,并协调整个请求-响应周期中的各个环节。

即MVC 的基本思路是屏蔽 Servlet 的概念,让程序员主要写业务逻辑代码。

浏览器访问的 URL 通过映射机制找到实际的业务逻辑方法。这里通过 Servlet 拦截所有请求,处理映射关系,调用业务逻辑代码,处理返回值回递给浏览器。程序员写的业务逻辑程序,也叫做 Bean。

这里的主要步骤是 :

  1. Servlet 初始化方法 . 主要作用是处理从外部传入的资源,将 XML 文件内容解析后,相应的包存入 packageNames 内。然后用 Refresh() 函数创建 Bean

  2. Refresh() 函数: 第一步初始化 controller方法 initController() ,, 第二步则是初始化 URL 映射 initMapping()

  3. initController() : 主要功能是对扫描到的每一个类进行加载和实例化,让 类与类名 , 类与对象 建立映射关系,分别存在 controllerClassescontrollerObjs 这两个 map 里,类名就是 key 的值。

  4. initMapping(): 主要功能是根据上一步中得到的类与类名,类与对象 的映射关系 ,

    先通过类与类名的映射关系,遍历类的方法, 初始化URL 映射 . 过程是先找到使用了注解 @RequestMapping 的方法, 再将其URL 存放到 urlMappingNames 里,url与方法的映射关系存放到 mappingMethods 里。

    再通过类与对象 的映射关系 得到实例化对象,将url与对象的映射关系放到 mappingObjs中.

  5. doGet() 方法. 由Servlet容器(如Tomcat)自动触发的方法,用于处理HTTP GET请求。当客户端发送一个HTTP GET请求到与该Servlet映射的URL时,Servlet容器会调用**doGet()**方法来处理这个请求。

    主要是通过 mappingMethodsmappingObjs 获取与请求路径对应的方法对象 ,依赖反射机制进行调用。

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
package com.minis.web;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.minis.test.HelloWorldBean;

/**
* Servlet implementation class DispatcherServlet
* DispatcherServlet 类是前端控制器,用于处理请求分发和处理。
*/
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private String sContextConfigLocation;
//用于储存需要扫描的package列表
private List<String> packageNames = new ArrayList<>();
//用于储存controller的名称与对象的映射关系
private Map<String,Object> controllerObjs = new HashMap<>();
//用于储存controller的名称列表
private List<String> controllerNames = new ArrayList<>();
//用于储存controller的名称与类的映射关系
private Map<String,Class<?>> controllerClasses = new HashMap<>();
//用于保存自定义的@RequestMapping名称(即url名称)的列表
private List<String> urlMappingNames = new ArrayList<>();
//用于保存url与对象的映射关系
private Map<String,Object> mappingObjs = new HashMap<>();
//用于保存url与方法的映射关系
private Map<String,Method> mappingMethods = new HashMap<>();

public DispatcherServlet() {
super();
}

/**
* Servlet 初始化方法
* 初始化主要
* 1.处理从外部传入的资源,将 XML 文件内容解析后,相应的包存入 packageNames 内。
* 2.调用 Refresh() 函数创建 Bean
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// 获取配置文件(web.xml)中的上下文配置位置参数,即minisMVC-servlet.xml
sContextConfigLocation = config.getInitParameter("contextConfigLocation");

URL xmlPath = null;
try {
// 获取上下文资源的路径
xmlPath = this.getServletContext().getResource(sContextConfigLocation);
} catch (MalformedURLException e) {
e.printStackTrace();
}
//把 minisMVC-servlet.xml 里扫描出来的 package 名称存入packageNames 列表
this.packageNames = XmlScanComponentHelper.getNodeValue(xmlPath);
//调用 Refresh() 函数创建 Bean
Refresh();

}

/**
* refresh() 方法分成两步:
* 第一步初始化 controller
* 第二步则是初始化 URL 映射。
*/
protected void Refresh() {
initController();
initMapping();
}

/**
* initController() ,其主要功能是对扫描到的每一个类进行加载和实例化,
* 与类的名字建立映射关系,分别存在 controllerClasses 和 controllerObjs 这两个 map 里,类名就是 key 的值。
*/
protected void initController() {
//扫描包,获取所有类名
this.controllerNames = scanPackages(this.packageNames);

for (String controllerName : this.controllerNames) {
Object obj = null;
Class<?> clz = null;

try {
//加载类
clz = Class.forName(controllerName);
//建立controller的名称与类的映射关系
this.controllerClasses.put(controllerName,clz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
obj = clz.newInstance();
//实例化bean,建立controller的名称与类的映射关系
this.controllerObjs.put(controllerName, obj);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

/**
* 给定包的列表,遍历所有包,获取其中所有的类
* @param packages
* @return
*/
private List<String> scanPackages(List<String> packages) {
List<String> tempControllerNames = new ArrayList<>();
for (String packageName : packages) {
tempControllerNames.addAll(scanPackage(packageName));
}
return tempControllerNames;
}

/**
* 给定包名称,获取其中所有的类
* @param packageName
* @return
*/
private List<String> scanPackage(String packageName) {
//暂时储存控制器名称
List<String> tempControllerNames = new ArrayList<>();
//将以.分隔的包名换成以/分隔的uri
URL url = this.getClass().getClassLoader().getResource("/"+packageName.replaceAll("\\.", "/"));
File dir = new File(url.getFile());
//处理对应的文件目录
for (File file : dir.listFiles()) { //目录下的文件或者子目录
//对子目录递归扫描
if(file.isDirectory()){
scanPackage(packageName + "." + file.getName());
}else{
//控制器名称
String controllerName = packageName + "." + file.getName().replace(".class", "");
tempControllerNames.add(controllerName);
}
}
return tempControllerNames;
}

/**
* initMapping() ,功能是初始化URL 映射
* 找到使用了注解 @RequestMapping 的方法:
* 1.URL 存放到 urlMappingNames 里
* 2.映射的对象存放到 mappingObjs 里
* 3.映射的方法存放到 mappingMethods 里。
*/
protected void initMapping() {
//controllerName为包中的一个个的类
for (String controllerName : this.controllerNames) {
Class<?> clazz = this.controllerClasses.get(controllerName);
Object obj = this.controllerObjs.get(controllerName);
Method[] methods = clazz.getDeclaredMethods();
if(methods!=null){
//检查所有的方法
for(Method method : methods){
//有RequestMapping注解
boolean isRequestMapping = method.isAnnotationPresent(RequestMapping.class);
if (isRequestMapping){
String methodName = method.getName();
String urlmapping = method.getAnnotation(RequestMapping.class).value();
this.urlMappingNames.add(urlmapping);
//用于保存url与对象的映射关系
this.mappingObjs.put(urlmapping, obj);
//用于保存url与方法的映射关系
this.mappingMethods.put(urlmapping, method);
}
}
}
}
}

/**
* doGet() 由Servlet容器(如Tomcat)自动触发的方法,用于处理HTTP GET请求。
* 当客户端发送一个HTTP GET请求到与该Servlet映射的URL时,Servlet容器会调用doGet()方法来处理这个请求。
* 通过 Bean 的 id 获取其对应的类和方法,依赖反射机制进行调用。
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取请求的Servlet路径
String sPath = request.getServletPath();
System.out.println("in doGet(),sPath :" + sPath);
if (!this.urlMappingNames.contains(sPath)) {
return;
}

Object obj = null;
Object objResult = null;
try {
// 获取与请求路径对应的方法和对象
Method method = this.mappingMethods.get(sPath);
obj = this.mappingObjs.get(sPath);
// 调用方法并获取结果
objResult = method.invoke(obj);
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 将方法执行结果写入响应
response.getWriter().append(objResult.toString());
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}

测试文件

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
package com.minis.test;

import com.minis.beans.BeansException;
import com.minis.context.ClassPathXmlApplicationContext;
import com.minis.web.RequestMapping;


public class HelloWorldBean {
@RequestMapping("/test1")
public String doTest1() {
return "test 1, hello world!";
}
@RequestMapping("/test2")
public String doTest2() {
return "test 2, hello world!";
}

@RequestMapping("/test3")
public String doTest3() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("annotationTest.xml");
AService aService;
try {
aService = (AService)ctx.getBean("aservice");
aService.sayHello();
} catch (BeansException e) {
e.printStackTrace();
}
return "aService.sayHello();";
}
}

启动Tomcat, 在浏览器输入框内键入:localhost:8080/test,即可看到结果.