前言
通过简单的代码,来学习 Spring 的设计思想,掌握 Spring 框架的基本脉络。主要目的是为了学习思想,有了思想之后,我们后面再学习源码的时候,就会相对轻松许多。
一、 mini 版 Spring MVC 基本架构设计
基本思路分为三步,因为是 mini 版,所以只是简单的实现基本功能。具体步骤如下:
1、配置阶段
配置 web.xml --> DispatchServlet
设定 init-param --> contextConfigLocation=classPath:application.xml
设定 url-pattern --> /*
配置 Annotation --> @Controller @Service @Autowrited @RequestMapping ...
2、初始化阶段
调用 init 方法 --> 加载配置文件
IOC 容器初始化 --> Map<String,Object>
扫描相关的类 --> scan-package = "xxx"
创建实例并保存到容器 --> 通过反射机制将类实例化放到 IOC 容器中
进行 DI 操作 --> 扫描 IOC 容器中的实例,给没有赋值的属性自动赋值
初始化 HandlerMapping --> 将一个 URL 和一个 Method 进行一对一的关联映射 Map<String,Method>
3、运行阶段
调用 doPost() / doGet() --> Web容器调用 doPost/doGet方法,获得 request / response 对象
匹配 HandlerMapping --> 从 request 对象中获得用户输入的 URL,找到其对应的 Method
反射调用 method.invoker() --> 利用反射调用方法并返回结果
response.getWrite().write() --> 将返回结果,输出到浏览器
二、mini 版 SpringMVC 的具体代码实现
清楚了基本的设计思想之后,我们根据上面的三个步骤来初步手写自己的 mini 版 MVC。
1. 自定义配置
配置 application.properties文件
为了解析方便,我们这里使用 application.properties 文件来代替 application.xml,具体配置如下,很简单:
scanPackage=com.mmt.mini
配置 web.xml 文件
大家都知道,所有依赖 web 容器的项目,都是从读取 web.xml 文件开始的。我们先配置好 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>Mini Web Application</display-name> <servlet> <servlet-name>mmt-mini-mvc</servlet-name> <!-- 最主要的是这个类 --> <servlet-class>com.mmt.mini.mvc.framework.MmtDispatchServlet</servlet-class> <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>mmt-mini-mvc</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
自定义 annotation
编写我们基本使用的几个常用注解,如:@controller、@service等,只不过,这里是完全使用我们自己的写的注解,包括项目中的依赖,都只是单纯了引入了一个 servlet 的依赖,和 spring 相关的包,一个都没有使用,先贴一下 pom.xml 中的依赖:
自定义 @MmtController
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtController {
String value() default "";
}
自定义 @MmtService
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtService {
String value() default "";
}
自定义 @MmtAutowired
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtAutowired {
String value() default "";
}
自定义 @MmtRequestMapping
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtRequestMapping {
String value() default "";
}
自定义 @MmtRequestParam
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MmtRequestParam {
String value() default "";
}
好了,基本的注解配置完成后,下面来配置业务实现类 DemoService:
package com.mmt.mini.demo.service;
public interface IDemoService {
String get(String name);
}
package com.mmt.mini.demo.service.impl;
import com.mmt.mini.demo.service.IDemoService;
import com.mmt.mini.mvc.framework.annotation.MmtService;
/**
* 核心业务逻辑 使用的是我们自己定义的注解
*/
@MmtService
public class DemoService implements IDemoService {
@Override
public String get(String name) {
return "Hello MVC : " + name + ",from service.";
}
}
配置请求入口类 DemoAction :
package com.mmt.mini.demo.action;
import com.mmt.mini.demo.service.IDemoService;
import com.mmt.mini.mvc.framework.annotation.MmtAutowired;
import com.mmt.mini.mvc.framework.annotation.MmtController;
import com.mmt.mini.mvc.framework.annotation.MmtRequestMapping;
import com.mmt.mini.mvc.framework.annotation.MmtRequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@MmtController
@MmtRequestMapping("/demo")
public class DemoAction {
@MmtAutowired
IDemoService demoService;
@MmtRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp, @MmtRequestParam("name") String name) {
String result = demoService.get(name);
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@MmtRequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp,
@MmtRequestParam("a") Integer a, @MmtRequestParam("b") Integer b) {
try {
resp.getWriter().write(a + "+" + b + "=" + (a + b));
} catch (IOException e) {
e.printStackTrace();
}
}
@MmtRequestMapping("/sub")
public void add(HttpServletRequest req, HttpServletResponse resp,
@MmtRequestParam("a") Double a, @MmtRequestParam("b") Double b) {
try {
resp.getWriter().write(a + "-" + b + "=" + (a - b));
} catch (IOException e) {
e.printStackTrace();
}
}
@MmtRequestMapping("/remove")
public String remove(@MmtRequestParam("id") Integer id) {
return "" + id;
}
}
大家注意看,这里使用的均是上面我们自己定义的注解,和 Spring 没有关系,看起来,是不是有点那个意思了?
好了,配置完了之后,配置阶段就完毕了,下面要进入容器初始化过程。
2. 容器初始化【核心内容】
初始化话容器的时候,进入到核心类 MmtDispatchServlet 中,利用 HttpServlet 的生命中周期,进行自定义的初始化过程,这里直接上代码,通过代码和注释,有助于理解:
package com.mmt.mini.mvc.framework;
import com.mmt.mini.mvc.framework.annotation.*;
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.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* mini mvc 入口
*
* @author: <a href="568227120@qq.com">heliang.wang</a>
* @date: 2022/5/15 3:19 下午
* @version: 1.0
*/
public class MmtDispatchServlet extends HttpServlet {
/**
* 保存application.properties配置文件中的内容
*/
private Properties contextConfig = new Properties();
/**
* 保存扫描的所有的类名
*/
private List<String> classNames = new ArrayList<String>();
/**
* 传说中的IOC容器,我们来揭开它的神秘面纱
* 为了简化程序,暂时不考虑ConcurrentHashMap
* 主要还是关注设计思想和原理
*/
private Map<String, Object> ioc = new HashMap<String, Object>();
/**
* 思考:为什么不用Map
* 你用Map的话,key,只能是url
* Handler 本身的功能就是把url和method对应关系,已经具备了Map的功能
* 根据设计原则:冗余的感觉了,单一职责,最少知道原则,帮助我们更好的理解
*/
private List<Handler> handlerMapping = new ArrayList<Handler>();
@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 {
//6、调用,运行阶段
try {
doDispatch(req, resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Exection,Detail : " + Arrays.toString(e.getStackTrace()));
}
}
/**
* 运行阶段,处理具体的浏览器请求
*
* @param req
* @param resp
* @throws Exception
*/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
Handler handler = getHandler(req);
if (handler == null) {
resp.getWriter().write("404 Not Found!!!");
return;
}
//获得方法的形参列表
Class<?>[] paramTypes = handler.getParamTypes();
Object[] paramValues = new Object[paramTypes.length];
Map<String, String[]> params = req.getParameterMap();
for (Map.Entry<String, String[]> parm : params.entrySet()) {
String value = Arrays.toString(parm.getValue()).replaceAll("\\[|\\]", "").replaceAll("\\s", ",");
if (!handler.paramIndexMapping.containsKey(parm.getKey())) {
continue;
}
int index = handler.paramIndexMapping.get(parm.getKey());
paramValues[index] = convert(paramTypes[index], value);
}
if (handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
}
if (handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
}
// 通过反射执行方法 method.invoke
Object returnValue = handler.method.invoke(handler.controller, paramValues);
if (returnValue == null || returnValue instanceof Void) {
return;
}
// 这里写的比较简单,直接返回浏览器了,其实还会有视图解析器的步骤,理解思想即可
resp.getWriter().write(returnValue.toString());
}
/**
* 从request 中获取请求
* @param req
* @return
*/
private Handler getHandler(HttpServletRequest req) {
if (handlerMapping.isEmpty()) {
return null;
}
//绝对路径
String url = req.getRequestURI();
//处理成相对路径
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath, "").replaceAll("/+", "/");
for (Handler handler : this.handlerMapping) {
Matcher matcher = handler.getPattern().matcher(url);
if (!matcher.matches()) {
continue;
}
return handler;
}
return null;
}
/**
* url传过来的参数都是String类型的,HTTP是基于字符串协议
* 只需要把String转换为任意类型就好
*
* @param type
* @param value
* @return
*/
private Object convert(Class<?> type, String value) {
//如果是int
if (Integer.class == type) {
return Integer.valueOf(value);
} else if (Double.class == type) {
return Double.valueOf(value);
}
//如果还有double或者其他类型,继续加if
//这时候,我们应该想到策略模式了
//在这里暂时不实现,希望小伙伴自己来实现
return value;
}
/**
* 初始化过程
*
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
//1、加载配置文件 <param-name>contextConfigLocation</param-name>
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2、扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//3、初始化扫描到的类,并且将它们放入到ICO容器之中
doInstance();
//4、完成依赖注入
doAutowired();
//5、初始化HandlerMapping
initHandlerMapping();
System.out.println("MMT MINI Spring framework is init.");
}
/**
* 加载配置文件
*
* @param contextConfigLocation
*/
private void doLoadConfig(String contextConfigLocation) {
//直接从类路径下找到Spring主配置文件所在的路径
//并且将其读取出来放到Properties对象中
//相对于scanPackage=com.mmt.mini 从文件中保存到了内存中
// 需要对路径进行处理,web.xml中配置的路径为 classpath:application.properties
InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation.replace("classpath:", ""));
try {
contextConfig.load(fis);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 扫描出相关的类
*
* @param scanPackage
*/
private void doScanner(String scanPackage) {
//scanPackage = com.mmt.mini ,存储的是包路径
//转换为文件路径,实际上就是把.替换为/就OK了
//classpath
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classPath = new File(url.getFile());
for (File file : classPath.listFiles()) {
if (file.isDirectory()) {
doScanner(scanPackage + "." + file.getName());
} else {
if (!file.getName().endsWith(".class")) {
continue;
}
String className = (scanPackage + "." + file.getName().replace(".class", ""));
classNames.add(className);
}
}
}
/**
* 初始化 bean
*/
private void doInstance() {
//初始化,为DI做准备
if (classNames.isEmpty()) {
return;
}
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
//什么样的类才需要初始化呢?
//加了注解的类,才初始化,怎么判断?
//为了简化代码逻辑,主要体会设计思想,只举例 @Controller和@Service,
// @Componment...就一一举例了
if (clazz.isAnnotationPresent(MmtController.class)) {
Object instance = clazz.newInstance();
//Spring默认类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, instance);
} else if (clazz.isAnnotationPresent(MmtService.class)) {
//1、自定义的beanName
MmtService service = clazz.getAnnotation(MmtService.class);
String beanName = service.value();
//2、默认类名首字母小写
if ("".equals(beanName.trim())) {
beanName = toLowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3、根据类型自动赋值,投机取巧的方式
for (Class<?> i : clazz.getInterfaces()) {
if (ioc.containsKey(i.getName())) {
throw new Exception("The “" + i.getName() + "” is exists!!");
}
//把接口的类型直接当成key了
ioc.put(i.getName(), instance);
}
} else {
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 自动依赖注入
*/
private void doAutowired() {
if (ioc.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
//Declared 所有的,特定的 字段,包括private/protected/default
//正常来说,普通的OOP编程只能拿到public的属性
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(MmtAutowired.class)) {
continue;
}
MmtAutowired autowired = field.getAnnotation(MmtAutowired.class);
//如果用户没有自定义beanName,默认就根据类型注入
//这个地方省去了对类名首字母小写的情况的判断
String beanName = autowired.value().trim();
if ("".equals(beanName)) {
//获得接口的类型,作为key待会拿这个key到ioc容器中去取值
beanName = field.getType().getName();
}
//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
//反射中叫做暴力访问, 强吻
field.setAccessible(true);
try {
//用反射机制,动态给字段赋值
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
/**
* 初始化url和Method的一对一对应关系
*/
private void initHandlerMapping() {
if (ioc.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(MmtController.class)) {
continue;
}
//保存写在类上面的@MmtRequestMapping("/demo")
String baseUrl = "";
if (clazz.isAnnotationPresent(MmtRequestMapping.class)) {
MmtRequestMapping requestMapping = clazz.getAnnotation(MmtRequestMapping.class);
baseUrl = requestMapping.value();
}
//默认获取所有的public方法
for (Method method : clazz.getMethods()) {
if (!method.isAnnotationPresent(MmtRequestMapping.class)) {
continue;
}
MmtRequestMapping requestMapping = method.getAnnotation(MmtRequestMapping.class);
//优化,防止 requestmapping中的请求路径没有写/ 或者都写了 / ,在这里,不管写没写,通统一处理一下
String regex = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
Pattern pattern = Pattern.compile(regex);
this.handlerMapping.add(new Handler(pattern, entry.getValue(), method));
System.out.println("Mapped :" + pattern + "," + method);
}
}
}
/**
* 如果类名本身是小写字母,确实会出问题
* 但是我要说明的是:这个方法是我自己用,private的
* 传值也是自己传,类也都遵循了驼峰命名法
* 默认传入的值,存在首字母小写的情况,也不可能出现非字母的情况
* 为了简化程序逻辑,就不做其他判断了,大家了解就OK
* 其实用写注释的时间都能够把逻辑写完了
*
* @param simpleName
* @return
*/
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
//之所以加,是因为大小写字母的ASCII码相差32,
// 而且大写字母的ASCII码要小于小写字母的ASCII码
//在Java中,对char做算学运算,实际上就是对ASCII码做算学运算
chars[0] += 32;
return String.valueOf(chars);
}
/**
* 保存一个url和一个Method的关系
*/
public class Handler {
//必须把url放到HandlerMapping才好理解吧
private Pattern pattern;
private Method method;
private Object controller;
private Class<?>[] paramTypes;
public Pattern getPattern() {
return pattern;
}
public Method getMethod() {
return method;
}
public Object getController() {
return controller;
}
public Class<?>[] getParamTypes() {
return paramTypes;
}
//形参列表
//参数的名字作为key,参数的顺序,位置作为值
private Map<String, Integer> paramIndexMapping;
public Handler(Pattern pattern, Object controller, Method method) {
this.pattern = pattern;
this.method = method;
this.controller = controller;
paramTypes = method.getParameterTypes();
paramIndexMapping = new HashMap<String, Integer>();
putParamIndexMapping(method);
}
private void putParamIndexMapping(Method method) {
//提取方法中加了注解的参数
//把方法上的注解拿到,得到的是一个二维数组
//因为一个参数可以有多个注解,而一个方法又有多个参数
Annotation[][] pa = method.getParameterAnnotations();
for (int i = 0; i < pa.length; i++) {
for (Annotation a : pa[i]) {
if (a instanceof MmtRequestParam) {
String paramName = ((MmtRequestParam) a).value();
if (!"".equals(paramName.trim())) {
paramIndexMapping.put(paramName, i);
}
}
}
}
//提取方法中的request和response参数
Class<?>[] paramsTypes = method.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);
}
}
}
}
}
3. 运行阶段
运行阶段的代码,已经放在了上面代码中,也就是第六步,doPost 方法中,利用委派墨水,通过 doDispatch方法,从 request 中获取对应的请求及参数,拿到请求后,通过我们自定义的HandlerMapping 中进行匹配,如果匹配成功,说明请求正确,存在我们的系统中,通过 method.invoker反射执行方法,从而返回响应。
以上,mini 版的 springMvc 框架就写完了,虽然功能不是很完善,但是基本的脉络思想已经描述清楚。
4. 运行效果图
真正的 Spring 要复杂很多,本文通过手写简易版的 SpringMVC,了解其基本的设计思想以及设计模式的简单使用,在 Spring 的源码中,使用了大量的设计模式,所以他的代码才会如此的优雅。