手动实现SpringMVC底层机制
- 🐟准备工作
- 实现任务阶段一
-
- 🍍开发ZzwDispatcherServlet
-
- [🥦说明: 编写ZzwDispatcherServlet充当原生的DispatcherServlet(即核心控制器)](#🥦说明: 编写ZzwDispatcherServlet充当原生的DispatcherServlet(即核心控制器))
- 🥦分析+代码实现
- [🥦配置Tomcat, 完成测试](#🥦配置Tomcat, 完成测试)
- 实现任务阶段二
-
- 🍍完成客户端/浏览器可以请求控制层
-
- 🥦1.创建自己的Controller和自定义注解
- 🥦2.配置zzwspringmvc.xml
- [🥦3.编写XMLParser工具类, 可以解析zzwspringmvc.xml](#🥦3.编写XMLParser工具类, 可以解析zzwspringmvc.xml)
- [🥦4.开发 ZzwWebApplicationContext, 充当Spring容器-得到扫描类的全路径列表.](#🥦4.开发 ZzwWebApplicationContext, 充当Spring容器-得到扫描类的全路径列表.)
- [🥦5.完善ZzwWebApplicationContext, 充当Spring容器-实例化对象到容器中](#🥦5.完善ZzwWebApplicationContext, 充当Spring容器-实例化对象到容器中)
- 🥦6.完成请求URL和控制器方法的映射关系
- [🥦7.完成ZzwDispatcherServlet 分发请求到对应控制器方法](#🥦7.完成ZzwDispatcherServlet 分发请求到对应控制器方法)
- 实现任务阶段三
- 实现任务阶段四
⬅️ 上一篇 : SpringMVC系列六: 视图和视图解析器
🎉 欢迎来到 SpringMVC系列七: 手动实现SpringMVC底层机制-上 🎉
在本篇文章中,我们将深入探讨如何手动实现SpringMVC的底层机制。通过理解这些机制,可以更好地掌握SpringMVC的工作原理。
🔧 本篇需要用到的项目 : zzw-springmvc项目
博客的技术栈分析 🛠️
主要技术
- 🌐 前端框架 : 无(此博客主要关注于 SpringMVC 后端实现,因此未涉及具体前端框架)
- 🔧 后端框架 : SpringMVC
- SpringMVC 是 Spring Framework 的一个模块,用于构建基于 MVC (Model-View-Controller) 架构的 web 应用程序。
- 博客中详细讲解了如何手动实现 SpringMVC 的核心机制,包括前端控制器、请求处理流程等。
- 📦 依赖管理 : Maven
- Maven 是一个项目管理和构建工具,用于管理项目依赖和构建流程。
- 通过配置
pom.xml
文件来管理项目的依赖项,如 Servlet API、Junit 等。
- 📋 注解处理 : 自定义注解
- 博客中使用了自定义注解(如
@Controller
和@RequestMapping
)来标识控制器类和方法,并通过反射实现注解处理。
- 博客中使用了自定义注解(如
辅助工具
- 📄 XML 配置 : Dom4j
- Dom4j 是一个用于处理 XML 的开源 Java 库。博客中使用 Dom4j 解析
zzwspringmvc.xml
配置文件,以获取需要扫描的包路径。
- Dom4j 是一个用于处理 XML 的开源 Java 库。博客中使用 Dom4j 解析
- 🔍 反射 API : Java 反射
- Java 反射 API 被广泛用于动态获取类信息和调用方法。博客通过反射机制来扫描包、实例化类和调用控制器方法。
- 🗂️ 集合框架 : ConcurrentHashMap
- 使用
ConcurrentHashMap
来存储 IoC 容器中的 bean 实例,确保线程安全。
- 使用
功能模块
- 核心控制器 :
ZzwDispatcherServlet
- 继承
HttpServlet
类,通过覆盖doGet
和doPost
方法实现核心控制器功能,处理所有请求并将其分发到对应的控制器方法。
- 继承
- 自定义 IoC 容器 :
ZzwWebApplicationContext
- 模拟 Spring 的 IoC 容器,扫描指定包路径下的类,并将带有注解的类实例化并存储到容器中。
- 请求映射处理 :
ZzwHandler
- 维护 URL 与控制器方法的映射关系,并在请求到达时根据 URL 查找并调用对应的控制器方法。
具体实现细节
-
核心控制器 (
ZzwDispatcherServlet
)- 通过在
web.xml
中配置,将所有请求映射到ZzwDispatcherServlet
,实现统一的请求分发。
- 通过在
-
IoC 容器 (
ZzwWebApplicationContext
)- 扫描指定包路径下的类,判断是否包含特定注解(如
@Controller
,@Service
),并实例化这些类,存储到ConcurrentHashMap
中。
- 扫描指定包路径下的类,判断是否包含特定注解(如
-
请求映射 (
ZzwHandler
)- 使用自定义注解
@RequestMapping
指定控制器方法的 URL 映射,在请求到达时,通过 URL 找到对应的控制器方法并调用。
- 使用自定义注解
-
反射机制
- 通过反射获取类的元数据和注解信息,动态调用方法。
-
XML 解析
- 使用 Dom4j 解析 Spring 配置文件
zzwspringmvc.xml
,获取需要扫描的包路径,实现配置的灵活性。
- 使用 Dom4j 解析 Spring 配置文件
总结
本博客深入剖析了 SpringMVC 的底层实现机制,通过手动实现类似 SpringMVC 的功能,展示了 Java 反射、注解处理、XML 解析等技术的应用。通过这种方式,读者能够更好地理解 SpringMVC 的工作原理,提升自身的编程能力和框架理解能力。
🐟准备工作
🍍搭建SpringMVC底层机制开发环境
1.创建zzw-springmvc项目, 这是一个maven-web项目
出现了点小插曲. 项目建成后, 没有src目录, 且右下角报错
Cannot find JRE '1.7
做如下修改
改成1.8
缺少的文件夹需自己手动创建
pom.xml配置
xml
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--引入原生servlet依赖的jar包-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!--解读
1.scope标签表示引入的jar包的作用范围
2.provided:表示该项目在打包, 放到生产环境时, 不需要带上servlet-api.jar包
3.因为tomcat本身是有servlet的jar包, 到时直接使用tomcat本身的servlet-api.jar包, 防止版本冲突
4.到后面会再次学习maven.
-->
<scope>provided</scope>
</dependency>
<!--引入dom4j-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!--引入常用工具类的jar包-该jar包含有很多常用的类-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
</dependencies>
实现任务阶段一
🍍开发ZzwDispatcherServlet
🥦说明: 编写ZzwDispatcherServlet充当原生的DispatcherServlet(即核心控制器)
🥦分析+代码实现
1.com.zzw.zzwspringmvc.servlet
包下新建ZzwDispatcherServlet.java
java
/**
* 解读
* 1.ZzwDispatcherServlet 充当原生的DispatcherServlet
* 2.本质是一个Servlet, 继承HttpServlet
*/
public class ZzwDispatcherServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ZzwDispatcherServlet doPost()...");
}
}
2.src/main/resources
(类路径)下新建 zzwspringmvc.xml
, spring的容器配置文件
xml
<!--先空着-->
对应的类路径
3.webapp/WEB-INF
配置web.xml
load-on-startup讲解
xml
<!--配置ZzwDispatcherServlet, 作为我们自己的前端控制器-->
<servlet>
<servlet-name>ZzwDispatcherServlet</servlet-name>
<servlet-class>com.zzw.zzwspringmvc.servlet.ZzwDispatcherServlet</servlet-class>
<!--给ZzwDispatcherServlet配置参数, 指定要操作的spring容器配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:zzwspringmvc.xml</param-value>
</init-param>
<!--ZzwDispatcherServlet在tomcat启动时, 就会自动加载. 调用init方法-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ZzwDispatcherServlet</servlet-name>
<!--因为ZzwDispatcherServlet作为前端控制器, 所以需要拦截所有请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
🥦配置Tomcat, 完成测试
1.配置tomcat
2.测试, 随便请求一个网址
实现任务阶段二
🍍完成客户端/浏览器可以请求控制层
🥦1.创建自己的Controller和自定义注解
示意图[分析说明]
1.在com.zzw.controller
下新建MonsterController
java
public class MonsterController {
//编写方法, 可以列出怪物列表
//springmvc 是支持原生的servlet api, 为了看到底层机制
//这里我们涉及两个参数
public void listMonster(HttpServletRequest request, HttpServletResponse response) {
//设置返回编码和返回类型
response.setContentType("text/html;charset=utf-8");
//获取writer返回信息
try {
response.getWriter().write("<h1>妖怪名信息: 孙悟空--猪八戒--沙僧</h1>");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
2.在com.zzw.zzwspringmvc.annotation
下新建注解类@Controller
RetentionPolicy.RUNTIME: 编译器把注解记录在class文件中, 当运行Java程序时, JVM 会保留注解. 程序可以通过反射获取该注解
java
/**
* @author 赵志伟
* @version 1.0
* 该注解用于标识一个控制器组件
* 这里涉及到注解知识, 在java基础
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
3.在该包下新建注解类RequestMapping
java
/**
* @author 赵志伟
* @version 1.0
* RequestMapping 注解用于指定控制器-方法的映射路径
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";;
}
4.在MonsterController
中添加注解
java
@Controller
public class MonsterController {
//编写方法, 可以列出怪物列表
//springmvc 是支持原生的servlet api, 为了看到底层机制
//这里我们涉及两个参数
@RequestMapping(value = "/monster/list")
public void listMonster(HttpServletRequest request, HttpServletResponse response) {
//设置返回编码和返回类型
response.setContentType("text/html;charset=utf-8");
//获取writer返回信息
try {
response.getWriter().write("<h1>妖怪名信息: 孙悟空--猪八戒--沙僧</h1>");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
🥦2.配置zzwspringmvc.xml
xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<!--指定要扫描的基本包以及子包的java类-->
<component-scan base-package="com.zzw.controller"/>
</beans>
🥦3.编写XMLParser工具类, 可以解析zzwspringmvc.xml
1.在com.zzw.zzwspringmvc.xml
编写XMLParser
工具类, 可以解析zzwspringmvc.xml
, 得到要扫描的包
java
/**
* @author 赵志伟
* @version 1.0
* XMLParser 用于解析spring配置文件
*/
@SuppressWarnings({"all"})
public class XMLParser {
public static String getBasePackage(String xmlFile) {
//1.得到解析器
SAXReader reader = new SAXReader();
//2.得到类的加载路径 => 获取到spring配置文件[对应的资源流]
InputStream inputStream =
XMLParser.class.getClassLoader().getResourceAsStream(xmlFile);
try {
//3.得到xml文件的文档
Document document = reader.read(inputStream);
//4.获取rootElement
Element rootElement = document.getRootElement();
//5.获取component-scan节点
Element componentScanElement =
(Element) rootElement.elements("component-scan").get(0);
//6.获取component-scan节点的base-package属性值
String basePackage = componentScanElement.attributeValue("base-package");
//7.返回
return basePackage;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
2.在com.zzw.test
新建ZzwSpringMVCTest.java
测试类
XMLParser类
在很多包下都有, 别选错
java
public class ZzwSpringMVCTest {
@Test
public void readXML() {
String basePackage = XMLParser.getBasePackage("zzwspringmvc.xml");
System.out.println(basePackage);
}
}
🥦4.开发 ZzwWebApplicationContext, 充当Spring容器-得到扫描类的全路径列表.
把指定的目录包括子目录下的java
类的全路径扫描到集合中, 比如 ArrayList
[java基础]
示意图[分析说明]
1.在com.zzw.zzwspringmvc.context
下新建ZzwWebApplicationContext.java
java
/**
* @author 赵志伟
* @version 1.0
* ZzwWebApplicationContext 表示我们自己的spring容器
*/
@SuppressWarnings({"all"})
public class ZzwWebApplicationContext {
//定义属性classFullPathList, 保存扫描包/子包的类的全路径
private List<String> classFullPathList =
new ArrayList<String>();
//编写方法, 完成自己的spring容器的初始化
public void init() {
String basePackage = XMLParser.getBasePackage("zzwspringmvc.xml");
scanPackage(basePackage);
}
/**
* 创建方法, 完成对包的扫描->涉及 io/容器/字符串处理
* @param pack 表示要扫描的包, 比如 com.zzw.controller
*/
public void scanPackage(String pack) {
//通过类的加载器, 得到指定的包所在的工作路径对应的绝对路径
//比如 com.zzw.controller => url = file:/D:/idea_project/zzw_springmvczzw-springmvc/target/classes/com/zzw/controller
ClassLoader classLoader = this.getClass().getClassLoader();
URL url = classLoader.getResource(pack.replace(".", "/"));
//细节说明:
// 1.不要直接使用Junit测试, 否则 url返回null
// 2.启动Tomcat测试, 才能得到这个类路径
System.out.println("url=" + url);
}
}
2.前端控制器ZzwDispatcherServlet
增加init
方法
java
/**
* 解读
* 1.ZzwDispatcherServlet 充当原生的DispatcherServlet
* 2.本质是一个Servlet, 继承HttpServlet
*/
public class ZzwDispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
ZzwWebApplicationContext zzwWebApplicationContext =
new ZzwWebApplicationContext();
zzwWebApplicationContext.init();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ZzwDispatcherServlet doPost()...");
}
}
3.启动Tomcat, 进行测试
4.开发自己的spring容器
java
/**
* @author 赵志伟
* @version 1.0
* ZzwWebApplicationContext 表示我们自己的spring容器
*/
@SuppressWarnings({"all"})
public class ZzwWebApplicationContext {
//定义属性classFullPathList, 保存扫描包/子包的类的全路径
private List<String> classFullPathList =
new ArrayList<String>();
//编写方法, 完成自己的spring容器的初始化
public void init() {
String basePackage = XMLParser.getBasePackage("zzwspringmvc.xml");
scanPackage(basePackage);
System.out.println("classFullPathList=" + classFullPathList);
}
/**
* 创建方法, 完成对包的扫描->涉及 io/容器/字符串处理
*
* @param pack 表示要扫描的包, 比如 com.zzw.controller
*/
public void scanPackage(String pack) {
//通过类的加载器, 得到指定的包所在的工作路径对应的绝对路径
//比如 com.zzw.controller => url = file:/D:/idea_project/zzw_springmvc/zzw-springmvc/target/zzw-springmvc/WEB-INF/classes/com/zzw/controller/
ClassLoader classLoader = this.getClass().getClassLoader();
URL url = classLoader.getResource(pack.replace(".", "/"));
//细节说明:
// 1.不要直接使用Junit测试, 否则 url返回null
// 2.启动Tomcat测试, 才能得到这个类路径
System.out.println("url=" + url);
//根据得到的路径, 对其进行扫描, 把类的全路径保存到classFullPathList
String path = url.getFile();
File dir = new File(path);//在io中, 目录也是文件
//遍历dir[文件/子目录]
for (File f : dir.listFiles()) {
if (f.isDirectory()) {//如果是一个目录, 需要递归扫描
scanPackage(pack + "." + f.getName());//f.getName() 子包的名称
} else {
//说明: 这时, 你扫描到的文件, 可能是.class文件, 也可以是其它文件
// 就算是.class文件, 也存在是不是需要注入到容器中的问题
// 目前先把所有.class文件的全路径都保存到集合中, 后面在注入对象到容器时, 再处理
// 这里只考虑 .class文件
String classFullPath = pack + "." + f.getName().replaceAll(".class", "");
classFullPathList.add(classFullPath);
}
}
}
}
5.在com.zzw.controller.xx
包下新建GoodsController, OrderController
.
6.重启Tomcat
, 测试
🥦5.完善ZzwWebApplicationContext, 充当Spring容器-实例化对象到容器中
功能说明: 将扫描到的类, 在满足条件的情况下(即有相应的注解@Controller @Service...
时), 反射到ioc容器.
1.ZzwWebApplicationContext
增加ioc
属性. 增加executeInstance
方法
java
/**
* @author 赵志伟
* @version 1.0
* ZzwWebApplicationContext 表示我们自己的spring容器
*/
@SuppressWarnings({"all"})
public class ZzwWebApplicationContext {
//定义属性ioc, 存放反射生成的bean对象
public ConcurrentHashMap<String, Object> ioc =
new ConcurrentHashMap<String, Object>();
//编写方法, 完成自己的spring容器的初始化
public void init() {
String basePackage = XMLParser.getBasePackage("zzwspringmvc.xml");
scanPackage(basePackage);
System.out.println("classFullPathList=" + classFullPathList);
//将扫描到的类, 反射到ioc容器
executeInstance();
System.out.println("扫描后的 ioc容器 " + ioc);
}
//编写方法, 将扫描到的类, 在满足条件的情况下, 反射到ioc容器
public void executeInstance() {
//判断是否扫描到类
if (classFullPathList.size() == 0) {//说明没有扫描到类
return;
}
try {
//遍历classFullPathList, 进行反射
for (String classFullPath : classFullPathList) {
Class<?> clazz = Class.forName(classFullPath);
//说明当前这个类有@Controller
if (clazz.isAnnotationPresent(Controller.class)) {
//beanName 假设是默认的, 即类名首字母小写
String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
ioc.put(beanName, clazz.newInstance());
}//如果有其它注解, 可以拓展!!
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
我这里输出的时候乱码, 我的解决方案是. 全改成UTF-8
测试
🥦6.完成请求URL和控制器方法的映射关系
功能说明: 将配置的@RequestMapping
的url
和 对应的 控制器-方法 映射关系保存到集合中
示意图[分析说明]
1.在com.zzw.zzwspringmvc.handler
下新建ZzwHandler
java
/**
* @author 赵志伟
* @version 1.0
* ZzwHandler 对象记录请求的url 和 控制器方法映射关系
*/
@SuppressWarnings({"all"})
public class ZzwHandler {
private String url;
private Object controller;
private Method method;
public ZzwHandler(String url, Object controller, Method method) {
this.url = url;
this.controller = controller;
this.method = method;
}
//getter, setter, toString方法
}
2.修改ZzwDispatcherServlet
- 将
init
方法内声明的zzwWebApplicationContext
属性提到外面, 扩大它的作用域 - 定义属性
handlerList
, 保存ZzwHandler
[url 和 控制器-方法的映射关系] - 编写方法[initHandlerMapping], 完成url 和 控制器-方法的映射 (initHandlerMapping也可以写在HandlerMapping类中, 逻辑是一样的)
java
/**
* 解读
* 1.ZzwDispatcherServlet 充当原生的DispatcherServlet
* 2.本质是一个Servlet, 继承HttpServlet
* 3.提示: 这里我们需要使用到 java web 讲解的Servlet
*/
public class ZzwDispatcherServlet extends HttpServlet {
//定义属性 handlerList, 保存ZzwHandler[url 和 控制器-方法的映射关系]
private List<ZzwHandler> handlerList
= new ArrayList<ZzwHandler>();
//定义属性 zzwWebApplicationContext, 自己的spring容器
ZzwWebApplicationContext zzwWebApplicationContext = null;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
zzwWebApplicationContext = new ZzwWebApplicationContext();
zzwWebApplicationContext.init();
//调用 initHandlerMapping, 完成url和控制器方法的映射
initHandlerMapping();
System.out.println("handlerList初始化的结果=" + handlerList);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ZzwDispatcherServlet doPost()...");
}
//编写方法, 完成url 和 控制器方法的映射
private void initHandlerMapping() {
if (zzwWebApplicationContext.ioc.isEmpty()) {
//判断当前的ioc容器是否为空
return;
}
//遍历ioc容器的bean对象, 然后进行url映射处理
//java基础 map遍历
for (Map.Entry<String, Object> entry : zzwWebApplicationContext.ioc.entrySet()) {
//先取出实例, 转化为clazz对象[要获取类的内部信息, 类的实例对象不好用, 要用类的Class对象 反射知识]
Class<?> clazz = entry.getValue().getClass();
//如果注入的bean是Controller
if (clazz.isAnnotationPresent(Controller.class)) {
//取出它所有的方法
Method[] declaredMethods = clazz.getDeclaredMethods();
//遍历方法
for (Method declaredMethod : declaredMethods) {
//判断该方法是否有@RequestMapping
if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
//取出@RequestMapping值 -> 就是映射路径
RequestMapping requestMappingAnnotation =
declaredMethod.getDeclaredAnnotation(RequestMapping.class);
String url = requestMappingAnnotation.value();
//创建ZzwHandler对象->就是一个映射关系 [保存映射关系]
ZzwHandler zzwHandler =
new ZzwHandler(url, entry.getValue(), declaredMethod);
//放入到 handlerList
handlerList.add(zzwHandler);
}
}
}
}
}
}
🥦7.完成ZzwDispatcherServlet 分发请求到对应控制器方法
功能说明: 完成ZzwDispatcherServlet 分发请求到对应控制器方法
示意图[分析说明]
-当用户发出请求, 根据用户请求url 找到对应的 控制器-方法, 并反射调用
-如果用户请求的路径不存在, 返回404
1.ZzwDispatcherServlet
添加getZzwHandler()
方法和executeDispatcher()
方法, 在doPost
中调用 executeDispatcher()
方法
java
public class ZzwDispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
//创建自己的spring容器
zzwWebApplicationContext = new ZzwWebApplicationContext();
zzwWebApplicationContext.init();
//调用 initHandlerMapping, 完成url和控制器方法的映射
initHandlerMapping();
System.out.println("handlerList初始化的结果=" + handlerList);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//System.out.println("ZzwDispatcherServlet doPost()...");
//调用方法, 完成请求转发
executeDispatcher(req, resp);
}
//编写方法, 通过request对象, 返回ZzwHandler对象
//如果没有, 就返回null
private ZzwHandler getZzwHandler(HttpServletRequest request) {
//1.先获取到用户请求的url 比如http://localhost:8080/zzw_springmvc/monster/list
// uri = /zzw_springmvc/monster/list
//2.这里要注意得到的uri 和 保存的url 有一个工程路径的问题
//两个方案解决 =>第一个方案: 简单 tomcat 直接配置 application context => /
// 第二个方案: 保存 zzwHandler对象的url时, 拼接 this.getServletContext().getContextPath()
String requestURI = request.getRequestURI();
//遍历 handlerList
for (ZzwHandler zzwHandler : handlerList) {
if (requestURI.equals(zzwHandler.getUrl())) {//说明匹配成功
return zzwHandler;
}
}
return null;
}
//编写方法, 完成分发请求任务
private void executeDispatcher(HttpServletRequest request,
HttpServletResponse response) {
try {
ZzwHandler zzwHandler = getZzwHandler(request);
if (zzwHandler == null) {//说明用户请求的路径/资源不存在
response.getWriter().print("<h1>404 NOT FOUND!</h1>");
} else {//匹配成功, 反射调用控制器的方法
zzwHandler.getMethod().
invoke(zzwHandler.getController(), request, response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
2.OrderController
增加两个方法listOrder(), addOrder()
别忘了加Controller注解
java
@Controller
public class OrderController {
@RequestMapping(value = "/order/list")
public void listOrder(HttpServletRequest request, HttpServletResponse response) {
//设置返回编码和返回类型
response.setContentType("text/html;charset=utf8");
//获取writer返回信息
try {
response.getWriter().write("<h1>订单列表信息</h1>");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@RequestMapping(value = "/order/add")
public void addOrder(HttpServletRequest request, HttpServletResponse response) {
//设置返回编码和返回类型
response.setContentType("text/html;charset=utf8");
//获取writer返回信息
try {
response.getWriter().write("<h1>添加订单信息</h1>");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
3.GoodsController
增加一个方法listGoods()
java
@Controller
public class GoodsController {
@RequestMapping(value = "/goods/list")
public void listGoods(HttpServletRequest request, HttpServletResponse response) {
//设置返回编码和返回类型
response.setContentType("text/html;charset=utf8");
//获取writer返回信息
try {
response.getWriter().write("<h1>商品列表信息...</h1>");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
4.测试(注意: 不要再加工程路径了)
handlerList初始化的结果=
[ZzwHandler{url='/goods/list', controller=com.zzw.controller.xx.GoodsController@79b1752f, method=public void com.zzw.controller.xx.GoodsController.listGoods(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)},
ZzwHandler{url='/order/add', controller=com.zzw.controller.xx.OrderController@1b82cb63, method=public void com.zzw.controller.xx.OrderController.addOrder(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)},
ZzwHandler{url='/order/list', controller=com.zzw.controller.xx.OrderController@1b82cb63, method=public void com.zzw.controller.xx.OrderController.listOrder(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)},
ZzwHandler{url='/monster/list', controller=com.zzw.controller.MonsterController@32128628, method=public void com.zzw.controller.MonsterController.listMonster(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)}]
实现任务阶段三
🍍从web.xml动态获取zzwspringmvc.xml
说明: 前面我们加载zzwspringmvc.xml是硬编码, 现在做活. 从web.xml动态获取
示意图[分析说明]
1.ZzwDispatcherServlet
在创建并初始化ZzwWebApplicationContext
时, 动态地从web.xml
中获取到spring
配置文件.
servletConfig使用
java
/**
* 解读
* 1.ZzwDispatcherServlet 充当原生的DispatcherServlet
* 2.本质是一个Servlet, 继承HttpServlet
* 3.提示: 这里我们需要使用到 java web 讲解的Servlet
*/
public class ZzwDispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
//获取到web.xml中的
/*
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:zzwspringmvc.xml</param-value>
</init-param>
*/
String configLocation = servletConfig.getInitParameter("contextConfigLocation");
//创建自己的spring容器
zzwWebApplicationContext = new ZzwWebApplicationContext(configLocation);
zzwWebApplicationContext.init();
//调用 initHandlerMapping, 完成url和控制器方法的映射
initHandlerMapping();
System.out.println("handlerList初始化的结果=" + handlerList);
}
.......
}
2.ZzwWebApplicationContext.java
中添加一个属性configLocation, 和一个无参构造器, 一个有参构造器, 并修改init()方法
java
/**
* @author 赵志伟
* @version 1.0
* ZzwWebApplicationContext 表示我们自己的spring容器
*/
public class ZzwWebApplicationContext {
//定义属性classFullPath, 保存扫描包/子包的类的全路径
private List<String> classFullPathList
= new ArrayList<String>();
//定义属性ioc, 存放反射生成的bean对象 有Controller/Service注解
public ConcurrentHashMap<String, Object> ioc
= new ConcurrentHashMap<String, Object>();
//创建一个属性, 表示spring容器配置文件
private String configLocation;
//添加一个无参构造器
public ZzwWebApplicationContext() {
}
//构建一个有参构造器
public ZzwWebApplicationContext(String configLocation) {
this.configLocation = configLocation;
}
//编写方法, 完成自己的spring容器的初始化
public void init() {
//这里我们写的是固定的spring容器配置文件 => 做活
//String basePackage = XMLParser.getBasePackage("zzwspringmvc.xml");
String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]);
scanPackage(basePackage);
System.out.println("basePackage=" + basePackage);
System.out.println("classFullPathList=" + classFullPathList);
//将扫描到的类, 反射到ioc容器
executeInstance();
System.out.println("扫描后的 ioc容器=" + ioc);
}
........
}
3.测试...
实现任务阶段四
🍍完成自定义@Service注解功能
说明: 如果给某个类加上@Service
, 则可以将其注入到我们的Spring容器
示意图[分析说明]
补充: DAO和DB由MyBatis接管, 和SpringMVC关系并不大. 所以我们暂时不考虑DAO和DB.
1.在com.zzw.entity
包下新建Monster
java
public class Monster {
private Integer id;
private String name;
private String skill;
private Integer age;
//全参构造器, getter, setter, toString方法
}
2.在com.zzw.zzwspringmvc.annotation
下新建@Service
. 这个注解是springmvc框架要支持的东西, 所以要在zzwspringmvc包下
java
/**
* @author 赵志伟
* @version 1.0
* Service 注解, 用于标识一个Service对象, 并注入到spring容器
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
String value() default "";
}
3.在com.zzw.service
下新建MonsterService接口
.
java
public interface MonsterService {
//增加方法-返回monster列表
public List<Monster> listMonster();
}
3.1在com.zzw.service.impl
新建MonsterServiceImpl实现类
. 并标注@Service
, 表示可以将对象注入到Spring
容器
java
/**
* @author 赵志伟
* @version 1.0
* MonsterServiceImpl 作为一个Service注入到spring容器
*/
@SuppressWarnings({"all"})
public class MonsterServiceImpl implements MonsterService {
//这里我们模拟数据->DB
public List<Monster> listMonster() {
List<Monster> monsters = new ArrayList<Monster>();
monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
monsters.add(new Monster(200, "汤姆猫", "抓老鼠", 200));
return monsters;
}
}
3.2完善zzwspringmvc.xml
, 加上com.zzw.service
xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<!--指定要扫描的基本包以及子包的java类-->
<component-scan base-package="com.zzw.controller,com.zzw.service"/>
</beans>
3.3更改ZzwWebApplicationContext.java
的init()
java
//编写方法, 完成自己的spring容器的初始化
public void init() {
//这里我们写的是固定的spring容器配置文件 => 做活
//String basePackage = XMLParser.getBasePackage("zzwspringmvc.xml");
String basePackage =
XMLParser.getBasePackage(configLocation.split(":")[1]);
//这时我们的basePackage => com.zzw.controller,com.zzw.service
//scanPackage(basePackage);
String[] basePackages = basePackage.split(",");
if (basePackages.length > 0) {
for (String pack : basePackages) {
scanPackage(pack);
}
}
........
}
4.ZzwWebApplicationContext
的executeInstance
增加一个else if分支. 并可以通过接口支持多级-类名来获取到Service Bean
java
//编写方法, 将扫描到的类, 在满足条件的情况下, 反射到ioc容器
public void executeInstance() {
//判断是否扫描到类
if (classFullPathList.size() == 0) {//说明没有扫描到类
return;
}
try {
//遍历classFullPathList, 进行反射
for (String classFullPath : classFullPathList) {
Class<?> clazz = Class.forName(classFullPath);
//说明当前这个类有@Controller注解
if (clazz.isAnnotationPresent(Controller.class)) {
//beanName 假设是默认的, 即类名首字母小写
String beanName = clazz.getSimpleName().substring(0, 1)
.toLowerCase() + clazz.getSimpleName().substring(1);
ioc.put(beanName, clazz.newInstance());
}//如果有其它注解, 可以拓展!! 处理@Service
else if (clazz.isAnnotationPresent(Service.class)) {//如果类有@Service
//先获取到@Service的value值 => 就是注入时的beanName
Service serviceAnnotation = clazz.getDeclaredAnnotation(Service.class);
String beanName = serviceAnnotation.value();
if ("".equals(beanName)) {//说明没有指定value, 我们就使用默认的机制注入Service
//可以通过 接口名/类名[首字母小写] 来注入ioc容器
//1.得到所有接口的名称=>接口
Class<?>[] interfaces = clazz.getInterfaces();
Object instance = clazz.newInstance();
//2.遍历接口, 然后通过多个接口名来注入
for (Class<?> anInterface : interfaces) {
//接口名->首字母小写
String beanName2 = anInterface.getSimpleName().substring(0, 1).toLowerCase()
+ anInterface.getSimpleName().substring(1);
ioc.put(beanName2, instance);
}
//3.这里老师给留了个作业: 使用类名的首字母小写来注入bean
// 通过 clazz 来获取即可.
String beanName2 = clazz.getSimpleName().substring(0, 1).toLowerCase()
+ clazz.getSimpleName().substring(1);
ioc.put(beanName2, instance);
} else {//如果有指定名称, 就使用该名称注入即可
ioc.put(beanName, clazz.newInstance());
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
5.测试-重启tomcat
扫描后的 ioc容器={goodsController=com.zzw.controller.xx.GoodsController@5fb9a20e, monsterService=com.zzw.service.impl.MonsterServiceImpl@3b03f989, monsterServiceImpl=com.zzw.service.impl.MonsterServiceImpl@3b03f989, orderController=com.zzw.controller.xx.OrderController@2f51e8b1, monsterController=com.zzw.controller.MonsterController@7a223f3b}
🔜 下一篇预告 🔜
敬请期待:SpringMVC系列八: 手动实现SpringMVC底层机制-下
📚 目录导航 📚
- SpringMVC系列一: 初识SpringMVC
- SpringMVC系列二: 请求方式介绍
- SpringMVC系列三: Postman(接口测试工具)
- SpringMVC系列四: Rest-优雅的url请求风格
- SpringMVC系列五: SpringMVC映射请求数据
- SpringMVC系列六: 视图和视图解析器
- SpringMVC系列七: 手动实现SpringMVC底层机制-上
- SpringMVC系列八: 手动实现SpringMVC底层机制-下
...
💬 读者互动 💬
在学习SpringMVC底层机制的过程中,你有哪些疑问或需要帮助的地方?欢迎在评论区留言,我们一起讨论。