hello, 我是郭屹。从这节课起,我们将进入一个新的部分:MVC(Model-View-Controller)模式的学习。
在前一章节中,我们已经实现了一个简易版本的IoC(Inversion of Control)容器。尽管我们的MiniSpring框架相对原生的Spring框架来说功能较为基础,但它已经包含了IoC的核心特性。基于这个基础,我们将继续扩展我们的框架,接下来要实现的是Spring MVC架构。
Spring MVC简介MVC是一种设计模式,它将应用程序分为三个核心组件:
- 模型(Model)
- 处理数据和业务逻辑。
- 视图(View)
- 显示数据(用户界面)。
- 控制器(Controller)
- 接收用户的输入,并调用模型和视图完成用户的请求。
基本工作流程1. 用户通过浏览器向服务器发送一个请求。2. 控制器拦截到这个请求。3. 控制器根据请求处理相应的业务逻辑,可能涉及与模型交互。4. 模型执行实际的数据操作或业务逻辑。5. 控制器接收模型返回的数据,选择适当的视图进行渲染。6. 渲染后的视图作为响应返回给用户。
实现单一Servlet分派任务为了实现上述的MVC架构,我们可以使用一个前端控制器(Front Controller)模式来统一管理所有的请求。在这个模式中,通常会有一个单独的Servlet作为入口点,它负责拦截所有进来的HTTP请求,然后根据请求的URL或者其他参数决定调用哪个具体的控制器方法。
步骤如下:
- 创建一个继承自
HttpServlet
的类作为前端控制器。
- 在
doGet
或doPost
方法中解析请求,并找到对应的处理器(Controller)。
- 调用处理器的方法,并获取返回的结果。
- 根据结果选择合适的视图模板进行渲染。
- 将渲染后的内容响应给客户端。
随着课程的进展,我们会详细探讨如何实现这些步骤,并最终将MVC架构与之前实现的IoC容器整合起来,以构建一个更为完整的Web应用框架。
这就是我们这一章的整体规划。让我们开始动手实践吧!
Spring MVC 初探
项目目录结构调整
为了实现一个基本的Spring MVC框架,我们首先需要调整项目目录结构,以适应Web项目的需求。以下是调整后的目录结构:
- WebContent:与src目录同级,用于存储前端页面所需的静态资源和XML配置文件。
- 引入Tomcat服务器:
- 需要将Tomcat服务器以及其相关的jar包引入到项目中,以便能够运行Web应用。
实现MVC框架
接下来,我们将通过以下步骤实现一个简单的MVC框架:
1. Controller的创建
创建一个Controller类,用于拦截用户的HTTP请求,并根据请求找到相应的处理类。
2. 业务逻辑处理
在处理类中实现具体的业务逻辑。
3. 响应客户端
将处理结果封装成响应,发送给客户端。
注意事项
1
2
3
4
5
6
7
8
9
10
11
12
13
|
src
└── com
│ ├── minis
│ │ ├── web
│ │ ├── util
│ │ └── test
WebContent
├── WEB-INF
│ ├── lib
│ ├── web.xml
│ ├── minisMVC-servlet.xml
└── META-INF
│ └── MANIFEST.MF
|
参考Spring MVC,我们定义web.xml和minisMVC-servlet.xml这两个配置文件的内容。
- minisMVC-servlet.xml
1
2
3
4
|
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<bean id="/helloworld" class="com.minis.test.HelloWorldBean" value="doGet"/>
</beans>
|
- web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?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-name>minisMVC</servlet-name>
<servlet-class>com.minis.web.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/minisMVC-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>minisMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
|
在MVC架构中,Servlet扮演着核心角色。它运行在Web服务器上,负责处理客户端的请求并返回响应。接下来,我们详细解读一下web.xml文件中的几个关键标签的含义:
-
: 这个标签用于定义一个Servlet。在这个例子中,HelloWorldBean
类被配置为一个Servlet,并且给它分配了一个唯一的ID /helloworld
。
-
: 这个标签用于将特定的URL模式映射到指定的Servlet。在这个例子中,当访问/helloworld
时,会调用ID为/helloworld
的Servlet,即HelloWorldBean
类。
-
: 这个标签指定了URL模式,用于匹配客户端请求的URL。在这个例子中,/helloworld
是匹配的模式,当客户端请求这个URL时,会触发相应的Servlet。
通过这些配置,我们可以实现一个简单的MVC应用,当用户访问/helloworld
时,会调用HelloWorldBean
类的doGet()
方法来处理请求。
JavaEE 架构概述
JavaEE架构是一种标准的企业级应用架构。当Servlet容器启动时,它会首先读取web.xml配置文件,并加载其中定义的servlet。在这个架构中,DispatcherServlet
是核心的servlet,它负责拦截所有的HTTP请求,并作为控制器的角色。
DispatcherServlet 的配置
DispatcherServlet
有一个重要的参数 contextConfigLocation
,这个参数指定了控制器需要查找的逻辑处理类的配置文件路径。在这个例子中,配置文件是 minisMVC-servlet.xml
。
总结
-
Servlet容器启动时读取web.xml配置文件。
-
加载并初始化DispatcherServlet
作为控制器。
-
DispatcherServlet
通过contextConfigLocation
参数查找逻辑处理类的配置文件。
以上是对JavaEE架构中DispatcherServlet
及其配置的简要说明。
1
2
3
4
|
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/minisMVC-servlet.xml</param-value>
</init-param>
|
因此,为了启动这个servlet,我们要提前解析minisMVC-servlet.xml文件。
解析servlet.xml
首先定义实体类MappingValue里的三个属性:uri、clz与method,分别与minisMVC-servlet.xml中标签的属性id、class与value对应。
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.minis.web;
public class MappingValue {
String uri;
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
String clz;
public String getClz() {
return clz;
}
public void setClz(String clz) {
this.clz = clz;
}
String method;
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public MappingValue(String uri, String clz, String method) {
this.uri = uri;
this.clz = clz;
this.method = method;
}
}
|
然后我们定义Resource用来加载配置文件。
1
2
3
4
|
package com.minis.web;
import java.util.Iterator;
public interface Resource extends Iterator<Object>{
}
|
这是具体的实现。
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.minis.web;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
public class ClassPathXmlResource implements Resource {
Document document;
Element rootElement;
Iterator<Element> elementIterator;
public ClassPathXmlResource(URL xmlPath) {
SAXReader saxReader=new SAXReader();
try {
this.document = saxReader.read(xmlPath);
this.rootElement=document.getRootElement();
this.elementIterator=this.rootElement.elementIterator();
} catch (DocumentException e) {
e.printStackTrace();
}
}
@Override
public boolean hasNext() {
return this.elementIterator.hasNext();
}
@Override
public Object next() {
return this.elementIterator.next();
}
}
|
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
|
package com.minis.web;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dom4j.Element;
public class XmlConfigReader {
public XmlConfigReader() {
}
public Map<String,MappingValue> loadConfig(Resource res) {
Map<String,MappingValue> mappings = new HashMap<>();
while (res.hasNext()) { //读所有的节点,解析id, class和value
Element element = (Element)res.next();
String beanID=element.attributeValue("id");
String beanClassName=element.attributeValue("class");
String beanMethod=element.attributeValue("value");
mappings.put(beanID, new MappingValue(beanID,beanClassName,beanMethod));
}
return mappings;
}
}
|
实现MVC的核心启动类DispatcherServlet
在完成项目的基础搭建与前期准备工作后,接下来我们将实现web.xml中配置的com.minis.web.DispatcherServlet
。该类是MiniSpring框架MVC模式的核心组件,负责处理HTTP请求、执行URL到业务逻辑方法的映射,并将结果返回给客户端。
MVC架构的基本概念
-
MVC(Model-View-Controller)是一种设计模式,旨在通过分离应用程序的不同方面来提高开发效率和代码的可维护性。
-
在MVC模式下,我们试图抽象出Servlet的具体细节,使开发者可以更专注于业务逻辑的编写。
-
当用户通过浏览器访问某个URL时,DispatcherServlet
会根据预先定义好的映射规则,找到对应的业务逻辑Bean并调用相应的方法处理请求,最后将处理结果呈现给用户。
DispatcherServlet的功能
-
DispatcherServlet
模仿了Spring MVC的工作方式,它作为一个前端控制器拦截所有进入的请求。
-
内部维护了三个关键的Map对象,分别用于存储:
-
URL路径与MappingValue
对象之间的对应关系;
-
URL路径与对应的Java类;
-
URL路径与具体方法的关联。
-
这些Map帮助DispatcherServlet
快速定位到处理特定请求所需的资源。
总结
通过上述机制,DispatcherServlet
有效地实现了从URL到后台业务逻辑的映射,使得开发者能够更加集中精力于核心业务功能的开发,而不必过多关心底层的请求处理流程。
1
2
3
|
private Map<String, MappingValue> mappingValues;
private Map<String, Class<?>> mappingClz = new HashMap<>();
private Map<String, Object> mappingObjs = new HashMap<>();
|
随后实现Servlet初始化方法,初始化主要处理从外部传入的资源,将XML文件内容解析后存入mappingValues内。最后调用Refresh()函数创建Bean,这节课的例子就是HelloWorldBean,这些Bean的类和实例存放在mappingClz和mappingObjs里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public void init(ServletConfig config) throws ServletException {
super.init(config);
sContextConfigLocation = config.getInitParameter("contextConfigLocation");
URL xmlPath = null;
try {
xmlPath = this.getServletContext().getResource(sContextConfigLocation);
} catch (MalformedURLException e) {
e.printStackTrace();
}
Resource rs = new ClassPathXmlResource(xmlPath);
XmlConfigReader reader = new XmlConfigReader();
mappingValues = reader.loadConfig(rs);
Refresh();
}
|
下面是Refresh()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//对所有的mappingValues中注册的类进行实例化,默认构造函数
protected void Refresh() {
for (Map.Entry<String,MappingValue> entry : mappingValues.entrySet()) {
String id = entry.getKey();
String className = entry.getValue().getClz();
Object obj = null;
Class<?> clz = null;
try {
clz = Class.forName(className);
obj = clz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
mappingClz.put(id, clz);
mappingObjs.put(id, obj);
}
}
|
在Web应用中,Refresh()
方法通过读取mappingValues
中的Bean定义来加载类并创建其实例。此过程完成后,整个DispatcherServlet
便准备就绪,可以处理各种Web请求。
DispatcherServlet的作用
处理流程概述
- 初始化阶段
- 请求处理阶段
- 响应返回
-
将处理结果封装成HTTP响应格式。
-
发送回客户端。
aaaaaaa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String sPath = request.getServletPath(); //获取请求的path
if (this.mappingValues.get(sPath) == null) {
return;
}
Class<?> clz = this.mappingClz.get(sPath); //获取bean类定义
Object obj = this.mappingObjs.get(sPath); //获取bean实例
String methodName = this.mappingValues.get(sPath).getMethod(); //获取调用方法名
Object objResult = null;
try {
Method method = clz.getMethod(methodName);
objResult = method.invoke(obj); //方法调用
} catch (Exception e) {
}
//将方法返回值写入response
response.getWriter().append(objResult.toString());
}
|
到这里,一个最简单的DispatcherServlet就完成了,DispatcherServlet就是一个普通的Servlet,并不神秘,只要我们有一个Servlet容器,比如Tomcat,它就能跑起来。
这个实现很简陋,调用的方法没有参数,返回值只是String,直接通过response回写。
我们试一个简单的测试类。
1
2
3
4
5
6
7
8
9
10
|
package com.minis.test;
public class HelloWorldBean {
public String doGet() {
return "hello world!";
}
public String doPost() {
return "hello world!";
}
}
|
启动Tomcat,在浏览器内键入localhost:8080/helloworld,就能显示返回结果"hello world for doGet!"。到这里,我们初步实现了MVC的框架,支持了一个简单的请求由Controller控制器(DispatcherServlet),到底层查找模型结构Model(helloWorldBean),最后返回前端渲染视图View(response.getWriter().append())的过程。## 扩展MVC在这个简陋的模型基础之上,我们一步步扩展,引入@RequestMapping,还会实现ComponentScan,简化配置工作。### 简化配置首先我们来简化XML中的繁琐配置,在minisMVC-servlet.xml里新增和两个标签,分别表示组件配置以及组件的扫描配置。也就是说,扫描一个包,自动配置包内满足条件的类,省去手工配置过程。你可以参考下面的代码。注意:请根据上面内容,做到下面操作 1.根据内容重新编写内容,把新写的内容放到字段content,内容要有条理性,有结构性。内容使用markdown格式输出。content 中的 换成
1
2
3
4
5
|
(minisMVC-servlet.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<components>
<component-scan base-package="com.minis.test" />
</components>
|
扫描com.minis.test包中的类文件
本文档将介绍如何扫描指定包(com.minis.test)中的所有类文件,并加载实例化它们。
引入@RequestMapping注解
在Spring框架中,@RequestMapping注解用于将URL与业务处理类的方法关联起来,从而避免了手动在XML配置文件中编写映射关系。在本文档中,我们将学习如何仅在方法上使用@RequestMapping注解,而不是在类上。
使用@RequestMapping注解
-
定义:@RequestMapping注解可以定义在方法上,用于指定某个方法处理特定的HTTP请求。
-
不支持类上的注解:根据文档要求,我们暂时不支持将@RequestMapping注解定义在类级别。
-
注解定义:我们将详细查看@RequestMapping注解的定义和使用方法。
注意事项
- 请确保按照文档要求,将’\n’替换为’aaaaaaa’,以保持文档的一致性和可读性。
1
2
3
4
5
6
7
8
9
10
11
12
|
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 {
String value() default "";
}
|
@RequestMapping定义很简单,现在只有value一个字段,用来接收配置的URL。
有了注解定义,我们就可以动手编程实现了。因为修改了minisMVC-servlet.xml这个文件内的标签结构,因此我们提供一个新类 XmlScanComponentHelper,专门用来解析新定义的标签结构。
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
|
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;
public class XmlScanComponentHelper {
public static List<String> getNodeValue(URL xmlPath) {
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;
}
}
|
aaaaaaa 在对系统进行重构的过程中,我们决定简化配置读取机制。原有的 XmlConfigReader
、Resource
、MappingValue
和 ClassPathXmlResource
将被移除,并引入了新的组件 XmlScanComponentHelper
来负责扫描指定包下的资源。这个新组件的核心功能是获取base-package
参数值,并将扫描到的包信息存储在一个名为 packages
的列表结构中。
修改 DispatcherServlet
在上述改动之后,接下来的重点是对 DispatcherServlet
类的调整,因为它是处理所有请求解析和响应的关键所在。为了适应新的架构,我们需要更新 DispatcherServlet
以利用新的数据结构来管理配置。
新的数据结构
packages
:一个列表(List),用来存储通过 XmlScanComponentHelper
扫描得到的所有基础包名(即由base-package
参数指定的那些)。
通过这样的设计,DispatcherServlet
可以更加高效地管理和访问应用程序的配置,从而更好地支持请求处理流程。
这些变动不仅简化了配置文件的处理逻辑,还提高了系统的可维护性和扩展性。
1
2
3
4
5
6
|
private List<String> packageNames = new ArrayList<>();
private Map<String,Object> controllerObjs = new HashMap<>();
private List<String> controllerNames = new ArrayList<>();
private Map<String,Class<?>> controllerClasses = new HashMap<>(); private List<String> urlMappingNames = new ArrayList<>();
private Map<String,Object> mappingObjs = new HashMap<>();
private Map<String,Method> mappingMethods = new HashMap<>();
|
我们看下这些变量的作用。
接下来,Servlet初始化时我们把 minisMVC-servlet.xml 里扫描出来的 package 名称存入 packageNames 列表,初始化方法 init 中增加以下这行代码。
1
|
this.packageNames = XmlScanComponentHelper.getNodeValue(xmlPath);
|
注:原有的与 ClassPathXmlResource 、Resource 相关代码要清除。
我们再将 refresh()方法分成两步:第一步初始化 controller ,第二步则是初始化 URL 映射。
对应的 refresh() 方法进行如下抽象:
1
2
3
4
|
protected void refresh() {
initController(); // 初始化 controller
initMapping(); // 初始化 url 映射
}
|
接下来完善initController() ,其主要功能是对扫描到的每一个类进行加载和实例化,与类的名字建立映射关系,分别存在 controllerClasses 和 controllerObjs 这两个map里,类名就是key的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
protected void initController() {
//扫描包,获取所有类名
this.controllerNames = scanPackages(this.packageNames);
for (String controllerName : this.controllerNames) {
Object obj = null;
Class<?> clz = null;
try {
clz = Class.forName(controllerName); //加载类
this.controllerClasses.put(controllerName, clz);
} catch (Exception e) {
}
try {
obj = clz.newInstance(); //实例化bean
this.controllerObjs.put(controllerName, obj);
} catch (Exception e) {
}
}
|
扫描程序是对文件目录的递归处理,最后的结果就是把所有的类文件扫描出来。
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
|
private List<String> scanPackages(List<String> packages) {
List<String> tempControllerNames = new ArrayList<>();
for (String packageName : packages) {
tempControllerNames.addAll(scanPackage(packageName));
}
return tempControllerNames;
}
private List<String> scanPackage(String packageName) {
List<String> tempControllerNames = new ArrayList<>();
URI uri = null;
//将以.分隔的包名换成以/分隔的uri
try {
uri = this.getClass().getResource("/" +
packageName.replaceAll("\\.", "/")).toURI();
} catch (Exception e) {
}
File dir = new File(uri);
//处理对应的文件目录
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注解的方法,并将URL存放到urlMappingNames里,映射的对象存放到mappingObjs里,映射的方法存放到mappingMethods里。这个方法将取代过去通过解析Bean得到的映射。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
protected void initMapping() {
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) {
//检查所有的方法
boolean isRequestMapping =
method.isAnnotationPresent(RequestMapping.class);
if (isRequestMapping) { //有RequestMapping注解
String methodName = method.getName();
//建立方法名和URL的映射
String urlMapping =
method.getAnnotation(RequestMapping.class).value();
this.urlMappingNames.add(urlMapping);
this.mappingObjs.put(urlMapping, obj);
this.mappingMethods.put(urlMapping, method);
}
}
}
}
}
|
最后略微调整 doGet() 方法内的代码,去除不再使用的结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String sPath = request.getServletPath();
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 (Exception e) {
}
response.getWriter().append(objResult.toString());
}
|
修改一下测试类,在com.minis.test.HelloworldBean内的测试方法上,增加@RequestMapping注解。
1
2
3
4
5
6
7
8
9
10
|
package com.minis.test;
import com.minis.web.RequestMapping;
public class HelloWorldBean {
@RequestMapping("/test")
public String doTest() {
return "hello world for doGet!";
}
}
|
启动Tomcat进行测试,在浏览器输入框内键入:localhost:8080/test。
小结
构建DispatcherServlet
在本节课中,我们构建了一个DispatcherServlet
,它是在Tomcat中注册的唯一Servlet,负责处理所有请求。它解析请求路径与业务类Bean中方法的映射关系,并调用Bean的相应方法,最终将结果返回给response。
映射关系的建立
最初,我们让用户在XML配置文件中手动声明映射关系。随后,我们引入了RequestMapping
注解,通过扫描包中的类并检查注解,自动注册映射关系。这样,我们初步实现了一个原始的MVC框架。
MVC框架的优势
在这个框架下,应用程序员无需关心Servlet的使用细节,他们可以直接创建业务类,并通过添加注解来运行。
完整源代码
相关源代码可以在以下GitHub链接中找到:Minis MVC Branch。
思考题
学完这节课后,思考一下:在MVC中使用的Bean与之前章节中的Bean有何关系?欢迎在留言区讨论或将这节课分享给需要的朋友。我们下节课再见!