SpringMvc手写简单实现篇 - MVC完结篇

本文详细介绍了手写实现SpringMVC框架的过程,从MVC架构概念出发,逐步讲解了如何配置servlet、创建HandlerMapping、视图解析器,并通过具体代码展示了doGet和doPost的处理逻辑,最后通过示例展示了正常响应、返回字符串及页面的情况。文章旨在阐述SpringMVC的工作原理。
摘要由CSDN通过智能技术生成

经过上面两篇文章,spring原理应该都大致懂了吧(代码不重要,重点看思路)。
spring实现了,那就不得不提MVC架构了。

MVC架构

如何设计一个程序的结构,这是一门专门的学问,叫做“架构模式”(architectural pattern),属于编程的方法论。MVC模式就是架构模式的一种:

Model(业务模型):数据和业务规则,一个M能给多个V提供数据,复用M减少代码重复性。
View(前端页面):用户看到并与之交互的界面。
Controller(业务控制器):接收用户的输入并调用M和V去完成用户的需求。

这三个应该不陌生吧。今天就来手写实现一下吧。

MVC手写简单实现

1.预先准备

1.1 代码结构变化(接上节)

在这里插入图片描述

1.2 需要自己配置tomcat启动,要依赖servlet,这里贴一下pom文件

<groupId>com.deno</groupId>
<artifactId>springmvc-demo</artifactId>
<packaging>war</packaging>
<version>1.0</version>

<dependencies>
	<dependency>
		<groupId>javax.servlet</groupId>
		<artifactId>servlet-api</artifactId>
		<version>2.4</version>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.16.10</version>
	</dependency>
</dependencies>

1.3 了解tomcat的肯定都知道web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
		 version="4.0">
	<display-name>test web spring</display-name>
	<servlet>
		//servlet名称
		<servlet-name>TestSpringMvc</servlet-name>
		//servlet实现类,这里我们自己实现
		<servlet-class>com.xxx.framework.springmvc.TDispatcherServlet</servlet-class>
		//init方法和参数
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:application.properties</param-value>
		</init-param>
		//启动加载次数
		<load-on-startup>1</load-on-startup>
	</servlet>
	//拦截路径配置
	<servlet-mapping>
		<servlet-name>TestSpringMvc</servlet-name>
		//拦截请求路径正则
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
</web-app>

1.4 TRequestMapping、TRequestParam注解

直接copy,这里不贴

1.5 配置文件以及 layouts目录

  • 配置文件新加

#MVC 前端页面目录
htmlRootTemplate=layouts

  • 404 和 500 页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>页面去火星了</title>
</head>
<body>
    <font size='30' color='red'>404 Not Found</font>
</body>
</html>

//500页面,使用#{}替代${}取值
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>服务器出错了</title>
</head>
<body>
    <font style="font-size: 18px" color='blue'>500 服务器好像有点累了,需要休息一下<br/>
    msg: #{msg}<br/>
    error: #{error}<br/>
    </font>
</body>
</html>
  • index.html 使用#{}替代${}取值
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<font color="red" style="font-size: 30px">
    welcome to #{name}<br>
    当前时间: #{date}<br>
    token: #{token}<br>
</font>
</body>
</html>

2. servlet初始化阶段TDispatcherServlet

现在开始一步一步分析。既然是继承了HttpServlet,那肯定由该类重写init()了。

2.1 init()配置解析

public class TDispatcherServlet extends HttpServlet {
	//上下文
    TestApplicationContext context;
    //请求路径对应的Controller spring 实际是list 这里简化用map
    Map<String, Handler> handlerMapping = new HashMap<String, Handler>();
    TView view;

    @Override
    public void init(ServletConfig config) throws ServletException {
        //上下文IOC DI阶段 前面的文章里已经写好了 这里直接调用
        this.context = new TestApplicationContext(
                config.getInitParameter("contextConfigLocation"));
				
		//springmvc实际上这里是初始化9大组件,后面揭晓
				
        //初始化HandlerMapping & 参数解析
        initHandlerMapping();

        //初始化视图转换器 传入配置的htmlRootPath
        initViewResolvers(context.getContextConfig().getProperty("htmlRootTemplate"));

        System.out.println("spring mvc is init");
    }
}

2.2 初始化mapping initHandlerMapping

private void initHandlerMapping() {
    //加载Controller层
    for (Map.Entry<String, Object> entry : this.context.getIoc().entrySet()) {
        Object instance = entry.getValue();
        Class<?> clazz = instance.getClass();
        if (!clazz.isAnnotationPresent(TController.class)) continue;

        //获取根路径
        String baseUrl = "";
        if (clazz.isAnnotationPresent(TRequestMapping.class)) {
            TRequestMapping annotation = clazz.getAnnotation(TRequestMapping.class);
            baseUrl = annotation.value();
        }
        //获取所有方法上的路径
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (!method.isAnnotationPresent(TRequestMapping.class)) continue;

            TRequestMapping annotation = method.getAnnotation(TRequestMapping.class);
            String reqUrl = (baseUrl + "/" + annotation.value()).replaceAll("/+", "/");
            if (this.handlerMapping.containsKey(reqUrl)) {
                throw new RuntimeException("[" + reqUrl + "] the handlerMapping is exists");
            }
			//保存到map中
            this.handlerMapping.put(reqUrl, new Handler(instance, method, reqUrl));
            System.out.println("mapping添加: " + reqUrl);
        }
    }
}
  • Handler 记录类信息和参数信息
@Data
public class Handler {

    Object target;  //实例类
    Method method;  //方法
    String url;     //请求路径
    Class<?>[] parameterTypes; //参数类型
    Map<String,Integer> mapIndex;   //参数位置


    public Handler(Object target, Method method, String reqUrl) {
        this.target = target;
        this.method = method;
        this.url = reqUrl;
        this.parameterTypes = method.getParameterTypes();
        doHanlerParam(method);
    }

    //初始化参数map 记录参数key和位置
    private void doHanlerParam(Method method) {
        this.mapIndex = new HashMap<String, Integer>();
        //先处理 req 和resp的位置
        for (int i = 0; i < this.parameterTypes.length; i++) {
            Class<?> type = this.parameterTypes[i];
            if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
                this.mapIndex.put(type.getName(),i);
            }
        }

        //处理参数的位置(根据注解)
        Annotation[][] pas = method.getParameterAnnotations();
        for (int k = 0; k < pas.length; k++) {  //如果用forEach 需要用Map存储参数位置
            Annotation[] pa = pas[k];
            for (int i = 0; i < pa.length; i++) {
                Annotation annotation = pa[i];
                if (annotation instanceof TRequestParam) {
                    String key = ((TRequestParam) annotation).name();
                    this. mapIndex.put(key,k);
                }
            }
        }
    }
}

2.3 初始化视图转换器

private void initViewResolvers(String htmlRootTemplate) {
   //加载html页面目录
   URL url = this.getClass().getClassLoader().getResource(htmlRootTemplate);

   //封装成view 在view中提供渲染方法
   this.view = new TView(url.getFile());
}
  • View保存htmlRootPath并提供渲染方法render()
@Data
public class TView {

    String htmlPath;

    public TView(String htmlPath) {
        this.htmlPath = htmlPath;
    }


    public void render(Map<String, Object> modelMap, String htmlName, HttpServletResponse resp) throws Exception {
        StringBuilder builder = new StringBuilder();
        //获取html的路径
        File viewFile = new File(this.htmlPath + "/" + htmlName
                + (htmlName.endsWith(".html") ? "" : ".html"));
        //读行
        RandomAccessFile accessFile = new RandomAccessFile(viewFile, "r");
        String line;
        while (null != (line = accessFile.readLine())) {
            line = new String(line.getBytes("ISO-8859-1"), "utf-8");
            //正则匹配#{}的字段
            Pattern pattern = Pattern.compile("#\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
            Matcher matcher = pattern.matcher(line);
            while (matcher.find()) {
                String paramName = matcher.group();
                //#{name}
                paramName = paramName.replaceAll("#\\{|\\}", "");
                //从map中取出并替换
                Object paramValue = modelMap.get(paramName);
                if (paramValue != null) {
                    line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
                } else {
                    line = matcher.replaceFirst("null");
                }
                matcher = pattern.matcher(line);
            }
            builder.append(line);
        }
        resp.setCharacterEncoding("utf-8");
        resp.getWriter().write(builder.toString());
    }

    //处理特殊字符
    public static String makeStringForRegExp(String str) {
        return str.replace("\\", "\\\\").replace("*", "\\*")
                .replace("+", "\\+").replace("|", "\\|")
                .replace("{", "\\{").replace("}", "\\}")
                .replace("(", "\\(").replace(")", "\\)")
                .replace("^", "\\^").replace("$", "\\$")
                .replace("[", "\\[").replace("]", "\\]")
                .replace("?", "\\?").replace(",", "\\,")
                .replace(".", "\\.").replace("&", "\\&");
    }
}

3.servlet调用阶段TDispatcherServlet

使用过servlet的时候,对doGetdoPost肯定不陌生吧

3.1 重写方法doGet和doPost

@Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
     this.doPost(req, resp);
 }

 @Override
 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
     try {
         //调用
         doDispatcher(req, resp);
     } catch (Exception e) {
         try {
             if ("404".equals(e.getMessage())) {
                 this.view.render(null, "404", resp);
             } else {
                 Map<String, Object> modelMap = new HashMap<String, Object>();
                 modelMap.put("msg", e.getMessage());
                 modelMap.put("error", Arrays.toString(e.getStackTrace()));
                 this.view.render(modelMap, "500", resp);
             }
         } catch (Exception ex) {
             ex.printStackTrace();
         }

     }
 }

3.2 调用处理doDispatcher

private void doDispatcher(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    //根据请求路径 得到存储Controller类的handler
    Handler handler = getHandler(req, resp);
    //解析参数
    Object[] objects = getParam(req, resp, handler);

    //反射调用
    Object result = handler.getMethod().invoke(handler.getTarget(), objects);

    if (null == result || result instanceof Void) return;
    //返回页面的
    if (result instanceof TModelAndView) {
        TModelAndView mv = (TModelAndView) result;
        this.view.render(mv.getModel(), mv.getHtmlName(), resp);
    } else {
        //解决前端页面显示中文乱码
        resp.setHeader("Content-Type", "text/html; charset=utf-8");
        resp.getWriter().write(result.toString());
    }
}
  • getHandler 获取Handler
private Handler getHandler(HttpServletRequest req, HttpServletResponse resp) {
    String uri = req.getRequestURI();
	//从mapping map中获取 不存在返回404
    if (this.handlerMapping.containsKey(uri)) {
        return this.handlerMapping.get(uri);
    }
    throw new RuntimeException("404");
}

3.3 参数处理

private Object[] getParam(HttpServletRequest req, HttpServletResponse resp, Handler handler) {
     //参数类型和参数的下标位置
     Class<?>[] parameterTypes = handler.getParameterTypes();
     Map<String, Integer> mapIndex = handler.getMapIndex();

     //传入参数
     Map<String, String[]> parameterMap = req.getParameterMap();

     Object[] params = new Object[mapIndex.size()];
     //根据下标位置获取参数
     for (Map.Entry<String, Integer> entry : mapIndex.entrySet()) {
         String key = entry.getKey();
         Integer index = entry.getValue();
         if (key.equals(HttpServletRequest.class.getName())) {
             params[index] = req;
         }
         if (key.equals(HttpServletResponse.class.getName())) {
             params[index] = resp;
         }
         //同名参数这版只考虑一个
         if (parameterMap.containsKey(key)) {
             String value = Arrays.toString(parameterMap.get(key)).replaceAll("\\[|\\]", "");
             //根据参数类型的数组 转换参数
             params[index] = converter(value, parameterTypes[index]);
         }
     }
     return params;
 }

 private Object converter(String value, Class<?> type) {
     if (type == Integer.class) {
         return Integer.valueOf(value);
     } else if (type == Double.class) {
         return Double.valueOf(value);
     }
	//其它不写了
     return value;
 }

4. 调用测试

打包,配置tomcat,HelloAction添加@TRequestMapping("/mvc")
启动日志输出

application is init
mapping添加: /mvc/index.html
mapping添加: /mvc/hello1
mapping添加: /mvc/hello2
spring mvc is init

4.1 正常response write返回

@TRequestMapping("/hello1")
public void hello1(@TRequestParam(name = "name") String name, HttpServletResponse response) {
    try {
        response.setHeader("Content-Type","text/html; charset=utf-8");
        response.getWriter().write(helloService.sayHello(name));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

请求 http://localhost:8080//mvc/hello1?name=王二麻子

my name is 王二麻子

4.2 直接return字符串

@TRequestMapping("/hello2")
public String hello2(@TRequestParam(name = "a") Integer a, @TRequestParam(name = "b") Integer b) {
    return "a + b = " + (a + b);
}

请求http://localhost:8080/mvc/hello2?a=28&b=232

a + b = 260

4.3 返回html页面

@TRequestMapping("/index.html")
public TModelAndView index(@TRequestParam(name = "name") String name) {
    Map<String, Object> modelMap;
    try {
        String time = helloService.getDataTime(name);
        modelMap = new HashMap<String, Object>();
        modelMap.put("name", name);
        modelMap.put("date", time);
        modelMap.put("token", UUID.randomUUID());
        return new TModelAndView("index", modelMap);
    } catch (Exception e) {
        modelMap = new HashMap<String, Object>();
        modelMap.put("msg", "服务器请求异常");
        modelMap.put("error", Arrays.toString(e.getStackTrace()));
        return new TModelAndView("500", modelMap);
    }
}

请求http://localhost:8080/mvc/index.html?name=北京

Title welcome to 北京
当前时间: 2021-12-14 11:30:33
token: af3efd56-7a13-4dd4-8ce1-a1327ab80fc4

4.4 service异常切面日志以及500页面

在service新加报错代码

public String getDataTime(String name) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String date = format.format(new Date());
    System.out.println("获取当前时间:" + date + ",name=" + name);
 	int i =1/0;     //测试切面异常和返回异常model
    return date;
}

请求http://localhost:8080/mvc/index.html?name=北京

  • 先看html页面返回
    在这里插入图片描述
  • Java 控制台日志打印
application is init
mapping添加: /mvc/index.html
mapping添加: /mvc/hello2
mapping添加: /mvc/hello1
spring mvc is init
[2021-12-14 11:34:17,868] Artifact springmvc-demo:war: Artifact is deployed successfully
[2021-12-14 11:34:17,868] Artifact springmvc-demo:war: Deploy took 698 milliseconds
方法之前调用:JoinPoint(target=null, method=null, args=[北京], result=null, throwName=null)
获取当前时间:2021-12-14 11:34:22,name=北京
出现异常调用:JoinPoint(target=null, method=null, args=[北京], result=null, throwName=/ by zero)

SpringMvc手写内容到这里就全部完成了,基本上也算是实现了主体功能,当然仅供参考思路哈。下篇文章开始源码的分析。

本文为学习记录,不支持转载

最后奉上:SpringMvc简单手写版Github地址

上一篇:SpringMvc手写简单实现篇 - AOP切换编程篇
下一篇:Spring源码分析第一弹 - IOC控制反转分析

黑发不知勤学早,白发方悔读书迟

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值