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

文章目录

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

前端控制器

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

lingspringmvc.xml

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

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注解。

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

RequestMapping注解

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

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

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

自定义容器LingWebApplicationContext

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

xml 复制代码
<?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集合用来存储。

java 复制代码
private ArrayList<String> classFullPathList=new ArrayList<>();

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

这个类路径就对应下图:

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

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

java 复制代码
  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容器中。如下:

java 复制代码
 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:

java 复制代码
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;
    }
}
java 复制代码
  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集合中。

完成分发请求

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

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

LingDispatcherServlet.java:

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请求的具体执行分发请求的方法。

java 复制代码
 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)的具体过程如下:

java 复制代码
 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中。

java 复制代码
 } 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()中。

java 复制代码
    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注解标识前端对应过来的参数字段。

java 复制代码
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    String value() default "";
}
java 复制代码
 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底层机制》

相关推荐
chinesegf7 小时前
图文并茂的笔记、便签是如何用py开发的
笔记·状态模式
云闲不收2 天前
GraphQL教程
后端·状态模式·graphql
e***98573 天前
SpringMVC的工作流程
状态模式
q***08744 天前
SpringMVC的工作流程
状态模式
g***78914 天前
SpringBoot中使用TraceId进行日志追踪
spring boot·后端·状态模式
shuxiaohua6 天前
使用HttpURLConnection调用SSE采坑记录
状态模式
崎岖Qiu6 天前
状态模式与策略模式的快速区分与应用
笔记·设计模式·状态模式·策略模式·开闭原则
Jonathan Star7 天前
前端需要做单元测试吗?哪些适合做?
前端·单元测试·状态模式
一水鉴天8 天前
整体设计 全面梳理复盘 之40 M3 统摄三层 AI 的动态运营社区(Homepage)设计
架构·transformer·状态模式·公共逻辑
前端玖耀里11 天前
Vue + Axios + Node.js(Express)如何实现无感刷新Token?
状态模式