目录
码字不易,喜欢就点个关注❤,持续更新技术内容。
完整的源码包请私信。
1 编写一个简易版 SpringMVC 框架
1.1 回顾反射
编写一个简易版 Spring 框架首先我们需要温习一下反射的知识。我们可以通过三种方式获取到反射的基石——Class 类对象,也就是字节码文件的对象,字节码文件对象就是由编译好的字节码文件加载了 JVM 的对象。通过类对象我们可以获取到它的所有信息,包括构造器,成员变量、方法等。
// 1.通过源代码阶段的,A.class 文件获取
Class clazz1 = Class.forName("全限定类名");
// 2.通过加载阶段:一般用于当作参数,像锁对象就是一个类对象。
Class clazz2 = A.class;
// 3.通过运行阶段
A a = new A();
Class clazz3 = a.getClass();
那接下来给你一个如下的类,如何通过反射获取类上的注解,以及里面的所有成员属性及其注解、成员方法呢?
@MyController
public class TestController {
@MyAutowired
private TestService testService;
@MyGetMapping("/query")
public void query(HttpServletRequest request, HttpServletResponse response, @MyRequestParam("name") String name) throws IOException {
String result = testService.queryName(name);
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("查询结果: " + result);
}
}
如图,我们首先复制它的相对路径,因为编译后字节码文件也在同一路径下。
反射代码过程:
public class test01 {
public static void main(String[] args) throws ClassNotFoundException {
// 反射的基石:字节码文件对象
Class<?> clazz = Class.forName("com.bree.controller.TestController");
// 1.获取类上的注解
// 1)获取类上所有注解
Annotation[] declaredAnnotations = clazz.getDeclaredAnnotations();
System.out.println("获取类上的所有注解:"+Arrays.toString(declaredAnnotations));
// 2)或者指定获取类上的某个注解
MyController annotation = clazz.getAnnotation(MyController.class);
System.out.println("获取类上指定注解:"+annotation.annotationType().getSimpleName());
// 3)获取注解中的属性值
String value = annotation.value();
System.out.println("获取类上指定注解中的值:"+value);
System.out.println("=====================================================");
// 2.获取类中所有属性对象,并获取属性成员上的注解。注意getFields和getDeclaredFields的区别
Field[] fields = clazz.getDeclaredFields();
for(Field field : fields) {
// 1)判断属性上是否存在指定注解
boolean annotationPresent = field.isAnnotationPresent(MyService.class);
if (annotationPresent) {
System.out.println(field.getType().getSimpleName()+"属性上存在注解:"+MyService.class.getSimpleName());
}
// 2)获取属性对象的类型
Class<?> type = field.getType();
System.out.println("该属性的类型为:"+type.getSimpleName());
// 3)获取该属性类上是否存在指定注解
MyService annotation1 = type.getAnnotation(MyService.class);
if (annotation1 != null) {
System.out.println(type.getSimpleName()+"类上存在注解:"+annotation1.annotationType().getSimpleName());
}
}
System.out.println("=====================================================");
// 3.获取类中成员方法对象。注意getMethods和getDeclaredMethods的区别。
Method[] methods = clazz.getDeclaredMethods();
Map<String, Object> handlerMappingMap = new HashMap<>();
for (Method method : methods) {
// 1)获取方法中的参数,以及参数的注解
Parameter[] parameters = method.getParameters();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
System.out.println("方法中的参数:"+Arrays.toString(parameters));
System.out.println("方法中参数的注解:"+Arrays.toString(parameterAnnotations));
// 2)判断方法上的注解并获取其值,将路径与方法映射
if(!method.isAnnotationPresent(MyGetMapping.class)) {
continue;
}
String url = method.getAnnotation(MyGetMapping.class).value();
if(handlerMappingMap.containsKey(url)) {
throw new IllegalArgumentException(method.getName() + "上路径重复");
}
handlerMappingMap.put(url, method);
}
}
}
输出:
1.2 目的和实现
和 SpringMVC 一样,该简易版的 SpringMVC 框架就是基于 servlet 实现的。没有 servlet 基础的可以看我上面的相关文章。该简易版的 SpringMVC 框架目的就是一个请求进来时能将其准确分配到 controller 中的一个方法进行处理。
过程就是一个请求进来先进入 doGet() 或 doPost() 方法,都去调用 doDispatch() 方法,在方法中获取该请求的请求路径,通过 handlerMappingMap 映射是否存在该路径,因为 handlerMappingMap 中的存储的 key 为接口路径,而 value 为请求处理的方法。获取方法中参数类型进行赋值,最后反射调用 controller 方法。在实现过程中还会细讲。
我们要实现的是只需要在 controller 中定义好相关接口和路径,不需要写很多的 servlet 和配置。当 Web 项目启动时,用户直接就能调用到控制器中的接口方法。和原来的 servlet 相比看起来不是那么直观,不过它本质就是大的 servlet,只是用到了 Java 中非常重要的功能——反射。我们需要通过反射将声明好的这些接口和路径存起来,请求进来时我们就可以进行匹配调用。
实现过程:
-
首先需要加载配置文件获取基础包的全限定名。
-
扫描基础包全限定获取所有类文件的全限定路径名,存入 List 集合中。
-
将 List 中所有类路径下的类通过反射创建,放入 Map 集合中,就是所谓的 IOC 容器,Spring 中也是一个 Map 集合。
-
依赖注入。将 IOC 容器中的类进行赋值,并且将 controller 中的接口方法跟请求路径进行映射(放入handlerMappingMap 中)。
现在就直接开始上手,最后再来进行总结,看看 SpringMVC 和 Spring 中 IOC DI 都是怎么个事。
2 开发流程
2.1 DispatcherServlet 创建和配置
如下,创建一个 DispatcherServlet 继承 HttpServlet,重写 doGet 和 doPost 方法,它们最终都去调用 doDispatch 方法。这个后面讲。
public class MyDispatcherServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
this.doDispatch(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后在 web.xml 中进行 servlet 的配置:(和普通的 servlet 配置没什么区别,只是多了一个配置文件 application.properties)
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>com.bree.spring.framework.MyDispatcherServlet</servlet-class>
<init-param>
<param-name>defaultConfig</param-name>
<param-value>application.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
2.2 重写 init
我们需要在 resources 目录下创建 application.properties 文件,在其中添加基础包的全限定类名配置:
basePackages=com.bree.spring
然后在程序启动时,也就是在运行我们写好的 servlet 时,需要去获取该配置路径然后扫描该路径下的文件。所以我们就使用到了 servlet 中的 init 方法,我们只需要在 MyDispatcherServlet 类中重写 init 方法,程序运行就能自动执行该方法下的程序。
实现如下:
@Override
public void init(ServletConfig config) throws ServletException {
//1.加载配置文件
try {
// 1.获取 application.properties 文件,读取其中基础包的配置
doLoadConfig(config.getInitParameter("defaultConfig"));
System.out.println("加载配置文件成功");
} catch (Exception e) {
System.out.println("加载配置文件失败");
return;
}
// 2.扫描基础包下的所有类路径,放入 List 中
doScanPacakge(myConfig.getBasePackages());
System.out.println("类路径扫描完成");
// 3.将扫描到的类进行初始化,并存放到IOC容器
doInitializedClass();
System.out.println("自动装配完成");
// 4.依赖注入
doDependencyInjection();
System.out.println("依赖注入完成");
System.out.println("DispatchServlet 初始化完成");
}
首先大体解释以下初始化完成的内容:(和前面说的实现过程一样)
-
首先需要加载配置文件获取基础包的全限定名。
-
扫描基础包全限定获取所有类文件的全限定路径名,存入 List 集合中。
-
将 List 中所有类路径下的类通过反射创建,放入 Map 集合中,就是所谓的 IOC 容器,Spring 中也是一个 Map 集合。
-
依赖注入。将 IOC 容器中的类进行赋值,并且将 controller 中的接口方法跟请求路径进行映射(放入handlerMappingMap 中)。
接下来就是具体开发过程。
2.3 加载配置项
首先来看第一步 doLoadConfig 方法:
// 将配置信息读取到 config 对象中
private void doLoadConfig(String configPath) {
// 1.通过输入流加载配置文件
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(configPath);
Properties properties = new Properties();
try {
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
System.out.println("配置文件加载失败");
}
// 2.获取配置文件中的配置项,并通过反射获取 config 中属性k的属性对象,并为成员对象设置属性,即配置项中值v。
properties.forEach((k, v) -> {
try {
Field[] declaredFields = myConfig.getClass().getDeclaredFields();
for (Field f : declaredFields) {
System.out.println(f.getName());
}
Field field = myConfig.getClass().getDeclaredField((String) k);
field.setAccessible(true);
field.set(myConfig, v);
} catch (Exception e) {
e.printStackTrace();
System.out.println("初始化配置类失败");
return;
}
});
}
2.4 递归获取类路径
获取到基础包路径的配置信息后,就可以调用 doScanPacakge 方法扫描路径下的所有类,然后将类路径放入 classNameList中,这部分涉及到文件的递归扫描,将所有类文件的路径插入到 List 集合中:
private void doScanPacakge(String basePackages) {
if (StringUtils.isBlank(basePackages)) {
return;
}
// 把包名的.替换为/
String scanPath = "/" + basePackages.replaceAll("\\.", "/");
// 获取到当前包所在磁盘的全路径
URL url = this.getClass().getClassLoader().getResource(scanPath);
// 获取当前路径下所有文件
File files = new File(url.getFile());
File[] listFiles = files.listFiles(); // 注意,tomcat所在目录千万不能带空格
if (listFiles != null) {
// 开始扫描路径下的所有文件
for (File file : files.listFiles()) {
// 开始扫描路径下的所有文件
if (file.isDirectory()) {
doScanPacakge(basePackages + "." + file.getName());
} else {
// 如果是文件则添加到集合。因为上面是通过类加载器获取到的文件路径,所以实际上是class文件所在路径
classNameList.add(basePackages + "." + file.getName().replace(".class", ""));
}
}
}
}
2.5 扫描 Bean
继续往下,在 doInitializedClass 方法中遍历 classNameList 中的类路径,获取到类对象,然后判断该类上是否有注解,比如像 @Controller
@Service
这种注解,然后判断注解是否设置了别名,否则使用 getSimpleName() 后转为首字母小写,作为 key,反射获取构造器创建对象作为 value,然后插入到 Map 中,也就是 IOC 容器。
// 实例化要交给 IOC 创建的所有类,也就是有 `@Component` `@Controller` `@Service` 这些注解的类,将它们放入 IOC 容器中,也就是一个 ContainerMap。
private void doInitializedClass() {
if (classNameList.isEmpty()) {
return;
}
for (String className : classNameList) {
if (StringUtils.isEmpty(className)) {
continue;
}
Class clazz;
try {
// 获取字节码文件对象,判断为 Bean 后反射创建存入 Map 容器中
clazz = Class.forName(className);
if (clazz.isAnnotationPresent(MyController.class)) {
String value = ((MyController) clazz.getAnnotation(MyController.class)).value();
// 如果直接指定了value则取value,否则取首字母小写类名作为key值存储类的实例对象
iocContainerMap.put(StringUtils.isBlank(value) ? StringUtils.uncapitalize(clazz.getSimpleName()) : value, clazz.getDeclaredConstructor().newInstance());
} else if (clazz.isAnnotationPresent(MyService.class)) {
String value = ((MyService) clazz.getAnnotation(MyService.class)).value();
iocContainerMap.put(StringUtils.isBlank(value) ? StringUtils.uncapitalize(clazz.getSimpleName()) : value, clazz.getDeclaredConstructor().newInstance());
} else if (clazz.isAnnotationPresent(MyMapper.class)) {
String value = ((MyMapper) clazz.getAnnotation(MyMapper.class)).value();
iocContainerMap.put(StringUtils.isBlank(value) ? StringUtils.uncapitalize(clazz.getSimpleName()) : value, clazz.getDeclaredConstructor().newInstance());
} else {
System.out.println("不考虑其他注解的情况");
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("初始化类失败,className为" + className);
}
}
}
2.6 扫描依赖,注入依赖
最后最重要的一步,依赖注入,扫描所有类反射获取字节码对象,判断其中有没有注入注解,也就是我们使用的 @Autowired
@Resource
,我们从容器中获取对应的对象赋值给这些注解修饰的成员变量。
doDependencyInjection 方法实现注入的过程如下:
// Container 中存放着某某 service,某某 controller。其中就声明了带着注入注解的类属性,也就是依赖的类。
// 比如开发中 service 就常常注入 mapper 代理对象。controller 中就常常注入业务逻辑处理类
//
private void doDependencyInjection() {
if (iocContainerMap.size() == 0) {
return;
}
// 获取 Container 中键值对set集合的迭代器
Iterator<Map.Entry<String, Object>> iterator = iocContainerMap.entrySet().iterator();
// 遍历每个 Bean,其中可能有所依赖的类
while (iterator.hasNext()) {
// 首先获取所有属性对象
Map.Entry<String, Object> entry = iterator.next();
Class<?> clazz = entry.getValue().getClass();
Field[] fields = clazz.getDeclaredFields();
// 遍历当前类中每一个属性
for (Field field : fields) {
// 1.判断该属性对象有 MyAutowired 注解,且该类型的类在定义时由一个Bean注解修饰,也就是该对象已经通过 IOC 创建放到 Container中的(暂时不考虑其他注解)
if ( field.isAnnotationPresent(MyAutowired.class) &&
( field.getType().isAnnotationPresent(MyService.class)||
field.getType().isAnnotationPresent(MyMapper.class)
)
) {
// 2.获取 Bean 的名字
// 默认bean的value为类名首字母小写
String simpleName = StringUtils.uncapitalize(field.getType().getSimpleName());
// 如果依赖对象中的Bean注解中设置了别名,key 就是别名。
if(field.getType().isAnnotationPresent(MyService.class)) {
String simpleName1 = field.getType().getAnnotation(MyService.class).value();
if (!Objects.equals(simpleName1, "")) {
simpleName = simpleName1;
}
}
if(field.getType().isAnnotationPresent(MyMapper.class)) {
String simpleName2 = field.getType().getAnnotation(MyMapper.class).value();
if (!Objects.equals(simpleName2, "")) {
simpleName = simpleName2;
}
}
// 通过别名从 Container 中获取对应创建好的对象,然后赋值当前类的属性。
field.setAccessible(true);
try {
// 3.将ioc中对应的对象赋值给该属性对象。所以说所谓的注入不过是使用反射进行赋值。
field.set(entry.getValue(), iocContainerMap.get(simpleName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
// Controller 类还需要进一步处理,因为其中声明了很多接口方法,每一个接口都对应一个 HandlerMapping
// 初始化 HandlerMapping,以键为请求路由,值为 HandlerMapping 存入 handlerMappingMap。
String requestUrl = "";
// 1.获取 Controller 类上的请求路径
if (clazz.isAnnotationPresent(MyController.class)) {
requestUrl = clazz.getAnnotation(MyController.class).value();
}
// 2.循环类中的接口方法,获取方法上的路径,将它们拼接一起就是一个完整的接口路径
// 就是要做两件事,获取接口方法的请求路由为键,初始化 handlerMapping 为值存入 handlerMappingMap 中
Method[] methods = clazz.getMethods();
for (Method method : methods) {
// 假设只有 WolfGetMapping 这一种注解
if (!method.isAnnotationPresent(MyGetMapping.class)) {
continue;
}
MyGetMapping wolfGetMapping = method.getDeclaredAnnotation(MyGetMapping.class);
requestUrl = requestUrl + "/" + wolfGetMapping.value(); // 拼成完成的请求路径
// 不考虑正则匹配路径/xx/* 的情况,只考虑完全匹配的情况
if (handlerMappingMap.containsKey(requestUrl)) {
System.out.println("重复路径");
continue;
}
// 3.获取接口方法中参数注解中的参数名称,存入参数集合中
Annotation[][] annotationArr = method.getParameterAnnotations();
Map<Integer, String> methodParam = new HashMap<>(); // 存储参数的顺序和参数名
retryParam:
for (int i = 0; i < annotationArr.length; i++) {
for (Annotation annotation : annotationArr[i]) {
if (annotation instanceof MyRequestParam) {
MyRequestParam wolfRequestParam = (MyRequestParam) annotation;
methodParam.put(i, wolfRequestParam.value()); // 存储参数的位置和注解中定义的参数名
continue retryParam;
}
}
}
// 4.最后每个接口方法都生成一个 HandlerMapping,以请求路径为键存入 handlerMappingMap 中
requestUrl = this.formatUrl(requestUrl); // 主要是防止路径多了/导致路径匹配不上
HandlerMapping handlerMapping = new HandlerMapping();
handlerMapping.setRequestUrl(requestUrl); //请求路径
handlerMapping.setMethod(method); // 请求方法
handlerMapping.setTarget(entry.getValue()); // 请求方法所在controller对象
handlerMapping.setMethodParams(methodParam); // 请求方法的参数信息
handlerMappingMap.put(requestUrl, handlerMapping); // 存入 hashmap
}
}
}
以上就是在程序启动时所有需要准备的事情。接下来就是一个请求过来,是怎么将该区请求分配给对应的接口进行处理的。
2.7 请求分配处理
这部分比较简单,我们以及准备好了 handlerMappingMap,只需根据请求路径查询对应的 handlerMapping,然后就可以直接反射调用接口方法,因为 handlerMapping 中存的有对应的接口方法对象,其中定义执行相关功能的程序,然后进行结果的响应。
private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 对url的纠正
String requestUrl = this.formatUrl(request.getRequestURI());
// 1.首先查询该请求路径有没有对应的 handlerMapping
HandlerMapping handlerMapping = handlerMappingMap.get(requestUrl);
if (null == handlerMapping) {
response.getWriter().write("404 Not Found");
return;
}
// 2.参数处理
// 获取接口方法中的参数类型
Class<?>[] paramTypeArr = handlerMapping.getMethod().getParameterTypes();
Object[] paramArr = new Object[paramTypeArr.length];
// 遍历每个参数类型
for (int i = 0; i < paramTypeArr.length; i++) {
Class<?> clazz = paramTypeArr[i];
// 参数只考虑三种类型,其他不考虑
if (clazz == HttpServletRequest.class) {
paramArr[i] = request;
} else if (clazz == HttpServletResponse.class) {
paramArr[i] = response;
} else if (clazz == String.class) {
Map<Integer, String> methodParam = handlerMapping.getMethodParams();
paramArr[i] = request.getParameter(methodParam.get(i)); // 比如通过第一参数的名称获取到该参数的值
} else {
System.out.println("暂不支持的参数类型");
}
}
// 3.反射调用controller方法
handlerMapping.getMethod().invoke(handlerMapping.getTarget(), paramArr);
}
private String formatUrl(String requestUrl) {
requestUrl = requestUrl.replaceAll("/+", "/");
if (requestUrl.lastIndexOf("/") == requestUrl.length() - 1) {
requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
}
return requestUrl;
}
3 测试
首先定义一个控制器,用自定义的 @MyController 标识为控制器,相当于声明了一个特殊的 Bean。在其中定义一个查询接口,该接口去调用 TestService 的 queryName 方法,该方法接收参数查询后返回结果。
@MyController
public class TestController {
@MyAutowired
private TestService testService;
@MyGetMapping("/query")
public void query(HttpServletRequest request, HttpServletResponse response, @MyRequestParam("name") String name) throws IOException {
String result = testService.queryName(name);
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("查询结果: " + result);
}
}
在 TestService 中我们又去调用 TestMapper 进行真正的数据库查询操作。
@MyService(value = "queryService")//为了演示能否正常取value属性
public class TestService {
@MyAutowired
private TestMapper testMapper;
public String queryName(String name) {
return testMapper.queryUserName(name);
}
}
当然这里做个简单的判断就行了。
@MyMapper
public class TestMapper {
public String queryUserName(String name) {
if(Objects.equals(name, "Max")) {
return name;
}
return "无该用户";
}
}
我们主要是关注注入的这些依赖是否成功生效。运行起来。首先打印日志显示成功注入。
我们发送请求试试,首先不带参数查询。未查询到。
再带一个有参数,但是不符合条件的参数查询,查无此人。
最后我们查询存在的用户,成功查询。
4 总结
所以这个简易的 Spring 和 SpringMVC 可以学到什么呢?我想通过去背一些专业术语和概念是不能够真正理解其奥妙的,那些庞杂的框架一口吞不下,需要抽丝剥茧地去理解。经过这样抽丝剥茧的学习,当再去面对 Spring 中庞杂的 BeanFactory 继承体系时就不会迷失方向了。
spring
首先 Spring,不严谨的说 Spring 就是一个容器 (在实际的 Spring 中有两个容器)。我们通过将反射每个声明的 Bean 初始化,并装配到容器中,然后由通过反射完成依赖的赋值。也就是 Spring 涉及到两次主要的反射过程,一次是反射创建,一次是反射赋值。
所以 Spring 真正给我们带来的是什么呢,就是简单的不用自己实例化?其实就是这么简单,但是其作用非常大,除了规范的开发流程以外,Spring "不用自己实例化" 的作用让它成为了许多中间件的港湾,使得很多框架可以很容易地整合到 Spring,开袋即食。
开发过程就是面向反射开发以及面向注解开发。核心是 IocContainerMap。IocContainer 存储了可以注入使用的对象,所以我们通过 IocContainerMap 来总结。
一句话总结就是程序运行起来通过扫描所有类,将加了Bean注解的类通过反射将它们自动装配到 IocContainerMap 集合中,依赖注入紧跟其后,在注入依赖的时候需要去扫一遍 IocContainerMap 集合判断类中是否有注入注解获取到属性对象,然后通过反射进行赋值。
springMVC
而 MVC 部分,核心思想就是通过一个大 Servlet 拦截所有请求,然后它通过路径匹配之后,反射调用对应的接口方法处理。本质上每个接口还是一个 Servlet。
我们只实现控制器部分,MVC 是很早就有的概念了,现在流行的分离式开发其实是对 V 的精细化。现在后端只需要关注接收数据进行业务逻辑的处理,也就是我们现在的 Java 后端工程大部分就是基于 Spring 的 REST 服务。RESTful 接口开发本质是 HTTP 的请求响应模型。