专栏首页编程之禅18.手写Spring MVC

18.手写Spring MVC

1.Spring MVC顶层设计

2.Spring MVC执行流程

下面是代码实现

3.GPdispatcherServelet 分发器

Serviet 的生命周期由 init()到 service()再到 destory()组成, destory()方法我们 不做实现。

package com.gupaoedu.vip.spring.framework.webmvc.servlet;


import com.gupaoedu.vip.spring.framework.annotation.*;
import com.gupaoedu.vip.spring.framework.context.GPApplicationContext;

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.IOException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.io.File;

/**
 * 委派模式
 * 职责:负责任务调度,请求分发
 */
public class GPDispatcherServlet extends HttpServlet {
    private GPApplicationContext applicationContext;

    private List<GPHandlerMapping> handlerMappings = new ArrayList<GPHandlerMapping>();

    private Map<GPHandlerMapping,GPHandlerAdapter> handlerAdapters = new HashMap<GPHandlerMapping, GPHandlerAdapter>();

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

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

        //6、委派,根据URL去找到一个对应的Method并通过response返回
        try {
            doDispatch(req,resp);
        } catch (Exception e) {
            try {
                processDispatchResult(req,resp,new GPModelAndView("500"));
            } catch (Exception e1) {
                e1.printStackTrace();
                resp.getWriter().write("500 Exception,Detail : " + Arrays.toString(e.getStackTrace()));
            }
        }
    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        //完成了对HandlerMapping的封装
        //完成了对方法返回值的封装ModelAndView
        //1、通过URL获得一个HandlerMapping
        GPHandlerMapping handler = getHandler(req);
        if(handler == null){
            processDispatchResult(req,resp,new GPModelAndView("404"));
            return;
        }
        //2、根据一个HandlerMaping获得一个HandlerAdapter
        GPHandlerAdapter ha = getHandlerAdapter(handler);
        //3、解析某一个方法的形参和返回值之后,统一封装为ModelAndView对象
        GPModelAndView mv = ha.handler(req,resp,handler);
        // 就把ModelAndView变成一个ViewResolver
        processDispatchResult(req,resp,mv);
    }

    private GPHandlerAdapter getHandlerAdapter(GPHandlerMapping handler) {
        if(this.handlerAdapters.isEmpty()){return null;}
        return this.handlerAdapters.get(handler);
    }

    private void processDispatchResult(HttpServletRequest req, HttpServletResponse resp, GPModelAndView mv) throws Exception {
        if(null == mv){return;}
        if(this.viewResolvers.isEmpty()){return;}

        for (GPViewResolver viewResolver : this.viewResolvers) {
            GPView view = viewResolver.resolveViewName(mv.getViewName());
            //直接往浏览器输出
            view.render(mv.getModel(),req,resp);
            return;
        }
    }

    private GPHandlerMapping getHandler(HttpServletRequest req) {
        if(this.handlerMappings.isEmpty()){return  null;}
        String url = req.getRequestURI();
        String contextPath = req.getContextPath();
        url = url.replaceAll(contextPath,"").replaceAll("/+","/");

        for (GPHandlerMapping mapping : handlerMappings) {
            Matcher matcher = mapping.getPattern().matcher(url);
            if(!matcher.matches()){continue;}
            return mapping;
        }
        return null;
    }

    @Override
    public void init(ServletConfig config) throws ServletException {

        //初始化Spring核心IoC容器
        applicationContext = new GPApplicationContext(config.getInitParameter("contextConfigLocation"));

        //完成了IoC、DI和MVC部分对接

        //初始化九大组件
        initStrategies(applicationContext);

        System.out.println("GP Spring framework is init.");
    }

    private void initStrategies(GPApplicationContext context) {
//        //多文件上传的组件
//        initMultipartResolver(context);
//        //初始化本地语言环境
//        initLocaleResolver(context);
//        //初始化模板处理器
//        initThemeResolver(context);
        //handlerMapping
        initHandlerMappings(context);
        //初始化参数适配器
        initHandlerAdapters(context);
//        //初始化异常拦截器
//        initHandlerExceptionResolvers(context);
//        //初始化视图预处理器
//        initRequestToViewNameTranslator(context);
        //初始化视图转换器
        initViewResolvers(context);
//        //FlashMap管理器
//        initFlashMapManager(context);
    }

    private void initViewResolvers(GPApplicationContext context) {
        String templateRoot = context.getConfig().getProperty("templateRoot");
        String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();

        File templateRootDir = new File(templateRootPath);
        for (File file : templateRootDir.listFiles()) {
            this.viewResolvers.add(new GPViewResolver(templateRoot));
        }

    }

    private void initHandlerAdapters(GPApplicationContext context) {
        for (GPHandlerMapping handlerMapping : handlerMappings) {
            this.handlerAdapters.put(handlerMapping,new GPHandlerAdapter());
        }
    }

    private void initHandlerMappings(GPApplicationContext context) {
        if(this.applicationContext.getBeanDefinitionCount() == 0){ return;}

        for (String beanName : this.applicationContext.getBeanDefinitionNames()) {
            Object instance = applicationContext.getBean(beanName);
            Class<?> clazz = instance.getClass();

            if(!clazz.isAnnotationPresent(GPController.class)){ continue; }

            //相当于提取 class上配置的url
            String baseUrl = "";
            if(clazz.isAnnotationPresent(GPRequestMapping.class)){
                GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
                baseUrl = requestMapping.value();
            }

            //只获取public的方法
            for (Method method : clazz.getMethods()) {
                if(!method.isAnnotationPresent(GPRequestMapping.class)){continue;}
                //提取每个方法上面配置的url
                GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);

                // //demo//query
                String regex = ("/" + baseUrl + "/" + requestMapping.value().replaceAll("\\*",".*")).replaceAll("/+","/");
                Pattern pattern = Pattern.compile(regex);
                //handlerMapping.put(url,method);
                handlerMappings.add(new GPHandlerMapping(pattern,instance,method));
                System.out.println("Mapped : " + regex + "," + method);
            }

        }
    }
}

我们 只实现了九大组件中的 三大 核 心 组件 的 基本功能,分 别 是 HandlerMapping 、 HandlerAdapter 、 ViewResolver , 完 成 MVC 最核 心的 调度功能。

  • 其中 HandlerMapping 就是策略模式的应用,用输入 URL 间接调用不同的 Method达到获取结果的目 的。
  • 顾名思义, HandlerAdapter 应用的是适配器模式,将 Request 的字符型参数自动适配为 Method 的 Java 实参, 主要实现参数列表自动适配和类型转换功能。
  • ViewResolver 也算一种策略,根据不 同的请求选择不同的模板引擎来进行页面的渲染。

4.GPHandlerMapping 映射器

public class GPHandlerMapping {
    private Pattern pattern;     //URL
    private Method method;  //对应的Method
    private Object controller;//Method对应的实例对象

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

    public Pattern getPattern() {
        return pattern;
    }

    public void setPattern(Pattern pattern) {
        this.pattern = pattern;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Object getController() {
        return controller;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }
}

5.GPHandlerAdapter 适配器

public class GPHandlerAdapter {

    public GPModelAndView handler(HttpServletRequest req, HttpServletResponse resp, GPHandlerMapping handler) throws Exception{

        //保存形参列表
        //将参数名称和参数的位置,这种关系保存起来
        Map<String,Integer> paramIndexMapping = new HashMap<String, Integer>();

        //通过运行时的状态去拿到你
        Annotation[] [] pa = handler.getMethod().getParameterAnnotations();
        for (int i = 0; i < pa.length ; i ++) {
            for(Annotation a : pa[i]){
                if(a instanceof GPRequestParam){
                    String paramName = ((GPRequestParam) a).value();
                    if(!"".equals(paramName.trim())){
//                        String value = Arrays.toString(params.get(paramName))
//                                .replaceAll("\\[|\\]","")
//                                .replaceAll("\\s+",",");
//                        paramValues[i] = value;
                        paramIndexMapping.put(paramName,i);
                    }
                }
            }
        }

        //初始化一下
        Class<?> [] paramTypes = handler.getMethod().getParameterTypes();
        for (int i = 0; i < paramTypes.length; i++) {
            Class<?> paramterType = paramTypes[i];
            if(paramterType == HttpServletRequest.class || paramterType == HttpServletResponse.class){
                paramIndexMapping.put(paramterType.getName(),i);
            }
        }


        //去拼接实参列表
        //http://localhost/web/query?name=Tom&Cat
        Map<String,String[]> params = req.getParameterMap();

        Object [] paramValues = new Object[paramTypes.length];

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

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

            int index = paramIndexMapping.get(param.getKey());

            //允许自定义的类型转换器Converter
            paramValues[index] = castStringValue(value,paramTypes[index]);
        }

        if(paramIndexMapping.containsKey(HttpServletRequest.class.getName())){
            int index = paramIndexMapping.get(HttpServletRequest.class.getName());
            paramValues[index] = req;
        }

        if(paramIndexMapping.containsKey(HttpServletResponse.class.getName())){
            int index = paramIndexMapping.get(HttpServletResponse.class.getName());
            paramValues[index] = resp;
        }

        Object result = handler.getMethod().invoke(handler.getController(),paramValues);
        if(result == null || result instanceof Void){return null;}

        boolean isModelAndView = handler.getMethod().getReturnType() == GPModelAndView.class;
        if(isModelAndView){
            return (GPModelAndView)result;
        }
        return null;
    }

    private Object castStringValue(String value, Class<?> paramType) {
        if(String.class == paramType){
            return value;
        }else if(Integer.class == paramType){
            return Integer.valueOf(value);
        }else if(Double.class == paramType){
            return Double.valueOf(value);
        }else {
            if(value != null){
                return value;
            }
            return null;
        }

    }
}

6.GPModelAndView 视图组件

public class GPModelAndView {
    private String viewName;
    private Map<String,?> model;

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

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

    public String getViewName() {
        return viewName;
    }

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

7.GPViewResolver 视图解析器

通过在 Serlvet 中调用 resolveViewName()方法来获得模板所对应的 View。

public class GPViewResolver {
    private final String DEFAULT_TEMPLATE_SUFFIX = ".html";
    private File tempateRootDir;
    public GPViewResolver(String templateRoot) {
        String templateRootPath = this.getClass().getClassLoader().getResource(templateRoot).getFile();
        tempateRootDir = new File(templateRootPath);
    }

    public GPView resolveViewName(String viewName){
        if(null == viewName || "".equals(viewName.trim())){return null;}
        viewName = viewName.endsWith(DEFAULT_TEMPLATE_SUFFIX)? viewName : (viewName + DEFAULT_TEMPLATE_SUFFIX);
        File templateFile = new File((tempateRootDir.getPath() + "/" + viewName).replaceAll("/+","/"));
        return new GPView(templateFile);
    }
}

8.GPView 自定义模板引擎

这里的 GPView 就是前面所说的自定义模板解析引擎,其核心方法是 render()。 在 render()方 法中完成对模板的渲染,最终返回浏览器能识别的字符串,通过 Response 输出。

public class GPView {

    private File viewFile;
    public GPView(File templateFile) {
        this.viewFile = templateFile;
    }

    public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse resp) throws Exception {
        StringBuffer sb = new StringBuffer();
        RandomAccessFile ra = new RandomAccessFile(this.viewFile,"r");

        String line = null;
        while (null != (line = ra.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();
                paramName = paramName.replaceAll("¥\\{|\\}","");
                Object paramValue = model.get(paramName);
                line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
                matcher = pattern.matcher(line);
            }
            sb.append(line);
        }
        resp.setCharacterEncoding("utf-8");
        resp.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("&", "\\&");
    }
}

下面实现 Service 业务代码

9.IQueryService 查询接口

/**
 * 查询业务
 */
public interface IQueryService {
   
   /**
    * 查询
    */
   public String query(String name);
}

10.QueryService 查询实现

/**
 * 查询业务
 */
@GPService
@Slf4j
public class QueryService implements IQueryService {

   /**
    * 查询
    */
   public String query(String name) {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      String time = sdf.format(new Date());
      String json = "{name:\"" + name + "\",time:\"" + time + "\"}";
      log.info("这是在业务方法中打印的:" + json);
      return json;
   }
}

11.IModifyService 增删改接口

/**
 * 增删改业务
 * @author Tom
 *
 */
public interface IModifyService {

   /**
    * 增加
    */
   public String add(String name, String addr);
   
   /**
    * 修改
    */
   public String edit(Integer id, String name);
   
   /**
    * 删除
    */
   public String remove(Integer id);
}

12.ModifyService 增、删、改实现

/**
 * 增删改业务
 */
@GPService
public class ModifyService implements IModifyService {

   /**
    * 增加
    */
   public String add(String name,String addr) {
      return "modifyService add,name=" + name + ",addr=" + addr;
   }

   /**
    * 修改
    */
   public String edit(Integer id,String name) {
      return "modifyService edit,id=" + id + ",name=" + name;
   }

   /**
    * 删除
    */
   public String remove(Integer id) {
      return "modifyService id=" + id;
   }
}

13. MyAction URL暴露层

/**
 * 公布接口url
 */
@GPController
@GPRequestMapping("/web")
public class MyAction {

   @GPAutowired IQueryService queryService;
   @GPAutowired IModifyService modifyService;

   @GPRequestMapping("/query.json")
   public GPModelAndView query(HttpServletRequest request, HttpServletResponse response,
                        @GPRequestParam("name") String name){
      String result = queryService.query(name);
      return out(response,result);
   }
   
   @GPRequestMapping("/add*.json")
   public GPModelAndView add(HttpServletRequest request,HttpServletResponse response,
            @GPRequestParam("name") String name,@GPRequestParam("addr") String addr){
      String result = modifyService.add(name,addr);
      return out(response,result);
   }
   
   @GPRequestMapping("/remove.json")
   public GPModelAndView remove(HttpServletRequest request, HttpServletResponse response,
                         @GPRequestParam("id") Integer id){
      String result = modifyService.remove(id);
      return out(response,result);
   }
   
   @GPRequestMapping("/edit.json")
   public GPModelAndView edit(HttpServletRequest request,HttpServletResponse response,
         @GPRequestParam("id") Integer id,
         @GPRequestParam("name") String name){
      String result = modifyService.edit(id,name);
      return out(response,result);
   }
   
   private GPModelAndView out(HttpServletResponse resp,String str){
      try {
         resp.getWriter().write(str);
      } catch (IOException e) {
         e.printStackTrace();
      }
      return null;
   }
}

14.PageAction 页面逻辑处理

/**
 * 公布接口url
 */
@GPController
@GPRequestMapping("/")
public class PageAction {

    @GPAutowired
    IQueryService queryService;

    @GPRequestMapping("/first.html")
    public GPModelAndView query(@GPRequestParam("teacher") String teacher){
        String result = queryService.query(teacher);
        Map<String,Object> model = new HashMap<String,Object>();
        model.put("teacher", teacher);
        model.put("data", result);
        model.put("token", "123456");
        return new GPModelAndView("first.html",model);
    }
}

15.模板页面

为了更全面地演示页面渲染效果,分别定义了 first.html 对应 PageAction 中的 first.html 请求、 404.html 默认页和 500.html 异常默认页。

<!DOCTYPE html>
<html lang="zh-cn">
<head>
   <meta charset="utf-8">
   <title>咕泡学院SpringMVC模板引擎演示</title>
</head>
<center>
   <h1>大家好,我是¥{teacher}老师<br/>欢迎大家一起来探索Spring的世界</h1>
   <h3>Hello,My name is ¥{teacher}</h3>
   <div>¥{data}</div>
   Token值:¥{token}
</center>
</html>
<!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/><font color='green'><i>Copyright@GupaoEDU</i></font>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <title>服务器好像累了</title>
</head>
<body>
    <font size='25' color='blue'>500 服务器好像有点累了,需要休息一下</font><br/>
    <b>Message:¥{detail}</b><br/>
    <b>StackTrace:¥{stackTrace}</b><br/>
    <font color='green'><i>Copyright@GupaoEDU</i></font>
</body>
</html>

参考资料:

1.《spring 5核心原理与30个类手写实战》谭勇德著

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 四、原型模式与建造者模式详解

    原型模式(PrototypePattern)是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式。

    编程之心
  • 八、适配器模式与桥接模式详解

    适配器模式的英文翻译是 Adapter Design Pattern。顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼...

    编程之心
  • 17.用300行代码手写初体验Spring V1.0版本

    3、 全程手写实现SpringMVC的核心功能,从最简单的V1版本一步一步优化为V2版本,最后到V3版本。

    编程之心
  • Spring Boot 实现定时任务的动态增删启停等管理!

    在spring boot项目中,可以通过@EnableScheduling注解和@Scheduled注解实现定时任务,也可以通过SchedulingConfig...

    业余草
  • 020.原型模式

    我们今天来考虑一下给用户邮箱发广告信这个模块是怎么开发的。既然是广告信,肯定需要一个模版,然后再从数据库中把客户的信息一个一个的取出,放到模版中生成一份完整的邮...

    CoderJed
  • Spring Security项目第三方登陆(四)

    楠楠
  • Activiti开发案例之代码生成工作流图片

    小柒2012
  • 微信公众号开发消息推送以及图文推送

    (adsbygoogle =window.adsbygoogle ||[]).push({});

    猿码优创
  • 聊聊NacosNamingService的selectOneHealthyInstance

    本文主要研究一下NacosNamingService的selectOneHealthyInstance

    codecraft
  • 方法参数过多怎么办

    我们在编程或阅读前人的代码时,经常会看到多个参数的方法,有的甚至达到二十个,看得人眼花缭乱,不便于阅读和维护,而且参数很容易混淆,如两个参数类型同为short型...

    java达人

扫码关注云+社区

领取腾讯云代金券