SpringMVC系列七: 手动实现SpringMVC底层机制-上
手动实现SpringMVC底层机制
- 博客的技术栈分析 🛠️
- 具体实现细节
- 总结
- 🐟准备工作
- 🍍搭建SpringMVC底层机制开发环境
- 实现任务阶段一
- 🍍开发ZzwDispatcherServlet
- 🥦说明: 编写ZzwDispatcherServlet充当原生的DispatcherServlet(即核心控制器)
- 🥦分析+代码实现
- 🥦配置Tomcat, 完成测试
- 实现任务阶段二
- 🍍完成客户端/浏览器可以请求控制层
- 🥦1.创建自己的Controller和自定义注解
- 🥦2.配置zzwspringmvc.xml
- 🥦3.编写XMLParser工具类, 可以解析zzwspringmvc.xml
- 🥦4.开发 ZzwWebApplicationContext, 充当Spring容器-得到扫描类的全路径列表.
- 🥦5.完善ZzwWebApplicationContext, 充当Spring容器-实例化对象到容器中
- 🥦6.完成请求URL和控制器方法的映射关系
- 🥦7.完成ZzwDispatcherServlet 分发请求到对应控制器方法
- 实现任务阶段三
- 🍍从web.xml动态获取zzwspringmvc.xml
- 实现任务阶段四
- 🍍完成自定义@Service注解功能
⬅️ 上一篇: 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底层机制开发环境
前提: 搭建maven环境
1.创建zzw-springmvc项目, 这是一个maven-web项目

出现了点小插曲. 项目建成后, 没有src目录, 且右下角报错
Cannot find JRE '1.7
做如下修改
改成1.8
缺少的文件夹需自己手动创建

pom.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
/*** 解读* 1.ZzwDispatcherServlet 充当原生的DispatcherServlet* 2.本质是一个Servlet, 继承HttpServlet*/
public class ZzwDispatcherServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("ZzwDispatcherServlet doPost()...");}
}
2.src/main/resources(类路径)下新建 zzwspringmvc.xml, spring的容器配置文件

<!--先空着-->

对应的类路径

3.webapp/WEB-INF配置web.xml
load-on-startup讲解
<!--配置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
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 会保留注解. 程序可以通过反射获取该注解
/*** @author 赵志伟* @version 1.0* 该注解用于标识一个控制器组件* 这里涉及到注解知识, 在java基础*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {String value() default "";
}
3.在该包下新建注解类RequestMapping
/*** @author 赵志伟* @version 1.0* RequestMapping 注解用于指定控制器-方法的映射路径*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {String value() default "";;
}
4.在MonsterController中添加注解
@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 version="1.0" encoding="UTF-8" ?>
<beans><!--指定要扫描的基本包以及子包的java类--> <component-scan base-package="com.zzw.controller"/>
</beans>
🥦3.编写XMLParser工具类, 可以解析zzwspringmvc.xml
Dom4j解析配置文件代码实现
1.在com.zzw.zzwspringmvc.xml编写XMLParser工具类, 可以解析zzwspringmvc.xml, 得到要扫描的包
/*** @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.获取rootElementElement 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类在很多包下都有, 别选错

public class ZzwSpringMVCTest {@Testpublic 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
/*** @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/controllerClassLoader classLoader = this.getClass().getClassLoader();URL url = classLoader.getResource(pack.replace(".", "/"));//细节说明:// 1.不要直接使用Junit测试, 否则 url返回null// 2.启动Tomcat测试, 才能得到这个类路径System.out.println("url=" + url);}
}
2.前端控制器ZzwDispatcherServlet增加init方法
/*** 解读* 1.ZzwDispatcherServlet 充当原生的DispatcherServlet* 2.本质是一个Servlet, 继承HttpServlet*/
public class ZzwDispatcherServlet extends HttpServlet {@Overridepublic void init(ServletConfig config) throws ServletException {super.init(config);ZzwWebApplicationContext zzwWebApplicationContext =new ZzwWebApplicationContext();zzwWebApplicationContext.init();}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("ZzwDispatcherServlet doPost()...");}
}
3.启动Tomcat, 进行测试

4.开发自己的spring容器
/*** @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);//根据得到的路径, 对其进行扫描, 把类的全路径保存到classFullPathListString 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方法
/*** @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);//说明当前这个类有@Controllerif (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
/*** @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类中, 逻辑是一样的)
/*** 解读* 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;@Overridepublic void init(ServletConfig config) throws ServletException {super.init(config);zzwWebApplicationContext = new ZzwWebApplicationContext();zzwWebApplicationContext.init();//调用 initHandlerMapping, 完成url和控制器方法的映射initHandlerMapping();System.out.println("handlerList初始化的结果=" + handlerList);}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected 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是Controllerif (clazz.isAnnotationPresent(Controller.class)) {//取出它所有的方法Method[] declaredMethods = clazz.getDeclaredMethods();//遍历方法for (Method declaredMethod : declaredMethods) {//判断该方法是否有@RequestMappingif (declaredMethod.isAnnotationPresent(RequestMapping.class)) {//取出@RequestMapping值 -> 就是映射路径RequestMapping requestMappingAnnotation =declaredMethod.getDeclaredAnnotation(RequestMapping.class);String url = requestMappingAnnotation.value();//创建ZzwHandler对象->就是一个映射关系 [保存映射关系]ZzwHandler zzwHandler =new ZzwHandler(url, entry.getValue(), declaredMethod);//放入到 handlerListhandlerList.add(zzwHandler);}}}}}
}
🥦7.完成ZzwDispatcherServlet 分发请求到对应控制器方法
功能说明: 完成ZzwDispatcherServlet 分发请求到对应控制器方法
示意图[分析说明]

-当用户发出请求, 根据用户请求url 找到对应的 控制器-方法, 并反射调用
-如果用户请求的路径不存在, 返回404
1.ZzwDispatcherServlet添加getZzwHandler()方法和executeDispatcher()方法, 在doPost中调用 executeDispatcher()方法
public class ZzwDispatcherServlet extends HttpServlet {@Overridepublic void init(ServletConfig config) throws ServletException {super.init(config);//创建自己的spring容器zzwWebApplicationContext = new ZzwWebApplicationContext();zzwWebApplicationContext.init();//调用 initHandlerMapping, 完成url和控制器方法的映射initHandlerMapping();System.out.println("handlerList初始化的结果=" + handlerList);}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//System.out.println("ZzwDispatcherServlet doPost()...");//调用方法, 完成请求转发executeDispatcher(req, resp);}//编写方法, 通过request对象, 返回ZzwHandler对象//如果没有, 就返回nullprivate 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();//遍历 handlerListfor (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注解
@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()
@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使用
/*** 解读* 1.ZzwDispatcherServlet 充当原生的DispatcherServlet* 2.本质是一个Servlet, 继承HttpServlet* 3.提示: 这里我们需要使用到 java web 讲解的Servlet*/
public class ZzwDispatcherServlet extends HttpServlet {@Overridepublic 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()方法
/*** @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
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包下
/*** @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接口.
public interface MonsterService {//增加方法-返回monster列表public List<Monster> listMonster();
}
3.1在com.zzw.service.impl新建MonsterServiceImpl实现类. 并标注@Service, 表示可以将对象注入到Spring容器
/*** @author 赵志伟* @version 1.0* MonsterServiceImpl 作为一个Service注入到spring容器*/
@SuppressWarnings({"all"})
public class MonsterServiceImpl implements MonsterService {//这里我们模拟数据->DBpublic 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 version="1.0" encoding="UTF-8" ?>
<beans><!--指定要扫描的基本包以及子包的java类--> <component-scan base-package="com.zzw.controller,com.zzw.service"/>
</beans>
3.3更改ZzwWebApplicationContext.java的init()
//编写方法, 完成自己的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
//编写方法, 将扫描到的类, 在满足条件的情况下, 反射到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());}//如果有其它注解, 可以拓展!! 处理@Serviceelse if (clazz.isAnnotationPresent(Service.class)) {//如果类有@Service//先获取到@Service的value值 => 就是注入时的beanNameService 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底层机制的过程中,你有哪些疑问或需要帮助的地方?欢迎在评论区留言,我们一起讨论。
相关文章:
SpringMVC系列七: 手动实现SpringMVC底层机制-上
手动实现SpringMVC底层机制 博客的技术栈分析 🛠️具体实现细节总结 🐟准备工作🍍搭建SpringMVC底层机制开发环境 实现任务阶段一🍍开发ZzwDispatcherServlet🥦说明: 编写ZzwDispatcherServlet充当原生的DispatcherSer…...
嵌入式web 服务器boa的编译和移植
编译环境:虚拟机 ubuntu 18.04 目标开发板:飞凌OKA40i-C开发板, Linux3.10 操作系统 开发板本身已经移植了boa服务器,但是在使用过程中发现POST方法传输大文件时对数据量有限制,超过1M字节就无法传输,这是…...
什么是js?特点是什么?组成部分?
Js是一种直译式脚本语言,一种动态类型,弱类型,基于原型的高级语言。 直译式:js程序运行过程中直接编译成机器语言。 脚本语言:在程序运行过程中逐行进行解释说明,不需要预编译。 动态类型:js…...
Java 面试题:如何保证集合是线程安全的? ConcurrentHashMap 如何实现高效地线程安全?
在多线程编程中,保证集合的线程安全是一个常见而又重要的问题。线程安全意味着多个线程可以同时访问集合而不会导致数据不一致或程序崩溃。在 Java 中,确保集合线程安全的方法有多种,包括使用同步包装类、锁机制以及并发集合类。 最简单的方法…...
打工人的PPT救星来了!用这款AI工具,10秒生成您的专属PPT
今天帮同事解决了一个代码合并的问题。其实问题不复杂,要把1的代码合到2的位置: 这个处理方式其实很简单,使用 “git cherry-pick hash值” 就可以。 同事直接对我赞许有加,不曾想被领导看到了,对我说了一句ÿ…...
GIT 合拼
合拼有多种方式: 1)合拼分支: git merge [source-branch] 2)合拼提交 : git cherry-pick [commit-hash] 3)合拼单个文件: git checkout [source-branch] – [file] 以上合拼,比如将分…...
利用 Python 和 AI 技术制作智能问答机器人
利用 Python 和 AI 技术制作智能问答机器人 引言 在人工智能的浪潮下,智能问答机器人成为了一种非常实用的技术。它们能够处理大量的查询,提供即时的反馈,并且可以通过机器学习技术不断优化自身的性能。本文将介绍如何使用 Python 来开发一…...
electron系列(一)调用dll
用electron的目的,其实很简单。就是web架构要直接使用前端电脑的资源,但是浏览器限制了使用,所以用electron来达到这个目的。其中调用dll是一个非常基本的操作。 安装 ffi-napi 和 ref-napi 包: npm install ffi-napi ref-napi main.js&…...
VUE3实现个人网站模板源码
文章目录 1.设计来源1.1 网站首页页面1.2 个人工具页面1.3 个人日志页面1.4 个人相册页面1.5 给我留言页面 2.效果和源码2.1 动态效果2.2 目录结构 源码下载万套模板,程序开发,在线开发,在线沟通 作者:xcLeigh 文章地址࿱…...
C语言 | Leetcode C语言题解之第162题寻找峰值
题目: 题解: int findPeakElement(int* nums, int numsSize) {int ls_max0;for(int i1;i<numsSize;i){if(nums[ls_max]>nums[i]);else{ls_maxi;}}return ls_max; }...
利用pickle保存和加载对象
使用 pickle.dump 保存下来的文件可以使用 pickle.load 打开和读取。以下是一个示例,展示了如何使用 pickle 模块保存和加载对象: 保存对象 import pickle# 假设有一个对象 obj obj {"key": "value"}# 将对象保存到文件 with ope…...
定制汽车霍尔传感器
磁电效应霍尔传感器、饱和霍尔传感器、非线性霍尔传感器 霍尔传感器原理 霍尔传感器的工作原理基于霍尔效应,即当一块通有电流的金属或半导体薄片垂直地放在磁场中时,薄片的两端会产生电位差。这种现象称为霍尔效应,两端具有的电位差值称为…...
【2024最新华为OD-C/D卷试题汇总】[支持在线评测] LYA的巡演(100分) - 三语言AC题解(Python/Java/Cpp)
🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 …...
ChatGPT 简介
ChatGPT 是一种基于大型语言模型的对话系统,由 OpenAI 开发。它的核心是一个深度学习模型,使用了 GPT(Generative Pre-trained Transformer)架构。以下是 ChatGPT 的原理和工作机制的详细介绍: ### GPT 架构 1. **Tr…...
大数据实训室建设可行性报告
一、建设大数据实训室的背景与意义 随着信息技术的飞速发展,大数据已成为推动社会进步和经济发展的重要力量。中高职院校作为技能型人才培养的摇篮,承担着为社会输送大数据领域高素质、高技能人才的重要任务。因此,建设大数据实训室…...
学懂C#编程:让函数返回 多个返回值 的几种常用技术
1. 使用 out 或 ref 参数 out 和 ref 参数允许方法修改传入变量的值,并通过它们“返回”多个值。ref 需要变量事先初始化,而 out 不要求。 public void GetValues(out int val1, out string val2) {val1 10;val2 "Hello"; }// 使用示例 int…...
蔚来汽车AI算法工程师,如何理解注意力?
大家好啊,我是董董灿。 今天分享一个上海蔚来汽车的AI算法岗位面试经验总结帖,面试岗位为算法工程师。 这次面试提到的问题,除了与实习相关内容和反问之外,面试官总共问了8个问题,主要集中在深度学习基础概念的理解上…...
信创适配评测
概叙 信创科普参考:全面国产化之路-信创-CSDN博客 有必要再解释一下两个名词“28N”,“79号文件”,因为“28N”指定了由政府牵头从各领域开启国产化的基调,而“79号文件”则指定了国产化的截止日期2027年。 信创的本质是实现中国信…...
【Qt6.3 基础教程 04】探索Qt项目结构和配置文件
文章目录 前言Qt项目的基本结构配置文件:.pro文件基本构成示例.pro文件: qmake和构建过程步骤简述: 修改项目设置结论 前言 当你开始使用Qt进行开发时,理解项目结构和配置文件的作用是至关重要的。这篇博文将带你深入了解Qt项目的…...
SpringBoot测试实践
测试按照粒度可分为3层: 单元测试:单元测试(Unit Testing)又称为模块测试 ,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中…...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...


