自己实现 SrpingMVC 底层机制
搭建环境
P85-86
1. 得到 XML 配置文件中包扫描路径下的的各个包的类的加载路径
1.使用 DOM4J 技术,得到 XML 配置文件中的包的扫描路径
package com.hspedu.xml;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
/**
* ClassName: XMLPaster
* Package: com.hspedu.xml
* Description:
*
* @Author 王文福
* @Create 2024/2/6 5:00
* @Version 1.0
*/
public class XMLPaser {
public static String getbasePackage(String xmlFile) {
try {
SAXReader saxReader = new SAXReader();
InputStream resourceAsStream =
XMLPaser.class.getClassLoader().getResourceAsStream(xmlFile);
System.out.println(resourceAsStream);
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
Element componentScanElement =
rootElement.element("component-scan");
Attribute attribute = componentScanElement.attribute("base-package");
String basePackage = attribute.getText();
return basePackage;
} catch (DocumentException e) {
e.printStackTrace();
}
return "";
}
}
2.
2-1.新建 ArrayList 集合 ,用于保存所有包(XML)下的类的加载路径
2-2.定义 1 个 scanPackage 方法,传入包扫描路径 pack
得到该包路径下的所有 class 文件的完整路径
并将这些路径保存到 ArrayList 集合中
3.
3-1 定义 1 个初始化方法 init,得到配置的 xml 中配置的包扫描路径
并调用(2)方法,得到各个包下的类的加载路径,并保存到集合中
4. 该 init 方法在 Tomcat 启动的时候被调用->init 方法
此时,ArrayList 集合中就保存了 XML 配置文件中的包扫描路径下所有 class 文件的完整路径
5.
5-1 遍历该集合中所有的类加载路径,并通过反射得到该Class对象
5-2 判断该Class对象,是否注解了 @Controller/@Service
5-3 如果注解了,则将通过反射new Instance创建实例,放入到 HashMap 容器中管理
5-4:在Tomcat启动的时候,在init方法中调用executeInstance方法,此时该容器中保存的是注 解的对象
2.实现简单的 @ReuquestMapping
实现效果
访问正确的路径:/springmvc02/list/monster
访问不正确路径:显示404
initHandlerMapping方法
思路分析:
在分发器中实现该方法
1.循环遍历 手写的IOC 容器,得到当前控制器对象 Controller
2.通过该对象进行反射得到Class对象
3.判断该Class对象中是否注解了@Controller
4.如果注解了,则遍历该Class对象的所有Method方法
5.判断当前方法中是否注解了@RequestMapping,
6.条件成立,得到该注解的value值,即URI映射地址
将 URI 地址、当前控制器 controller、方法 Method 当作实参
初始化 HspHandler 对象
7.将uri当作key, HspHandler对象当作value,保存到HashMap容器中
/**
* 初始化Handler映射路径
*
* @param :
* @return void
* @author "卒迹"
* @description TODO
* @date 20:50
*/
private void initHandlerMapping() {
// 1.得到IOC容器
ConcurrentHashMap<String, Object> ioc = hspWebApplicationContext.getIoc();
// 2.判断该ioc容器是否为null
if (ioc.size() == 0) {
return;
}
// 3.循环遍历该IOC容器-得到标记注解的对象实例
Set<Map.Entry<String, Object>> entries = ioc.entrySet();
for (Map.Entry<String, Object> entry : entries) {
// 得到当前对象的class对象
Class<?> clazz = entry.getValue().getClass();
// 如果当前对象,注解了Controller
if (clazz.isAnnotationPresent(Controller.class)) {
// 遍历该对象中所有的Methods方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
// 判断当前方法,是否注解了@RequestMapping
if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
// 得到当前方法的注解
RequestMapping annotation = declaredMethod.getAnnotation(RequestMapping.class);
// 得到该注解的value值(URI映射地址),项目根目录+URI
String uri = annotation.value();
// 将uri、controller对象、method方法保存到HspHandler对象中
try {
HspHandler hspHandler = new HspHandler(uri, entry.getValue(), declaredMethod);
// 将该对象保存到HashMap容器中,key:URI value:hspHandler对象
concurrentHashMap.put(uri, hspHandler);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
}
在Tomcat启动的时候,在init方法中调用该初始化initHandlerMapping方法,初始化Handler容器
getHandler方法
7.定义1个方法,
用于判断浏览器请求的路径是否与Handler对象中的注解的映射@RequestMapping路径匹配
1.得到浏览器的映射URI地址,根据浏览器请求的的URI地址,
从HashMap容器中查找该Handler对象
2.如果不为null,则返回该 Handler 对象,否则返回null
// 外部地址与Handler对象的uri地址匹配,匹配到了则返回该Handler对象
private HspHandler getHandler(HttpServletRequest request, HttpServletResponse response) {
// 1.得到外部访问的路径URI
String requestURI = request.getRequestURI();
// 2.从容器中,得到Handler对象
if (concurrentHashMap.get(requestURI) != null) {
// 直接返回HspHandler对象
return concurrentHashMap.get(requestURI);
}
return null;
}
HspHander对象
package com.hspedu.handler;
import java.lang.reflect.Method;
/**
* ClassName: HspHandler
* Package: com.hspedu.handler
* Description:
*
* @Author 王文福
* @Create 2024/2/7 2:19
* @Version 1.0
*/
public class HspHandler {
private String url;//@RequestMapping映射路径
private Object controller;//控制器@Controller
private Method method;//控制器中的方法
public HspHandler() {
}
public HspHandler(String url, Object controller, Method method) {
this.url = url;
this.controller = controller;
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
@Override
public String toString() {
return "HspHandler{" +
"url='" + url + '\'' +
", controller=" + controller +
", method=" + method +
'}';
}
}
分发器的实现 dispatcher
根据requset对象,得到Handler对象,
如果该Hanlder对象不为空,则通过反射调用该Handler对象的属性
Controller对象Methods方法
/**
* 用于处理分发请求
*
* @param :
* @return void
* @author "卒迹"
* @description TODO
* @date 20:35
*/
private void dispatcher(HttpServletRequest request, HttpServletResponse response) {
HspHandler handler = getHandler(request, response);
// 判断当前handler对象是否为null
if (handler == null) {
try {
// 请求失败返回404
response.getWriter().print("<h1>404 NOT FOUND</h1>");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 调用该handler对应的Controller对象的Method方法-必须传入与该方法对应的形参
try {
handler.getMethod().invoke(handler.getController(), request, response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
在doGet/doPost方法中调用该分发器方法
3.动态的读取配置 Web.xml 配置文件(96)
分析:在 HspWebApplicationContext.java 文件中
该 init 方法,读取配置文件是写死的,不利于复用
我们希望通过 web.xml 文件读取该配置文件名,并希望动态的读取
实现思路:
1.HspDispatcherServlet.java
当 Tomcat 启动的时候会调用 init 方法,我们可以通过提供 ServletConfig 读取 initParam 标签内的值
2.HspWebApplicationContext.java
创建有参构造器
并通过静态代理模式(构造器入参)的方式,完成动态读取配置文件
3.HspDispatcherServlet.java
通过构造器带参,初始化 configLocation 属性(String)
4.HspWebApplicationContext.java
4.自定义 Service 注解
1.
新增 JavaBean 对象 Monster-提供无参、有参、get、set、toString 方法
2.定义了一个接口 MonsterService
3. 实现类 MonsterServiceImpl-在类上注解了自定义注解 @ Service
3.提供自定义注解 @Service,默认以首字母小写当作key
添加扫描包的路径XML
4.根据配置 @Service 注解,将该对象实例注入到 IOC 容器中
经过测试-在 Tomcat 启动完毕后,此时 IOC 容器存在了 Controller/Service 对象
默认是以首字母小写(key)保存到当前 IOC 容器中的
如果我们对 Service 注解指定了 value 值此时根据 debug
5.自定义 AutoWired 注解
通过自定义注解,完成对象属性的装配
public void executeAutowired() {
// 1.判断IOC容器是否为空,如果没有则没有必要装配
if (ioc.isEmpty()) {
throw new RuntimeException("IOC容器中没有要装配的Bean对象");
}
// 2.获取到IOC容器中,所有对象实例,并获取到当前对象的所有的字段
Set<Map.Entry<String, Object>> entries = ioc.entrySet();
for (Map.Entry<String, Object> entry : entries) {
Object bean = entry.getValue();
// 通过bean对象,反射得到Class对象
Class<?> aClass = bean.getClass();
// 通过Class对象,得到所有的字段Field
Field[] declaredFields = aClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
// 当前字段是否注解了AutoWired
if (declaredField.isAnnotationPresent(AutoWired.class)) {
// 得到当前字段的value值:对象名
AutoWired annotation = declaredField.getAnnotation(AutoWired.class);
String beanName = annotation.value();
// 判断当前是否设置了该注解的value值
// 默认自动装配-按照字段类型的名称首字母小写
if ("".equals(beanName)) {
Class<?> type = declaredField.getType();
beanName = type.getSimpleName().substring(0, 1).toLowerCase() + type.getSimpleName().substring(1);
}
// 判断该Bean对象是否IOC容器中,如果不在则抛出异常
// 默认是以配置对象的类型的首字母小写,从IOC容器中查找
// 如果该配置的对象设置value,则以value值查找(存在Bug,这里这是简单模拟)
if (null == ioc.get(beanName)) {
throw new RuntimeException("需要装配的对象不在IOC容器中");
}
// 防止装配的对象的属性私有化,需要暴力破解
declaredField.setAccessible(true);
try {
// 可以装配
// 参数1:当前注解AutoWired的所在的对象
// 参数2:需要装配的那个对象(IOC容器中存在的)
declaredField.set(bean, ioc.get(beanName));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
1. 在 Tomcat 启动的时候,在 init 方法中调用该方法进行初始化
2.在对象的字段中设置自定义注解@ Autowired
这里只能使用对象名,否则 IOC 容器中会找不到该对象实例
6.自定义 RequestParam 注解
功能说明
功能实现
完成:将方法的 HttpServletRequest 和 HttpServletResponse 参数封装到参数数组,进行反射调用
测试:访问该 URI 地址,看是否能够正常访问
完成:在方法参数指定@RequestParam的参数封装到参数数组,进行反射调用
当发起请求后,会先经过 dispachter 分发器,得到参数
简单模拟 1 个数据集合-实际开发中是通过数据库查询
创建自定义注解 RequestParam
/**
* 返回该目标方法的形参,在形参列表的第几个索引位置
*
* @param method:目标方法
* @param name: 形参的名
* @return int
* @author "卒迹"
* @description TODO
* @date 21:21
*/
public int getIndexRequestParameterIndex(Method method, String name) {
// 1.得到该目标方法的所有参数列表
Parameter[] parameters = method.getParameters();
// 2.分两种情况,
// 2-1注解了@RequestParam的形参
// 2-2没有注解的形参
for (int i = 0; i < parameters.length; i++) {
// 得到当前的形参
Parameter parameter = parameters[i];
// 判断当前形参是否注解了RequestParam
if (parameter.isAnnotationPresent(RequestParam.class)) {
// 得到该注解的value值,判断是否与请求的参数名name一致
RequestParam annotation = parameter.getAnnotation(RequestParam.class);
String value = annotation.value();
if (value.equals(name)) {
return i;// 得到该形参,在形参列表的索引位置
}
}
}
return -1;// 如果没有匹配成功,返回-1
}
测试:没有设置注解,则按照形参名称匹配
<build>
<finalName>springmvc02</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
7.实现简单的视图解析
login.jsp
login_ok.jsp
login_error.jsp
MonsterService
MonsterServiceImpl
MonsterController
防止中文乱码-经过分发器
在得到所有的参数列表前,设置 UTF-8 格式防止中文乱码
此时,该方法还无法识别该请求转发的字符串,解析该请求转发的字符串
接收返回的请求字符串,并进行解析
login_ok.jsp
login_error.jsp
8.自定义@ResponseBody 注解
分析:
浏览器请求对应的 URI 地址,后端接收到请求后交给分发器进行处理
底层通过反射调用对应 Controller 对象的方法,并返回 1 个 JSON 格式的数据给到浏览器
判断定义的方法
返回的是否是 1 个集合,并且是否注解了@ResponseBody
代码实现
自定义注解@ResponseBody
在控制层编写 1 个方法返回 Json 格式的数据
思路分析;
目标方法返回的结果是 springmvc 底层通过反射调用的位置
我们在springmvc 底层通过反射调用的位置,接收到结果并解析即可
HspDisPatcherServlet-executeDispatch 方法
核心思路:
该方法在浏览器/客户端请求 get/post 的时候被调用
分发器executeDispatch 方法中,会根据浏览/客户端 URI 映射路径从容器中
匹配找到对应的 handler 对象
此时我们就可以通过该 handler 对象
使用反射调用该 handler 对象中控制层 Controller(对象) 的方法
测试
当我们去请求注解了@ResponseBody并且返回值是List集合的方法的该映射路径的地址(/spingmvc02/json )
此时,控制台已经将对应方法的 List 集合的数据转换成了 JSON 格式数据
并打印在控制台上
引入 FastJSON 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>