说在前头: 笔者本人为大三在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正。若在阅读时有任何的问题,也可通过评论提出,本人将根据自身能力对问题进行一定的解答。
手撸Spring系列是笔者本人首次尝试的、较为规范的系列博客,将会围绕Spring框架分为IOC/DI 思想
、Spring MVC
、AOP 思想
、Spring JDBC
四个模块,并且每个模块都会分为理论篇
、源码篇
、实战篇
三个篇章进行讲解(大约12篇文章左右的篇幅)。从原理出发,深入浅出,一步步接触Spring源码并手把手带领大家一起写一个 迷你版的Spring框架 ,促进大家进一步了解Spring的本质!
由于源码篇涉及到源码的阅读,可能有小伙伴没有成功构建好Spring源码的阅读环境,笔者强烈建议:想要真正了解Spring,一定要构建好源码的阅读环境再进行研究,具体构建过程可查看笔者此前的博客:《如何构建Spring5源码阅读环境》
前言
经过前面Spring MVC 理论篇和源码篇的讲解,我们终于又再次迎来了实战篇。( 注意:Spring MVC 实战篇代码的运行建立在Spring IOC 实战篇中的代码基础之上!!,在阅读此篇博客之前,建议先将前面章节的博客浏览一遍~!! )
与Spring IOC 的实战篇源码一样,笔者也会开源到码云Gitee上,仓库地址如下:https://gitee.com/bosen-once/mini-spring
在正式编写代码之前,还是让各位读者朋友们先看一下整个程序的大致架构(如下)
一、编写前准备
由于我们是编写与web相关的框架代码,因此不能少了我们熟悉的老朋友Servlet
的加入(需要导入第三方的包),依赖如下:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
二、注解类的编写
和Spring IOC 实战篇一样,我们的编写工作还是先从最简单的注解类开始,Spring MVC 的注解类在IOC注解的基础上还要加上两个注解类(@RequestMapping
、@RequestParam
)
1.@RequestMapping
package org.springframework.annotation;
import java.lang.annotation.*;
/**
* <p>url映射注解</p>
* @author Bosen
* @date 2021/9/15 22:06
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
2.@RequestParam
package org.springframework.annotation;
import java.lang.annotation.*;
/**
* <p>参数注解</p>
* @author Bosen
* @date 2021/9/15 22:43
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
String value() default "";
}
三、核心类的编写
在Spring MVC 理论篇和源码篇中,我们得知了在Spring MVC 的核心类主要是前端控制器DispatcherServlet
、映射器HandlerMapping
、适配器HandlerAdapter
、视图解析器ViewResolver
、视图View
以及ModelAndView
,那么我们的编写任务就是完成上述几个类迷你版的实现!!
1.HandlerMapping
知识温故: HandlerMapping
是用于保存URL和Method的对应关系的映射器。
package org.springframework.web.servlet;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
/**
* <p>用于保存URL和Method的对应关系</p>
* @author Bosen
* @date 2021/9/15 11:24
*/
public class HandlerMapping {
/**
* <p>对应的controller对象</p>
*/
private Object controller;
/**
* <p>对应的方法</p>
*/
private Method method;
/**
* <p>URL的封装</p>
*/
private Pattern pattern;
public HandlerMapping(Object controller, Method method, Pattern pattern) {
this.controller = controller;
this.method = method;
this.pattern = pattern;
}
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;
}
public Pattern getPattern() {
return pattern;
}
public void setPattern(Pattern pattern) {
this.pattern = pattern;
}
}
2.HandlerAdapter
知识温故: HandlerAdapter
是完成参数列表与Method实参的对应关系的适配器。
package org.springframework.web.servlet;
import org.springframework.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* <p>完成参数列表与Method实参的对应关系</p>
* @author Bosen
* @date 2021/9/15 13:04
*/
public class HandlerAdapter {
/**
* <p>判断handler是否属于HandlerMapping</p>
*/
public boolean supports(Object handler) {
return handler instanceof HandlerMapping;
}
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws InvocationTargetException, IllegalAccessException {
HandlerMapping handlerMapping = (HandlerMapping) handler;
// 方法的形参列表
Map<String, Integer> paramMapping = new HashMap<>();
// 处理被@ReauestParam标记的参数
Annotation[][] paramAnnotations = handlerMapping.getMethod().getParameterAnnotations();
for (int i = 0; i < paramAnnotations.length; i++) {
for (Annotation paramAnnotation : paramAnnotations[i]) {
if (paramAnnotation instanceof RequestParam) {
String paramName = ((RequestParam) paramAnnotation).value();
if (!"".equals(paramName.trim())) {
paramMapping.put(paramName, i);
}
}
}
}
// 处理HttpServletRequest和HttpServletResponse参数
Class<?>[] paramTypes = handlerMapping.getMethod().getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
Class<?> type = paramTypes[i];
if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
paramMapping.put(type.getName(), i);
}
}
// 用户通过URL传递过来的参数列表
Map<String, String[]> requestParamMap = request.getParameterMap();
// 实参列表
Object[] paramValues = new Object[paramTypes.length];
// 设置由@RequestParam标记的参数
for (Map.Entry<String, String[]> param : requestParamMap.entrySet()) {
String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "");
// 如果方法的形参列表中不存在该参数则跳过
if (!paramMapping.containsKey(param.getKey())) {
continue;
}
// 获取该参数在形参列表中的下标
int index = paramMapping.get(param.getKey());
// 参数类型转换
paramValues[index] = caseStringValue(value, paramTypes[index]);
}
// 设置request参数
if (paramMapping.containsKey(HttpServletRequest.class.getName())) {
paramValues[paramMapping.get(HttpServletRequest.class.getName())] = request;
}
// 设置response参数
if (paramMapping.containsKey(HttpServletResponse.class.getName())) {
paramValues[paramMapping.get(HttpServletResponse.class.getName())] = response;
}
// 执行目标方法
Object result = handlerMapping.getMethod().invoke(handlerMapping.getController());
if (handlerMapping.getMethod().getReturnType() == ModelAndView.class) {
return (ModelAndView) result;
}
return null;
}
/**
* <p>参数类型的转换</p>
*/
private Object caseStringValue(String value, Class<?> clazz) {
if (clazz == String.class) {
return value;
}
if (clazz == Integer.class || clazz == int.class) {
return Integer.valueOf(value);
}
return null;
}
}
3.ModelAndView
知识温故: ModelAndView
用于存储视图名称和数据
package org.springframework.web.servlet;
import java.util.Map;
/**
* <p>存储视图名称和数据</p>
* @author Bosen
* @date 2021/9/15 13:04
*/
public class ModelAndView {
/**
* <p>视图名称</p>
*/
private String viewName;
/**
* <p>参数</p>
*/
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 void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, ?> getModel() {
return model;
}
public void setModel(Map<String, ?> model) {
this.model = model;
}
}
4.ViewResolver
知识温故: ViewResolver
完成模板名称和模板解析引擎的匹配的工作
package org.springframework.web.servlet;
import java.io.File;
/**
* <p>模板名称和模板解析引擎的匹配</p>
* @author Bosen
* @date 2021/9/15 13:06
*/
public class ViewResolver {
/**
* <p>默认解析的文件后缀</p>
*/
private final String DEFAULT_TEMPLATE_SUFFIX = ".html";
/**
* <p>模板文件的目录</p>
*/
private File templateRootDir;
/**
* <p>视图名称</p>
*/
private String viewName;
public ViewResolver(String templateRoot) {
String templateRootPath = this.getClass().getResource(templateRoot).getFile();
this.templateRootDir = new File(templateRootPath);
}
/**
* <p>解析视图名</p>
*/
public View resolveViewName(String viewName) {
this.viewName = viewName;
if (viewName == null || "".equals(viewName.trim())) {
return null;
}
// 如果viewName没有加上后缀则加上
viewName = viewName.endsWith(DEFAULT_TEMPLATE_SUFFIX) ? viewName : viewName + DEFAULT_TEMPLATE_SUFFIX;
// 获取对应的模板文件
File templateFile = new File((templateRootDir.getPath() + "/" + viewName).replaceAll("/+", "/"));
// 通过模板文件返回视图对象
return new View(templateFile);
}
}
5.View
知识温故: View
完成视图的渲染(注入model)并将视图响应给用户
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>视图对象</p>
* @author Bosen
* @date 2021/9/15 13:11
*/
public class View {
/**
* <p>内容类型</p>
*/
public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=utf-8";
/**
* <p>模板文件</p>
*/
private File viewFile;
public View(File viewFile) {
this.viewFile = viewFile;
}
/**
* <p>解析模板文件,将model注入到view中,并respone给用户</p>
*/
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws IOException {
RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r");
StringBuffer sb = new StringBuffer();
// 对模板文件逐行读取,当有符合${}包裹的变量时,将其替换掉
try {
String line = null;
while ((line = ra.readLine()) != null) {
// 避免出现乱码
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("\\$\\{|\\}", "");
// 查找model中对应的变量
Object paramValue = model.get(paramName);
if (paramValue == null) {
continue;
}
line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
matcher = pattern.matcher(line);
}
sb.append(line);
}
} finally {
ra.close();
}
// 通过response响应给用户
response.setContentType(DEFAULT_CONTENT_TYPE);
response.setCharacterEncoding("utf-8");
response.getWriter().write(sb.toString());
}
/**
* <p>处理特殊字符串</p>
*/
public static String makeStringForRegExp(String string) {
return string
.replace("\\", "\\\\")
.replace("*", "\\")
.replace("+", "\\+")
.replace("|", "\\|")
.replace("{", "\\{")
.replace("}", "\\}")
.replace("(", "\\(")
.replace(")", "\\)")
.replace("^", "\\^")
.replace("$", "\\$")
.replace("[", "\\[")
.replace("]", "\\]")
.replace("?", "\\?")
.replace(",", "\\,")
.replace(".", "\\.")
.replace("&", "\\&");
}
}
6.DispatcherServlet
知识温故: DispatcherServlet
主要完成Spring MVC 的初始化工作,以及与用户的交互,通过它来调用映射器、适配器等组件工作。
package org.springframework.web.servlet;
import org.springframework.annotation.Controller;
import org.springframework.annotation.RequestMapping;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import javax.servlet.ServletConfig;
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.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>Spring MVC 入口类</p>
* @author Bosen
* @date 2021/9/15 11:19
*/
public class DispatcherServlet extends HttpServlet {
private final String LOCATION = "contextConfigLocation";
private List<HandlerMapping> handlerMappings = new ArrayList<>();
private Map<HandlerMapping, HandlerAdapter> handlerAdapters = new HashMap<>();
private List<ViewResolver> viewResolvers = new ArrayList<>();
private AbstractApplicationContext context;
@Override
public void init(ServletConfig config) {
try {
// 初始化IOC容器
context = new AnnotationConfigApplicationContext(
Class.forName(config.getInitParameter(LOCATION)));
} catch (Exception e) {
e.printStackTrace();
}
// 初始化Spring MVC九大组件
initStrategies(context);
}
/**
* <p>初始化Spring MVC九大组件</p>
* @param context IOC容器
*/
protected void initStrategies(AbstractApplicationContext context) {
// 初始化多文件上传组件
initMultipartResolver(context);
// 初始化本地语言环境
initLocaleResolver(context);
// 初始化模板处理器
initThemeResolver(context);
// 初始化映射器
initHandlerMappings(context);
// 初始化适配器
initHandlerAdapters(context);
// 初始化异常拦截器
initHandlerExceptionResolvers(context);
// 初始化视图预处理器
initRequestToViewNameTranslator(context);
// 初始化视图转换器
initViewResolvers(context);
// 初始化FlashMap管理器
initFlashMapManager(context);
}
/**
* <p>初始化九大组件的方法,由于我们实现的是迷你版的spring,只对映射器、视图解析器和适配器做初始化</p>
*/
private void initMultipartResolver(AbstractApplicationContext context) {}
private void initLocaleResolver(AbstractApplicationContext context) {}
private void initThemeResolver(AbstractApplicationContext context) {}
private void initHandlerExceptionResolvers(AbstractApplicationContext context) {}
private void initRequestToViewNameTranslator(AbstractApplicationContext context) {}
private void initFlashMapManager(AbstractApplicationContext context) {}
/**
* <p>初始化映射器</p>
* @param context IOC容器
*/
private void initHandlerMappings(AbstractApplicationContext context) {
// 从IOC容器中获取所有的实例
List<String> beanNames = new ArrayList<>(context.beanDefinitionMap.keySet());
for (String beanName : beanNames) {
// 获取当前bean的类型
Object controller = context.getBean(beanName);
Class<?> clazz = controller.getClass();
// 如果没有被@Controller标记则跳过
if (!clazz.isAnnotationPresent(Controller.class)) {
continue;
}
// 如果该类中被@RequestMapping标记,则需要获取请求地址的前缀
String baseUrl = "";
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
baseUrl = requestMapping.value();
}
// 扫描所有public类型的方法
Method[] methods = clazz.getMethods();
for (Method method : methods) {
// 如果该方法没有被@RequestMapping标记则跳过
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
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));
}
}
}
/**
* <p>初始化适配器</p>
* @param context IOC容器
*/
private void initHandlerAdapters(AbstractApplicationContext context) {
for (HandlerMapping handlerMapping : handlerMappings) {
this.handlerAdapters.put(handlerMapping, new HandlerAdapter());
}
}
/**
* <p>初始化视图解析器</p>
* @param context IOC容器
*/
private void initViewResolvers(AbstractApplicationContext context) {
// 默认扫描"/template"目录
String templateRoot = "/templates";
String templateRootPath = this.getClass().getResource(templateRoot).getFile();
File templateRootDir = new File(templateRootPath);
Arrays.asList(templateRootDir.listFiles()).forEach(
iter -> this.viewResolvers.add(new ViewResolver(templateRoot))
);
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
doDispatch(request, response);
} catch (Exception e) {
response.getWriter().write("" +
"<h1>500</h1><hr/>" + Arrays.toString(e.getStackTrace()));
e.printStackTrace();
}
}
private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws IOException, InvocationTargetException, IllegalAccessException {
// 根据用户请求获取映射器
HandlerMapping handler = getHandler(request);
if (handler == null) {
processDispatchResult(request, response, new ModelAndView("404"));
return;
}
// 获取适配器
HandlerAdapter handlerAdapter = getHandlerAdapter(handler);
// 调用对应的方法
ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
// 渲染并响应给用户
processDispatchResult(request, response, modelAndView);
}
/**
* <p>获取映射器</p>
*/
private HandlerMapping getHandler(HttpServletRequest request) {
if (this.handlerMappings.isEmpty()) {
return null;
}
String url = request.getRequestURI();
for (HandlerMapping handlerMapping : this.handlerMappings) {
Matcher matcher = handlerMapping.getPattern().matcher(url);
if (matcher.matches()) {
return handlerMapping;
}
}
return null;
}
/**
* <p>获取适配器</p>
*/
private HandlerAdapter getHandlerAdapter(HandlerMapping handlerMapping) {
if (this.handlerAdapters.isEmpty()) {
return null;
}
// 获取映射器相对应的适配器
HandlerAdapter handlerAdapter = this.handlerAdapters.get(handlerMapping);
if (handlerAdapter.supports(handlerMapping)) {
return handlerAdapter;
}
return null;
}
/**
* <p>渲染并响应给用户</p>
*/
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView) throws IOException {
if (modelAndView == null || this.viewResolvers.isEmpty()) {
return;
}
for (ViewResolver viewResolver : viewResolvers) {
View view = viewResolver.resolveViewName(modelAndView.getViewName());
if (view != null) {
view.render(modelAndView.getModel(), request, response);
return;
}
}
}
}
至此,迷你版的Spring MVC 的已经通过我们灵巧的双手撸出来啦!!接下来对这个刚刚完成的迷你版框架进行测试!
四、迷你版Spring MVC测试
1.ApplicationConfig
编写一个配置类
package org.springframework.test.config;
import org.springframework.annotation.ComponentScan;
/**
* <p>配置类</p>
* @author Bosen
* @date 2021/9/11 14:11
*/
@ComponentScan("org.springframework.test")
public class ApplicationConfig {}
2.TestController
编写一个控制器
package org.springframework.test.controller;
import org.springframework.annotation.Controller;
import org.springframework.annotation.RequestMapping;
import org.springframework.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import java.util.HashMap;
import java.util.Map;
/**
* @author Bosen
* @date 2021/9/11 22:31
*/
@Controller
public class TestController {
@RequestMapping("/test")
public ModelAndView test(@RequestParam("author") String author) {
Map<String, Object> model = new HashMap<>();
model.put("author", author);
return new ModelAndView("test", model);
}
@RequestMapping("/exception")
public void exception() {
int i = 1/0;
}
}
3.test.html
TestController.test
中指定跳转的页面(即:测试页面),${author}
经过View
对象的渲染后会替换成具体的值。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
@author: ${author}
</body>
</html>
4.web.xml
首先先来配置web项目的配置文件,主要将前端控制器DispatcherServlet
、和配置类ApplicationConfig
的全类路径填写正确即可!
<?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">
<servlet>
<servlet-name>mini-mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>org.springframework.test.config.ApplicationConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mini-mvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
5.404.html
当用户输入了不存在的url时要跳转的页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404</title>
</head>
<body>
<h1>404</h1>
</body>
</html>
6.开始测试
启动web程序后,首先让我们先测试404页面是否能显示成功,访问一个映射器中不存在的地址,如:http://localhost/demo(笔者的tomcat端口设置了为80),进入该地址,页面显示如下:
接着测试出现异常的页面,访问:http://localhost/exception (该地址故意执行了1/0
的逻辑错误代码)页面显示如下:
在404和500页面都测试无误后,我们开始测试具体要给用户展示的页面,即需要View
对象渲染并响应的页面,访问:http://localhost/test?author=Bosen 页面显示如下:
至此,我们迷你版的Spring MVC 已经成功实现并测试完毕~!!