自己实现 SrpingMVC 底层机制

自己实现 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>

前端测试:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值