手动开发-实现SpringMVC底层机制--小试牛刀

这篇具有很好参考价值的文章主要介绍了手动开发-实现SpringMVC底层机制--小试牛刀。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在这里说的底层机制的实现主要是指:前端控制器、Controller、Service注入容器、对象自动装配、控制器方法获取参数、视图解析、返回json数据。

前端控制器

前端控制器就是核心控制器。在这里我们可以设计一个Servlet来充当核心控制器:LingDispatcherServlet.java.这个控制器的作用主要是接收响应前端过来的Http请求和Response响应。一开始需要在web.xml中配置好控制器的 请求路径,还要配置好SpringMVC的xml文件: lingspringmvc.xml.

lingspringmvc.xml

<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <component-scan base-package="com.linghu.controller"></component-scan>
</beans>

有了这个xml文件就好办了,里面写了Controller层的类路径,只需要通过dom4j技术将类路径读出来,就可以轻松的将该文件下的类文件进行读取遍历,再去分析他们是否加了我们设计的注解,如果加了就保留类路径,甚至加入到我们自己设计的ioc容器中。在读取xml文件里的内容的时候,可以单独写一个工具类XMLParser.java

package com.linghu.springmvc.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;

/**
 * @author linghu
 * @date 2023/9/11 14:22
 */
public class XMLParser {
    public static String getBasePackage(String xmlFile){
        SAXReader saxReader = new SAXReader();
        InputStream resourceAsStream =
                XMLParser.class.getClassLoader().getResourceAsStream(xmlFile);

        try {
            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();
            System.out.println("basePackage="+basePackage);
            return basePackage;
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Controller注解

在Controller层,我们会在类上标注Controller注解,容器就会将这个类路径扫描加入到我们的容器中。在这里我们将自己设计一个自己的注解Controller注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
    String value() default "";
}

RequestMapping注解

前端发出请求的时候,请求地址为:IP+端口+URI。RequestMapping注解可以规定我们请求的URI。同样我们需要自己设计一个属于自己的RequestMapping注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
    String value() default "";
}

前端发出请求的时候,如果携带了参数,后端接收的时候就要标注接收的参数字段,会用到注解 RequestMapping注解。

自定义容器LingWebApplicationContext

需要自己设计一个容器,将我们从lingspringmvc.xml中读取到的类路径下的.class文件的路径列表全部存起来。具体来说 就是:

<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <component-scan base-package="com.linghu.controller"></component-scan>
</beans>

需要将上面com.linghu.controller这个路径下的类文件的路径进行存储。以后这个扫描的包不光有controller,还会有service,dao等等。

这个容器里最重要的就是要保存我们扫描包和子包下的全类路径,所以我们定义了一个 classFullPathList集合用来存储。

private ArrayList<String> classFullPathList=new ArrayList<>();

接下来就是利用工具类 XMLParser去读取springmvc.xml配置文件里的包路径,接着利用文件和IO的知识去扫描这个包路径下的文件和目录,将这个路径下的所有.class文件的后缀.class裁剪掉,将路径名和文件名进行拼接就得到了类文件的类路径了,我们将他们存储到classFullPathList集合中即可。如下:

手动开发-实现SpringMVC底层机制--小试牛刀,SpringMvc,状态模式

这个类路径就对应下图:

手动开发-实现SpringMVC底层机制--小试牛刀,SpringMvc,状态模式

这里目前只讨论controller注解的类,因为springmvc.xml扫描的包只写了controller这个包。

利用文件和IO的知识去扫描这个包路径下的文件和目录是重点,在扫描以后,我们要将这个路径下的所有.class文件的后缀.class裁剪掉,将路径名和文件名进行拼接。这两步是最重要的。scanPackage()函数可以完成这两个重要的东西。

  public void init(){
        String basePackage = XMLParser.getBasePackage("lingspringmvc.xml");
        String[] packages = basePackage.split(",");
        if (packages.length>0){
            for (String pack :packages) {
                scanPackage(pack);
            }
        }
        System.out.println("classFullPathList="+classFullPathList);
    }

    public void scanPackage(String pack){
        URL url = this.getClass().
                getClassLoader().
                getResource("/" + pack.replaceAll("\\.", "/"));
        String path = url.getFile();//获取所有文件的目录
        File dir = new File(path);
        for (File f :dir.listFiles()) {
            if (f.isDirectory()){
                scanPackage(pack+"."+f.getName());
            }else {
                String classFullPath=pack+"."+f.getName().replaceAll(".class","");
                classFullPathList.add(classFullPath);
            }
        }

    }

将classFullPathList存放的扫描的全类路径文件的类名提出来,将第一个字母小写,作为bean对象的名称,类似于以前xml配置bean对象的时候,声明的bean的id。然后将类路径进行反射创建对象,同时将beanName和反射创建好的对象放到ioc容器中。如下:

手动开发-实现SpringMVC底层机制--小试牛刀,SpringMvc,状态模式

 public void executeInstance(){
        if (classFullPathList.size()==0){
            return;
        }

        try {
            for (String  classFullPath:classFullPathList) {
                Class<?> clazz = Class.forName(classFullPath);
                if (clazz.isAnnotationPresent(Controller.class)){
                    String beanName=clazz.getSimpleName().substring(0,1).toLowerCase()+
                            clazz.getSimpleName().substring(1);
                    //同时将beanName和反射创建好的对象放到ioc容器中
                    ioc.put(beanName,clazz.newInstance());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

设计handlerList

Controller层会写很多接口,规范化请求的地址,用注解RequestMapping进行了标识,RequestMapping的Value值其实就是请求的地址,我们把它取出来单独命名成url,在把RequestMapping标识的方法的方法名取出来命名成method,最后把当前方法所在的类,也就是Controller标识的这个类的对象命名成controller。我们现在有了url,controller,method。其实就已经拿到了url和method的映射关系了,现在将其封装保存到handerList集合中。这样做的好处是,当前端发来请求的时候,我们可以取出请求的url,通过url在handerList中找到对应的调用的方法名,实现调用的映射。

先设计一个hander,用来封装url,controller,method:

package com.linghu.springmvc.handler;

import java.lang.reflect.Method;

/**
 * @author linghu
 * @date 2023/9/12 9:23
 */
public class LingHandler {
    private String url;
    private Object controller;
    private Method method;

    public LingHandler() {
    }

    public LingHandler(String url, Object controller, Method method) {
        this.url = url;
        this.controller = controller;
        this.method = method;
    }

    @Override
    public String toString() {
        return "LingHandler{" +
                "url='" + url + '\'' +
                ", controller=" + controller +
                ", 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;
    }
}

  private void initHandlerMapping(){
        if (lingWebApplicationContext.ioc.isEmpty()){
            return;
        }
        for (Map.Entry<String,Object> entry :lingWebApplicationContext.ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();
            if (clazz.isAnnotationPresent(Controller.class)){
                Method[] declaredMethods = clazz.getDeclaredMethods();
                for (Method method :declaredMethods) {
                    if (method.isAnnotationPresent(RequestMapping.class)){
                        RequestMapping requestMappingAnnotation =
                                method.getAnnotation(RequestMapping.class);
                        String url = requestMappingAnnotation.value();
                        LingHandler lingHandler = new LingHandler(url,entry.getValue(),method);
                        handlerList.add(lingHandler);
                    }
                }

            }

        }

    }

initHandlerMapping()会将容器中的对象,也就是之前 我们通过类路径反射创建的对象进行遍历,遍历过程中筛选出被Controller注解标识过的类,将类里的方法再全部进行遍历,遍历过程中将被RequestMapping注解标识的方法选出来,将这些方法的方法名命名成method,注解RequestMapping的value值取出来命名成url,将当前类命名成controller,最后将url,controller,method放到LingHandler对象中,在将LingHandler对象放到handerList集合中。

手动开发-实现SpringMVC底层机制--小试牛刀,SpringMvc,状态模式

手动开发-实现SpringMVC底层机制--小试牛刀,SpringMvc,状态模式

完成分发请求

前端发送一个请求过来,无论是get,post请求都要经过我的servlet,这个时候可以取出请求request信息里的uri,这样我就得到了前端想要请求的Controller层的具体方法,其实拿到这个方法我们就可以利用反射进行调用了。所以完成分发请求的还是我们的servlet,也就是文章一开篇就说的前端控制器,核心控制器,它就是我们的整个大脑核心,负责接收请求,完成请求分发,分发到具体要执行的Controller层的方法去。

前端过来的请求,我们可以让它统一都走post请求。

LingDispatcherServlet.java:

   @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("LingDispatcherServlet doPost");
        executeDispatch(req, resp);
    }

executeDispatch(req, resp)便是携带着前端request请求的具体执行分发请求的方法。

 public void executeDispatch(HttpServletRequest request,
                                HttpServletResponse response){
        LingHandler lingHandler = getLingHandler(request);
        try {
            if (lingHandler==null){
                response.getWriter().print("<h1>404</h1>");
            }else {
                lingHandler.getMethod().invoke(lingHandler.getController(),request,response);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

lingHandler.getMethod().invoke(lingHandler.getController(),request,response);便是反射调用,是完成分发的核心。在这里lingHandler.getMethod()本身就是Method类型的对象,所以可以进行调用。getLingHandler(request)的具体过程如下:

 public LingHandler getLingHandler(HttpServletRequest request){
        String requestURI = request.getRequestURI();
        for (LingHandler lingHandler :handlerList) {
            if (requestURI.equals(lingHandler.getUrl())){
                return lingHandler;
            }
        }
        return null;
    }

getLingHandler完成的便是查看handlerList集合中有没有前端请求的方法url,有的话就直接把handler对象返回,handler对象里保存着前端请求url和调用后端方法名的映射关系,通过映射关系我们可以查到具体要调用的方法是谁。

Service注解和AutoWired注解

这两个注解的实现其实和Controller注解差不多的流程三板斧。先通过元注解定义好这两个注解,在扫描全类路径的时候,去判断有没有Service注解,有的话就获取文件下的所有接口名,对接口名字首字母变小写,然后拼接得到新的beanName,最后通过反射创建对象,将beanName和对象加入到ioc中。

 } else if (clazz.isAnnotationPresent(Service.class)) {
                    Service serviceAnnotation = clazz.getAnnotation(Service.class);
                    String beanName = serviceAnnotation.value();
                    if ("".equals(beanName)){
                        Class<?>[] interfaces = clazz.getInterfaces();
                        for (Class<?> anInterface :interfaces) {
                            String beanName2=anInterface.getSimpleName().substring(0,1).toLowerCase()+
                                    anInterface.getSimpleName().substring(1);
                            ioc.put(beanName2,clazz.newInstance());
                        }
                    }else {
                        ioc.put(beanName,clazz.newInstance());
                    }
                }

设计Controller注解的时候,我们是直接获取的 类名,在这里设计Service注解的时候我们获取的是接口名字,是因为接口内部装满了所有实现类,而我们的Service注解又是写在这些实现类上面 的,我们通过获取接口,就有机会遍历到这些实现类,如果不通过获取接口,直接获取实现类代价要大点。

AutoWired注解的作用是将dao层和业务层对象注入到ioc中,方便业务层或者控制层调用dao层和业务层。扫描什么的其实不难,就是全文扫描带AutoWired注解的属性,将其加入到容器中。加入的这个过程放在一个函数executeAutoWired()中。

    public void executeAutoWired(){
        if (ioc.isEmpty()){
            throw new RuntimeException("容器中没有可以装配的bean");
        }
        for (Map.Entry<String,Object> entry :ioc.entrySet()) {
            String key = entry.getKey();
            Object bean = entry.getValue();
            Field[] declaredFields = bean.getClass().getDeclaredFields();
            for (Field declaredField :declaredFields) {
                if (declaredField.isAnnotationPresent(AutoWired.class)){
                    AutoWired autoWiredAnnotation =
                            declaredField.getAnnotation(AutoWired.class);
                    String beanName = autoWiredAnnotation.value();
                    if ("".equals(beanName)){
                        Class<?> type = declaredField.getType();
                        beanName= type.getSimpleName().substring(0,1).toLowerCase()+
                                        type.getSimpleName().substring(1);

                    }
                    declaredField.setAccessible(true);

                    try {
                        if (ioc.get(beanName)==null){
                            throw new RuntimeException("容器中没有注入该bean");
                        }
                        declaredField.set(bean,ioc.get(beanName));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

RequestParam注解

前端在发送请求的时候,会携带一些参数过来,这个时候后端要接收请求的时候处理好参数字段的对应关系。我们可以用requestparam注解标识前端对应过来的参数字段。

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    String value() default "";
}
 public void executeDispatch(HttpServletRequest request,
                                HttpServletResponse response){
        LingHandler lingHandler = getLingHandler(request);

        try {
            if (lingHandler==null){
                response.getWriter().print("<h1>404</h1>");
            }else {
//                lingHandler.getMethod().invoke(lingHandler.getController(),request,response);
                Class<?>[] parameterTypes =
                        lingHandler.getMethod().getParameterTypes();
                Object [] params = new Object[parameterTypes.length];

                for (int i=0;i< parameterTypes.length;i++){
                    Class<?> parameterType=parameterTypes[i];
                    if ("HttpServletRequest".equals(parameterType.getSimpleName())){
                        params[i]=request;
                    } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                        params[i]=response;
                    }
                }
                Map<String,String[]> parameterMap =
                        request.getParameterMap();

                for (Map.Entry<String,String[]> entry :parameterMap.entrySet()) {
                    String name = entry.getKey();
                    String value = entry.getValue()[0];
                    System.out.println("请求的参数:"+name+"----"+value);
                    int indexRequestParamIndex=
                            getIndexRequestParamIndex(lingHandler.getMethod(),name);

                    if (indexRequestParamIndex!=-1){
                        params[indexRequestParamIndex]=value;
                    }else {

                    }
                }
                lingHandler.getMethod().invoke(lingHandler.getController(),params);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public int getIndexRequestParamIndex(Method method,String name){
        Parameter[] parameters = method.getParameters();
        for (int i=0;i<parameters.length;i++){
            Parameter parameter=parameters[i];
            boolean annotationPresent =
                    parameter.isAnnotationPresent(RequestParam.class);
            if (annotationPresent){
                RequestParam requestParamAnnotation =
                        parameter.getAnnotation(RequestParam.class);
                String value = requestParamAnnotation.value();
                if (name.equals(value)){
                    return i;
                }
            }
        }
        return -1;
    }

完整代码

《实现SpringMVC底层机制》文章来源地址https://www.toymoban.com/news/detail-712419.html

到了这里,关于手动开发-实现SpringMVC底层机制--小试牛刀的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Spring】手动实现Spring底层机制-问题的引出

    🎄欢迎来到@边境矢梦°的csdn博文🎄 🎄本文主要梳理手动实现Spring底层机制-问题的引出 🎄 🌈我是边境矢梦°,一个正在为秋招和算法竞赛做准备的学生🌈 🎆喜欢的朋友可以关注一下 🫰🫰🫰 ,下次更新不迷路🎆 Ps: 月亮越亮说明知识点越重要 (重要性或者难度越大)🌑

    2024年02月09日
    浏览(32)
  • 从零开始搭建flink流式计算项目-2小试牛刀-物联网场景下,如何实现设备采集参数监控报警功能

    * 设备ID */ private Integer deviceId; * 监控的变量名称 */ private String varName; * 最小值 */ private Double min; * 最大值 */ private Double max; } /** * 报警消息 */ @Data public class AlarmMessage { * 设备 */ private Integer deviceId; * 报警时间 */ private Long timestamp; /** * 触发报警的采集变量名称 */ private String ala

    2024年04月11日
    浏览(31)
  • Mapreduce小试牛刀(1)

    1.与hdfs一样,mapreduce基于hadoop框架,所以我们首先要启动hadoop服务器 --------------------------------------------------------------------------------------------------------------------------------- 2.修改hadoop-env.sh位置JAVA_HOME配置,在JAVA_HOME前面加上export,重启主虚拟机,最好也把另外两个节点同位置的

    2024年02月04日
    浏览(31)
  • 网页学习-小试牛刀

    分为三大部分: HTML 、 CSS 和 JavaScript 。 HTML(Hyper Text Markup Language,即超文本标记语言),网页骨架。 CSS(Cascading Style Sheets,层叠样式表),使页面变得美观、优雅,网页皮肤。 JavaScript(简称JS,是一种脚本语言),实现实时、动态、交互的页面功能,网页肌肉。 学习目的

    2023年04月22日
    浏览(51)
  • 运维Shell脚本小试牛刀(一)

    运维Shell脚本小试牛刀(一) 运维Shell脚本小试牛刀(二) 运维Shell脚本小试牛刀(三)::$(cd $(dirname $0); pwd)命令详解 运维Shell脚本小试牛刀(四): 多层嵌套if...elif...elif....else fi_蜗牛杨哥的博客-CSDN博客 Cenos7安装小火车程序动画 运维Shell脚本小试牛刀(五):until循环 运维Shell脚本小试牛刀

    2024年02月11日
    浏览(31)
  • 快速上手kettle(二)小试牛刀

    目录 一 、前言 二 、两个小目标 三、 kettle核心概念介绍 3.1 转换 3.1.1 步骤(Step) 3.1.2 跳(Hop) 3.1.3 元素据 3.1.4 数据类型 3.1.5 并发执行 3.2 作业 四、实践操作 4.1 案例1 将csv文件转换成excel文件 4.1.1 在kettle中新建一个转换 4.1.2选择输入控件并设置 4.1.3 选择输出控件并设置 4.

    2024年02月06日
    浏览(30)
  • 运维Shell脚本小试牛刀(二)

    运维Shell脚本小试牛刀(一) 运维Shell脚本小试牛刀(二) 运维Shell脚本小试牛刀(三)::$(cd $(dirname $0); pwd)命令详解 [root@www shelldic]# cat checkpass.sh  #!/bin/bash - #================================================================================================================== # # #                          

    2024年02月10日
    浏览(25)
  • 【LED子系统】八、小试牛刀

    个人主页:董哥聊技术 我是董哥,高级嵌入式软件开发工程师,从事嵌入式Linux驱动开发和系统开发,曾就职于世界500强公司! 创作理念:专注分享高质量嵌入式文章,让大家读有所得!

    2024年02月06日
    浏览(30)
  • verilator——牛刀小试

    安装verilator可见:https://blog.csdn.net/qq_40676869/article/details/132648522?spm=1001.2014.3001.5501 编写一个异或的电路模块如下: top.v 编写C++测试文件 tb_top.cpp 编译并运行 –cc 将.v文件翻译成c++ –exe 创建可执行文件 –build verilator自动进行make –trace 记录波形 波形如下: github链接:https:/

    2024年02月10日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包