手撸一个简易Spring框架(五)

前言

这次我们来完成MVC的模块,最终目标需要达到用户能够使用Controller,浏览器能够显示模板引擎渲染的结果。为了能够解析前端的HTTP协议请求,需要在项目POM中引入Servlet

<!--引入Servlet-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>${servlet.api.version}</version>
    <scope>provided</scope>
</dependency>

DispatcherServlet

DispatcherServlet继承自HttpServlet,必然需要重写doGetdoPost来接收和处理用户的前端请求。又因为DispatcherServlet在初始化的时候就要先初始化ApplicationContext以及MVC九大组件(为了达成目的只需实现其中三个即可),因此还要重写父类init()方法。

package com.lqb.springframework.webmvc.servlet;


import com.lqb.springframework.annotation.Controller;
import com.lqb.springframework.annotation.RequestMapping;
import com.lqb.springframework.context.support.DefaultApplicationContext;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.regex.Pattern;

public class DispatcherServlet extends HttpServlet {

    /**配置文件地址,从web.xml中获取*/
    private static final String CONTEXT_CONFIG_LOCATION = "contextConfigLocation";

    private DefaultApplicationContext context;

    @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 {

    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        //1、初始化ApplicationContext
        context = new DefaultApplicationContext(config.getInitParameter(CONTEXT_CONFIG_LOCATION));

        //2、初始化Spring MVC 九大组件
        initStrategies(context);
    }

    //初始化策略
    protected void initStrategies(DefaultApplicationContext context) {
        //多文件上传的组件

        //初始化本地语言环境

        //初始化模板处理器

        //handlerMapping,必须实现
        initHandlerMappings(context);

        //初始化参数适配器,必须实现
        initHandlerAdapters(context);

        //初始化异常拦截器

        //初始化视图预处理器

        //初始化视图转换器,必须实现
        initViewResolvers(context);

        //参数缓存器
    }
}    

下面我们先来实现这三个组件HandlerMappingHandlerAdapter以及ViewResolver

HandlerMapping

HandlerMapping保存了用户写的Controller实例、所有浏览器能访问到的方法,以及使用@RequestMapping定义的URL表达式

package com.lqb.springframework.webmvc.servlet;

import lombok.Data;

import java.lang.reflect.Method;
import java.util.regex.Pattern;

@Data
public class HandlerMapping {

    //保存方法对应的实例
    private Object controller;

    //保存映射的方法
    private Method method;

    //URL的正则匹配
    private Pattern pattern;

    public HandlerMapping(Object controller, Method method, Pattern pattern) {
        this.controller = controller;
        this.method = method;
        this.pattern = pattern;
    }
}

填充到DispatcherServlet的流程中,代码逻辑如下:

  • 遍历容器中的Bean,找到被@Controller注解的
  • 遍历Controller的所有方法,找到被@RequestMapping注解的
  • 获取URL表达式,编译成正则
  • HandlerMapping添加到集合中保存起来
private List<HandlerMapping> handlerMappings = new ArrayList<>();

private void initHandlerMappings(DefaultApplicationContext context) {
    String[] beanNames = context.getBeanDefinitionNames();

    try {
        for (String beanName : beanNames) {
            Object controller = context.getBean(beanName);
            Class<?> clazz = controller.getClass();
            if (!clazz.isAnnotationPresent(Controller.class)) {
                continue;
            }

            String baseUrl = "";
            //获取Controller的url配置
            if (clazz.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
                baseUrl = requestMapping.value();
            }

            //获取Method的url配置
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {

                //没有加RequestMapping注解的直接忽略
                if (!method.isAnnotationPresent(RequestMapping.class)) {
                    continue;
                }

                //映射URL
                RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                String regex = ("/" + baseUrl + "/" + requestMapping.value().replaceAll("\\*", ".*")).replaceAll("/+", "/");
                Pattern pattern = Pattern.compile(regex);

                this.handlerMappings.add(new HandlerMapping(controller, method, pattern));
                System.out.println("Mapped " + regex + "," + method);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

HandlerAdapter

HandlerAdapter简单讲就是负责接收用户的请求,然后将参数填充到Controller中的方法中调用。

package com.lqb.springframework.webmvc.servlet;

import com.lqb.springframework.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class HandlerAdapter {

    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMapping handlerMapping = (HandlerMapping) handler;

        //把方法的形参列表和request的参数列表所在顺序进行一一对应
        Map<String, Integer> paramIndexMapping = new HashMap<>();

        //提取方法中加了注解的参数
        //把方法上的注解拿到,得到的是一个二维数组
        //因为一个参数可以有多个注解,而一个方法又有多个参数
        Annotation[][] pa = handlerMapping.getMethod().getParameterAnnotations();
        for (int i = 0; i < pa.length; i++) {
            for (Annotation a : pa[i]) {
                if (a instanceof RequestParam) {
                    String paramName = ((RequestParam) a).value();
                    if (!"".equals(paramName.trim())) {
                        paramIndexMapping.put(paramName, i);
                    }
                }
            }
        }

        //提取方法中的request和response参数
        Class<?>[] paramsTypes = handlerMapping.getMethod().getParameterTypes();
        for (int i = 0; i < paramsTypes.length; i++) {
            Class<?> type = paramsTypes[i];
            if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
                paramIndexMapping.put(type.getName(), i);
            }
        }

        //获得方法的形参列表
        Map<String, String[]> params = request.getParameterMap();

        //controller的方法实参列表
        Object[] paramValues = new Object[paramsTypes.length];

        for (Map.Entry<String, String[]> parm : params.entrySet()) {
            String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]", "")
                    .replaceAll("\\s", ",");

            if (!paramIndexMapping.containsKey(parm.getKey())) {
                continue;
            }

            int index = paramIndexMapping.get(parm.getKey());
            paramValues[index] = parseStringValue(value, paramsTypes[index]);
        }

        //填充HttpServletRequest参数
        if (paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
            int reqIndex = paramIndexMapping.get(HttpServletRequest.class.getName());
            paramValues[reqIndex] = request;
        }

        //填充HttpServletResponse参数
        if (paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
            int respIndex = paramIndexMapping.get(HttpServletResponse.class.getName());
            paramValues[respIndex] = response;
        }

        //反射调用controller的方法
        Object result = handlerMapping.getMethod().invoke(handlerMapping.getController(), paramValues);
        if (result == null || result instanceof Void) {
            return null;
        }

        //解析controller的方法返回
        Class<?> returnType = handlerMapping.getMethod().getReturnType();
        boolean isModelAndView = returnType == ModelAndView.class;
        if (isModelAndView) {
            return (ModelAndView) result;
        } else if (returnType == Void.class) {
            return null;
        } else if (returnType == String.class) {
            //return (String) result;
        }

        return null;
    }

    /**
     * request中接收的参数都是string类型的,需要转换为controller中实际的参数类型
     * 暂时只支持string、int、double类型
     */
    private Object parseStringValue(String value, Class<?> paramsType) {
        if (String.class == paramsType) {
            return value;
        }

        if (Integer.class == paramsType) {
            return Integer.valueOf(value);
        } else if (Double.class == paramsType) {
            return Double.valueOf(value);
        } else {
            if (value != null) {
                return value;
            }
            return null;
        }
        //还有,继续加if
        //其他类型在这里暂时不实现,希望小伙伴自己来实现
    }
}

ModelAndViewController方法返回的类型,封装了模板引擎名称和参数

package com.lqb.springframework.webmvc.servlet;

import java.util.Map;

public class ModelAndView {

    //模板名字
    private String viewName;

    //模板中填充的参数
    private Map<String, ?> model;

    public ModelAndView(String viewName) {
        this.viewName = viewName;
    }

    public ModelAndView(String viewName, Map<String, ?> model) {
        this.viewName = viewName;
        this.model = model;
    }

    public String getViewName() {
        return viewName;
    }

    public Map<String, ?> getModel() {
        return model;
    }
}

填充创建HandlerAdapter逻辑到DispatcherServlet的流程中

private Map<HandlerMapping, HandlerAdapter> handlerAdapters = new HashMap<>();

private void initHandlerAdapters(DefaultApplicationContext context) {
    //一个HandlerMapping对应一个HandlerAdapter
    for (HandlerMapping handlerMapping : this.handlerMappings) {
        this.handlerAdapters.put(handlerMapping, new HandlerAdapter());
    }
}

ViewResolver

ViewResolver需要根据模板名找到对应的模板,封装成View

package com.lqb.springframework.webmvc.servlet;

import java.io.File;
import java.util.Locale;

public class ViewResolver {
    
    private final String DEFAULT_TEMPLATE_SUFFIX = ".html";

    /**模板根目录*/
    private File templateRootDir;

    public ViewResolver(String templateRoot) {
        String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
        templateRootDir = new File(templateRootPath);
    }

    public View resolveViewName(String viewName, Locale locale) throws Exception{
        if(null == viewName || "".equals(viewName.trim())){return null;}
        viewName = viewName.endsWith(DEFAULT_TEMPLATE_SUFFIX) ? viewName : (viewName + DEFAULT_TEMPLATE_SUFFIX);
        File templateFile = new File((templateRootDir.getPath() + "/" + viewName).replaceAll("/+","/"));
        return new View(templateFile);
    }
}

View封装了模板,提供了渲染的功能

package com.lqb.springframework.webmvc.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.RandomAccessFile;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class View {

    /**模板*/
    private File viewFile;

    /**占位符表达式*/
    private Pattern pattern = Pattern.compile("#\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);

    public View(File viewFile) {
        this.viewFile = viewFile;
    }

    /**
     * 渲染
     */
    public void render(Map<String, ?> model,
                       HttpServletRequest request,
                       HttpServletResponse response) throws Exception {

        StringBuilder sb = new StringBuilder();
        RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r");
        String line;

        while (null != (line = ra.readLine())) {
            line = new String(line.getBytes("ISO-8859-1"), "utf-8");
            Matcher matcher = this.pattern.matcher(line);
            //找到下一个占位符
            while (matcher.find()) {
                String paramName = matcher.group();
                paramName = paramName.replaceAll("#\\{|\\}", "");
                Object paramValue = model.get(paramName);
                if (null == paramValue) {
                    continue;
                }
                //替换占位符为实际值
                line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
                //接着匹配下一个占位符
                matcher = pattern.matcher(line);
            }
            sb.append(line);
        }

        response.setCharacterEncoding("utf-8");
        //输出到response
        response.getWriter().write(sb.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("&", "\\&");
    }
}

最后填充初始化ViewResolver的逻辑到DispatcherServlet的中

private List<ViewResolver> viewResolvers = new ArrayList<>();

private void initViewResolvers(DefaultApplicationContext context) {
    //配置文件中拿到模板的存放目录
    String templateRoot = context.getConfig().getProperty("templateRoot");
    String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
    File templateRootDir = new File(templateRootPath);
    String[] templates = templateRootDir.list();
    for (int i = 0; i < templates.length; i ++) {
        this.viewResolvers.add(new ViewResolver(templateRoot));
    }
}

至此组件都初始化好了,接着利用这些组件来处理用户请求了。

doDispatch

DispatcherServlet中,doGetdoPost需要调用doDispatch来处理用户请求

@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 {
    try {
        this.doDispatch(req, resp);
    } catch (Exception e) {
        resp.getWriter().write("500 Exception,Details:\r\n"
                + Arrays.toString(e.getStackTrace()).replaceAll("\\[|\\]", "")
                .replaceAll(",\\s", "\r\n"));
        e.printStackTrace();
    }
}

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    //1、通过从request中拿到URL,去匹配一个HandlerMapping
    HandlerMapping handler = getHandler(req);

    if (handler == null) {
        //没有找到handler返回404
        processDispatchResult(req, resp, new ModelAndView("404"));
        return;
    }

    //2、准备调用前的参数
    HandlerAdapter ha = getHandlerAdapter(handler);

    //3、真正的调用controller的方法
    ModelAndView mv = ha.handle(req, resp, handler);

    //4、渲染页面输出
    processDispatchResult(req, resp, mv);
}

第一步通过用户访问的URL,拿到HandlerMapping

private HandlerMapping getHandler(HttpServletRequest req) throws Exception {
    if (this.handlerMappings.isEmpty()) {
        return null;
    }

    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replace(contextPath, "").replaceAll("/+", "/");

    for (HandlerMapping handler : this.handlerMappings) {
        try {
            Matcher matcher = handler.getPattern().matcher(url);
            //如果没有匹配上继续下一个匹配
            if (!matcher.matches()) {
                continue;
            }
            return handler;
        } catch (Exception e) {
            throw e;
        }
    }
    return null;
}

第二步,根据HandlerMapping拿到HandlerAdapter

private HandlerAdapter getHandlerAdapter(HandlerMapping handler) {
    if (this.handlerAdapters.isEmpty()) {
        return null;
    }
    HandlerAdapter ha = this.handlerAdapters.get(handler);
    return ha;
}

最后,调用完Controller方法渲染模板输出到用户

private void processDispatchResult(HttpServletRequest req, HttpServletResponse resp, ModelAndView mv) throws Exception {
    if (null == mv) {
        return;
    }

    if (this.viewResolvers.isEmpty()) {
        return;
    }

    for (ViewResolver viewResolver : this.viewResolvers) {
        //根据模板名拿到View
        View view = viewResolver.resolveViewName(mv.getViewName(), null);
        //开始渲染
        view.render(mv.getModel(), req, resp);
        return;
    }
}

至此,MVC模块已经搞定,下面来展示成果。

成果展示

mvn install发布到本地仓库,然后在src/main/webapp/WEB-INF目录下新建web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:javaee="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
         version="2.4">

    <display-name>My Web Application</display-name>

    <servlet>
        <servlet-name>mymvc</servlet-name>
        <!--配置Servlet-->
        <servlet-class>com.lqb.springframework.webmvc.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!--指定配置文件路径-->
            <param-value>application.properties</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>mymvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

配置文件application.properties中指定模板位置

# 模板目录
templateRoot=layouts

在resource目录下新建layouts文件夹,然后我们创建一个叫test.html的模板,包含两个占位符“data1”和“data2”

<!DOCTYPE html>
<html lang="zh-cn">
	<head>
		<meta charset="utf-8">
		<title>手写一个Spring</title>
	</head>
	<center>
		<h1>#{data1}大家好,我是#{data2}</h1>
	</center>
</html>

再创建一个叫404的模板

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <title>页面去火星了</title>
</head>
<body>
    <font size='25' color='red'>404 Not Found</font><br/>
</body>
</html>

创建一个Controller

package com.lqb.demo;

import com.lqb.springframework.annotation.Controller;
import com.lqb.springframework.annotation.RequestMapping;
import com.lqb.springframework.webmvc.servlet.ModelAndView;
import java.util.HashMap;

@Controller
public class HelloController {

    @RequestMapping("/hello")
    public ModelAndView hello() {
        HashMap<String, Object> model = new HashMap<>();
        model.put("data1", "hello");
        model.put("data2", "world");
        return new ModelAndView("test", model);
    }
}

因为我们是一个Web项目,因此需要启动Web Server,我这里是启动了Jetty
启动jetty
打开浏览器,地址栏输入http://localhost:8080/hello,成功输入模板引擎的内容
在这里插入图片描述

最后

《手撸一个简易Spring框架》系列正式结束了。通过手写简易Spring我们应该要掌握Spring的IOC启动流程、DI依赖注入以及AOP代理创建过程,另外就是涉及到的设计模式,如AOP中的责任链。

Github源码

系列:
手撸一个简易Spring框架(一)
手撸一个简易Spring框架(二)
手撸一个简易Spring框架(三)
手撸一个简易Spring框架(四)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值