Spring 框架对于 Java 后端程序员来说再熟悉不过了, 以前只知道它用的反射实现的, 但了解之后才知道有很多巧妙的设计在里面. 如果不看 Spring 的源码, 你将会失去一次和大师学习的机会: 它的代码规范, 设计思想很值得学习.
我们程序员大部分人都是野路子, 不懂什么叫代码规范. 写了一个月的代码, 最后还得其他老司机花 3 天时间重构, 相信大部分老司机都很头疼看新手的代码.
废话不多说, 我们进入今天的正题, 在 Web 应用程序设计中, MVC 模式已经被广泛使用. SpringMVC 以 DispatcherServlet 为核心, 负责协调和组织不同组件以完成请求处理并返回响应的工作, 实现了 MVC 模式. 点击这里学习 Spring MVC 常用注解.
想要实现自己的 SpringMVC 框架, 需要从以下几点入手:
了解 SpringMVC 运行流程及九大组件
梳理自己的 SpringMVC 的设计思路
实现自己的 SpringMVC 框架
一, 了解 SpringMVC 运行流程及九大组件
1,SpringMVC 的运行流程
用户发送请求至前端控制器 DispatcherServlet
DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器.
处理器映射器根据请求 url 找到具体的处理器, 生成处理器对象及处理器拦截器 (如果有则生成) 一并返回给 DispatcherServlet.
DispatcherServlet 通过 HandlerAdapter 处理器适配器调用处理器
执行处理器(Controller, 也叫后端控制器).
Controller 执行完成返回 ModelAndView
HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet
DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器
ViewReslover 解析后返回具体 View
DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中).
DispatcherServlet 响应用户. 从上面可以看出, DispatcherServlet 有接收请求, 响应结果, 转发等作用. 有了 DispatcherServlet 之后, 可以减少组件之间的耦合度.
2,SpringMVC 的九大组件
- protected void initStrategies(ApplicationContext context) {
- // 用于处理上传请求. 处理方法是将普通的 request 包装成 MultipartHttpServletRequest, 后者可以直接调用 getFile 方法获取 File.
- initMultipartResolver(context);
- //SpringMVC 主要有两个地方用到了 Locale: 一是 ViewResolver 视图解析的时候; 二是用到国际化资源或者主题的时候.
- initLocaleResolver(context);
- // 用于解析主题. SpringMVC 中一个主题对应 一个 properties 文件, 里面存放着跟当前主题相关的所有资源,// 如图片, css 样式等. SpringMVC 的主题也支持国际化,
- initThemeResolver(context);
- // 用来查找 Handler 的.
- initHandlerMappings(context);
- // 从名字上看, 它就是一个适配器. Servlet 需要的处理方法的结构却是固定的, 都是以 request 和 response 为参数的方法.// 如何让固定的 Servlet 处理方法调用灵活的 Handler 来进行处理呢? 这就是 HandlerAdapter 要做的事情
- initHandlerAdapters(context);
- // 其它组件都是用来干活的. 在干活的过程中难免会出现问题, 出问题后怎么办呢?// 这就需要有一个专门的角色对异常情况进行处理, 在 SpringMVC 中就是 HandlerExceptionResolver.
- initHandlerExceptionResolvers(context);
- // 有的 Handler 处理完后并没有设置 View 也没有设置 ViewName, 这时就需要从 request 获取 ViewName 了,// 如何从 request 中获取 ViewName 就是 RequestToViewNameTranslator 要做的事情了.
- initRequestToViewNameTranslator(context);
- //ViewResolver 用来将 String 类型的视图名和 Locale 解析为 View 类型的视图.//View 是用来渲染页面的, 也就是将程序返回的参数填入模板里, 生成 html(也可能是其它类型)文件.
- initViewResolvers(context);
- // 用来管理 FlashMap 的, FlashMap 主要用在 redirect 重定向中传递参数.
- initFlashMapManager(context);
- }
二, 梳理 SpringMVC 的设计思路
本文只实现自己的 @Controller,@RequestMapping,@RequestParam 注解起作用, 其余 SpringMVC 功能读者可以尝试自己实现.
1, 读取配置
从图中可以看出, SpringMVC 本质上是一个 Servlet, 这个 Servlet 继承自 HttpServlet.FrameworkServlet 负责初始化 SpringMVC 的容器, 并将 Spring 容器设置为父容器. 因为本文只是实现 SpringMVC, 对于 Spring 容器不做过多讲解. 点击这里学习 Spring MVC 常用注解.
为了读取 web.xml 中的配置, 我们用到 ServletConfig 这个类, 它代表当前 Servlet 在 web.xml 中的配置信息. 通过 web.xml 中加载我们自己写的 MyDispatcherServlet 和读取配置文件.
2, 初始化阶段
在前面我们提到 DispatcherServlet 的 initStrategies 方法会初始化 9 大组件, 但是这里将实现一些 SpringMVC 的最基本的组件而不是全部, 按顺序包括:
加载配置文件
扫描用户配置包下面所有的类
拿到扫描到的类, 通过反射机制, 实例化. 并且放到 ioc 容器中(Map 的键值对 beanName-bean) beanName 默认是首字母小写
初始化 HandlerMapping, 这里其实就是把 url 和 method 对应起来放在一个 k-v 的 Map 中, 在运行阶段取出
3, 运行阶段
每一次请求将会调用 doGet 或 doPost 方法, 所以统一运行阶段都放在 doDispatch 方法里处理, 它会根据 url 请求去 HandlerMapping 中匹配到对应的 Method, 然后利用反射机制调用 Controller 中的 url 对应的方法, 并得到结果返回. 按顺序包括以下功能:
异常的拦截
获取请求传入的参数并处理参数
通过初始化好的 handlerMapping 中拿出 url 对应的方法名, 反射调用.
三, 实现自己的 SpringMVC 框架
工程文件及目录:
首先, 新建一个 maven 项目, 在 pom.xml 中导入以下依赖:
- 4.0.0
- com.liugh
- liughMVC
- 0.0.1-SNAPSHOT
- war
- UTF- 8
- 1.8
- 1.8
- 1.8
- javax.servlet
- javax.servlet-api
- 3.0.1
- provided
接着, 我们在 WEB-INF 下创建一个 web.xml, 如下配置:
- xmlns="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/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
- version="3.0">
- MySpringMVC
- com.liugh.servlet.MyDispatcherServlet
- contextConfigLocation
- application.properties
- 1
- MySpringMVC
- /*
application.properties 文件中只是配置要扫描的包到 SpringMVC 容器中.
scanPackage=com.liugh.core
创建自己的 Controller 注解, 它只能标注在类上面:
- package com.liugh.annotation;
- import java.lang.annotation.Documented;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface MyController {
- /**
- * 表示给 controller 注册别名
- * @return
- */
- String value() default "";
- }
RequestMapping 注解, 可以在类和方法上:
RequestParam 注解, 只能注解在参数上
- package com.liugh.annotation;
- import java.lang.annotation.Documented;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- @Target(ElementType.PARAMETER)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface MyRequestParam {
- /**
- * 表示参数的别名, 必填
- * @return
- */
- String value();
- }
然后创建 MyDispatcherServlet 这个类, 去继承 HttpServlet, 重写 init 方法, doGet,doPost 方法, 以及加上我们第二步分析时要实现的功能:
- package com.liugh.servlet;
- import java.io.File;
- import java.io.IOException;
- import java.io.InputStream;
- import java.lang.reflect.Method;
- import java.net.URL;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Map.Entry;
- import java.util.Properties;
- import javax.servlet.ServletConfig;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import com.liugh.annotation.MyController;
- import com.liugh.annotation.MyRequestMapping;
- public class MyDispatcherServlet extends HttpServlet{
- private Properties properties = new Properties();
- private List classNames = new ArrayList<>();
- private Map ioc = new HashMap<>();
- private Map handlerMapping = new HashMap<>();
- private Map controllerMap =new HashMap<>();
- @Override
- public void init(ServletConfig config) throws ServletException {
- //1. 加载配置文件
- doLoadConfig(config.getInitParameter("contextConfigLocation"));
- //2. 初始化所有相关联的类, 扫描用户设定的包下面所有的类
- doScanner(properties.getProperty("scanPackage"));
- //3. 拿到扫描到的类, 通过反射机制, 实例化, 并且放到 ioc 容器中(k-v beanName-bean) beanName 默认是首字母小写
- doInstance();
- //4. 初始化 HandlerMapping(将 url 和 method 对应上)
- initHandlerMapping();
- }
- @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 {
- // 处理请求
- doDispatch(req,resp);
- } catch (Exception e) {
- resp.getWriter().write("500!! Server Exception");
- }
- }
- private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
- if(handlerMapping.isEmpty()){
- return;
- }
- String url =req.getRequestURI();
- String contextPath = req.getContextPath();
- url=url.replace(contextPath, "").replaceAll("/+","/");
- if(!this.handlerMapping.containsKey(url)){
- resp.getWriter().write("404 NOT FOUND!");
- return;
- }
- Method method =this.handlerMapping.get(url);
- // 获取方法的参数列表
- Class[] parameterTypes = method.getParameterTypes();
- // 获取请求的参数
- Map parameterMap = req.getParameterMap();
- // 保存参数值
- Object [] paramValues= new Object[parameterTypes.length];
- // 方法的参数列表
- for (int i = 0; i
- // 根据参数名称, 做某些处理
- String requestParam = parameterTypes[i].getSimpleName();
- if (requestParam.equals("HttpServletRequest")){
- // 参数类型已明确, 这边强转类型
- paramValues[i]=req;
- continue;
- }
- if (requestParam.equals("HttpServletResponse")){
- paramValues[i]=resp;
- continue;
- }
- if(requestParam.equals("String")){
- for (Entry param : parameterMap.entrySet()) {
- String value =Arrays.toString(param.getValue()).replaceAll("\[|\]", "").replaceAll(",\s",",");
- paramValues[i]=value;
- }
- }
- }
- // 利用反射机制来调用
- try {
- method.invoke(this.controllerMap.get(url), paramValues);// 第一个参数是 method 所对应的实例 在 ioc 容器中
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- private void doLoadConfig(String location){
- // 把 web.xml 中的 contextConfigLocation 对应 value 值的文件加载到流里面
- InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(location);
- try {
- // 用 Properties 文件加载文件里的内容
- properties.load(resourceAsStream);
- } catch (IOException e) {
- e.printStackTrace();
- }finally {
- // 关流
- if(null!=resourceAsStream){
- try {
- resourceAsStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- private void doScanner(String packageName) {
- // 把所有的. 替换成 /
- URL url =this.getClass().getClassLoader().getResource("/"+packageName.replaceAll("\.", "/"));
- File dir = new File(url.getFile());
- for (File file : dir.listFiles()) {
- if(file.isDirectory()){
- // 递归读取包
- doScanner(packageName+"."+file.getName());
- }else{
- String className =packageName +"." +file.getName().replace(".class", "");
- classNames.add(className);
- }
- }
- }
- private void doInstance() {
- if (classNames.isEmpty()) {
- return;
- }
- for (String className : classNames) {
- try {
- // 把类搞出来, 反射来实例化(只有加 @MyController 需要实例化)
- Class clazz =Class.forName(className);
- if(clazz.isAnnotationPresent(MyController.class)){
- ioc.put(toLowerFirstWord(clazz.getSimpleName()),clazz.newInstance());
- }else{
- continue;
- }
- } catch (Exception e) {
- e.printStackTrace();
- continue;
- }
- }
- }
- private void initHandlerMapping(){
- if(ioc.isEmpty()){
- return;
- }
- try {
- for (Entry entry: ioc.entrySet()) {
- Class clazz = entry.getValue().getClass();
- if(!clazz.isAnnotationPresent(MyController.class)){
- continue;
- }
- // 拼 url 时, 是 controller 头的 url 拼上方法上的 url
- String baseUrl ="";
- if(clazz.isAnnotationPresent(MyRequestMapping.class)){
- MyRequestMapping annotation = clazz.getAnnotation(MyRequestMapping.class);
- baseUrl=annotation.value();
- }
- Method[] methods = clazz.getMethods();
- for (Method method : methods) {
- if(!method.isAnnotationPresent(MyRequestMapping.class)){
- continue;
- }
- MyRequestMapping annotation = method.getAnnotation(MyRequestMapping.class);
- String url = annotation.value();
- url =(baseUrl+"/"+url).replaceAll("/+", "/");
- handlerMapping.put(url,method);
- controllerMap.put(url,clazz.newInstance());
- System.out.println(url+","+method);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- /**
- * 把字符串的首字母小写
- * @param name
- * @return
- */
- private String toLowerFirstWord(String name){
- char[] charArray = name.toCharArray();
- charArray[0] += 32;
- return String.valueOf(charArray);
- }
- }
这里我们就开发完了自己的 SpringMVC, 现在我们测试一下:
- package com.liugh.core.controller;
- import java.io.IOException;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import com.liugh.annotation.MyController;
- import com.liugh.annotation.MyRequestMapping;
- import com.liugh.annotation.MyRequestParam;
- @MyController
- @MyRequestMapping("/test")
- public class TestController {
- @MyRequestMapping("/doTest")
- public void test1(HttpServletRequest request, HttpServletResponse response,
- @MyRequestParam("param") String param){
- System.out.println(param);
- try {
- response.getWriter().write( "doTest method success! param:"+param);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- @MyRequestMapping("/doTest2")
- public void test2(HttpServletRequest request, HttpServletResponse response){
- try {
- response.getWriter().println("doTest2 method success!");
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
访问
http://localhost:8080/liughMVC/test/doTest?param=liugh 如下:
访问一个不存在的试试:
到这里我们就大功告成了! 水平有限, 文章难免有错误, 欢迎牺牲自己宝贵时间的读者, 就本文内容直抒己见, 我的目的仅仅是希望对读者有所帮助.
来源: http://developer.51cto.com/art/201808/581288.htm