前言
学习如何使用 Spring,SpringMVC 是很快的, 但是在往后使用的过程中难免会想探究一下框架背后的原理是什么, 本文将通过讲解如何手写一个简单版的 springMVC 框架, 直接从代码上看框架中请求分发, 控制反转和依赖注入是如何实现的.
建议配合示例源码阅读, GitHub 地址如下:
https://github.com/liuyj24/mini-spring
项目搭建
项目搭建可以参考 GitHub 中的项目, 先选好 jar 包管理工具, Maven 和 Gradle 都行, 本项目使用的是 Gradle.
然后在项目下建两个模块, 一个是 framework, 用于编写框架; 另外一个是 test, 用于应用并测试框架(注意 test 模块要依赖 framework 模块).
接着在 framework 模块下按照 spring 创建好 beans,core,context,web 等模块对应的包, 完成后便可以进入框架的编写了.
请求分发
在讲请求分发之前先来梳理一下整个 Web 模型:
首先用户在客户端发送一个请求到服务器, 经操作系统的 TCP/IP 栈解析后会交到在某个端口监听的 Web 服务器.
Web 服务器程序监听到请求后便会把请求分发给对应的程序进行处理. 比如 Tomcat 就会将请求分发给对应的 java 程序 (servlet) 进行处理, Web 服务器本身是不进行请求处理的.
本项目的 Web 服务器选择 Tomcat, 而且为了能让项目直接跑起来, 选择了在项目中内嵌 Tomcat, 这样框架在做测试的时候就能像 spring boot 一样一键启动, 方便测试.
Servlet
既然选择了使用 Java 编写服务端程序, 那就不得不提到 Servlet 接口了. 为了规范服务器与 Java 程序之间的通信方式, Java 官方制定了 Servlet 规范, 服务端的 Java 应用程序必须实现该接口, 把 Java 作为处理语言的服务器也必须要根据 Servlet 规范进行对接.
在还没有 spring 之前, 人们是这么开发 Web 程序的: 一个业务逻辑对应一个 Servlet, 所以一个大项目中会有多个 Servlet, 这大量的 Servlet 会被配置到一个叫 Web.xml 的配置文件中, 当服务器运行的时候, tomcat 会根据请求的 uri 到 Web.xml 文件中寻找对应的 Servlet 业务类处理请求.
但是你想, 每来一个请求就创建一个 Servlet, 而且一个 Servlet 实现类中我们通常只重写一个 service 方法, 另外四个方法都只是给个空实现, 这太浪费资源了. 而且编起程序来创建很多 Servlet 还很难管理. 能不能改进一下?
Spring 的 DispatcherServlet
方法确实有:
从上图可以看到, 我们原来是经过 Web 服务器把请求分发到不同的 Servlet; 我们可以换个思路, 让 Web 服务器把请求都发送到一个 Servlet, 再由这个 Servlet 把请求按照 uri 分发给不同的方法进行处理.
这样一来, 不管收到什么请求, Web 服务器都会分发到同一个 Servlet(DispatcherServlet), 避免了多个 Servlet 所带来的问题, 有以下好处:
把分发请求这一步从 Web 服务器移动到框架内, 这样更容易控制, 也方便扩展.
可以把同一个业务的处理方法集中到同一个类里, 把这种类起名为 controller, 一个 controller 中有多个处理方法, 这样配置分散不杂乱.
配置 uri 映射路径的时候可以不使用配置文件, 直接在处理方法上用注解配置即可, 解决了配置集中, 大而杂的问题.
实操
建议配合文章开头给出的源码进行参考
首先在 Web.mvc 包中创建三个注解: Controller,RequestMapping,RequestParam, 有了注解我们才能在框架启动时动态获得配置信息.
由于处理方法都是被注解的, 要想解析被注解的类, 首先得获得项目中相关的所有类, 对应是源码中 core 包下的 ClassScanner 类
- public class ClassScanner {
- public static List<Class<?>> scanClass(String packageName) throws IOException, ClassNotFoundException {
- // 用于保存结果的容器
- List<Class<?>> classList = new ArrayList<>();
- // 把文件名改为文件路径
- String path = packageName.replace(".", "/");
- // 获取默认的类加载器
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
- // 通过文件路径获取该文件夹下所有资源的 URL
- Enumeration<URL> resources = classLoader.getResources(path);
- int index = 0;// 测试
- while(resources.hasMoreElements()){
- // 拿到下一个资源
- URL resource = resources.nextElement();
- // 先判断是否是 jar 包, 因为默认. class 文件会被打包为 jar 包
- if(resource.getProtocol().contains("jar")){
- // 把 URL 强转为 jar 包链接
- JarURLConnection jarURLConnection = (JarURLConnection)resource.openConnection();
- // 根据 jar 包获取 jar 包的路径名
- String jarFilePath = jarURLConnection.getJarFile().getName();
- // 把 jar 包下所有的类添加的保存结果的容器中
- classList.addAll(getClassFromJar(jarFilePath, path));
- }else{// 也有可能不是 jar 文件, 先放下
- //todo
- }
- }
- return classList;
- }
- /**
- * 获取 jar 包中所有路径符合的类文件
- * @param jarFilePath
- * @param path
- * @return
- */
- private static List<Class<?>> getClassFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
- List<Class<?>> classes = new ArrayList<>();// 保存结果的集合
- JarFile jarFile = new JarFile(jarFilePath);// 创建对应 jar 包的句柄
- Enumeration<JarEntry> jarEntries = jarFile.entries();// 拿到 jar 包中所有的文件
- while(jarEntries.hasMoreElements()){
- JarEntry jarEntry = jarEntries.nextElement();// 拿到一个文件
- String entryName = jarEntry.getName();// 拿到文件名, 大概是这样: com/shenghao/test/Test.class
- if (entryName.startsWith(path) && entryName.endsWith(".class")){// 判断是否是类文件
- String classFullName = entryName.replace("/", ".")
- .substring(0, entryName.length() - 6);
- classes.add(Class.forName(classFullName));
- }
- }
- return classes;
- }
- }
然后在 handler 包创建 MappingHandler 类, 在将来框架运行的过程中, 一个 MappingHandler 就对应一个业务逻辑, 比如说增加一个用户. 所以一个 MappingHandler 中要有 "请求 uri, 处理方法, 方法的参数, 方法所处的类" 这四个字段, 其中请求 uri 用于匹配请求 uri, 后面三个参数用于运行时通过反射调用该处理方法
- public class MappingHandler {
- private String uri;
- private Method method;
- private Class<?> controller;
- private String[] args;
- MappingHandler(String uri, Method method, Class<?> cls, String[] args){
- this.uri = uri;
- this.method = method;
- this.controller = cls;
- this.args = args;
- }
- public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
- // 拿到请求的 uri
- String requestUri = ((HttpServletRequest)req).getRequestURI();
- if(!uri.equals(requestUri)){// 如果和自身 uri 不同就跳过
- return false;
- }
- Object[] parameters = new Object[args.length];
- for(int i = 0; i <args.length; i++){
- parameters[i] = req.getParameter(args[i]);
- }
- Object ctl = BeanFactory.getBean(controller);
- Object response = method.invoke(ctl, parameters);
- res.getWriter().println(response.toString());
- return true;
- }
- }
接下来在 handler 包创建 HandlerManager 类, 这个类拥有一个静态的 MappingHandler 集合, 这个类的作用是从获得的所有类中, 找到被 @controller 注解的类, 并将 controller 类中每个被 @ReqeustMapping 注解的方法封装成一个 MappingHandler, 然后把 MappingHandler 放入静态集合中
- public class HandlerManager {
- public static List<MappingHandler> mappingHandlerList = new ArrayList<>();
- /**
- * 处理类文件集合, 挑出 MappingHandler
- * @param classList
- */
- public static void resolveMappingHandler(List<Class<?>> classList){
- for(Class<?> cls : classList){
- if(cls.isAnnotationPresent(Controller.class)){//MappingHandler 会在 controller 里面
- parseHandlerFromController(cls);// 继续从 controller 中分离出一个个 MappingHandler
- }
- }
- }
- private static void parseHandlerFromController(Class<?> cls) {
- // 先获取该 controller 中所有的方法
- Method[] methods = cls.getDeclaredMethods();
- // 从中挑选出被 RequestMapping 注解的方法进行封装
- for(Method method : methods){
- if(!method.isAnnotationPresent(RequestMapping.class)){
- continue;
- }
- String uri = method.getDeclaredAnnotation(RequestMapping.class).value();// 拿到 RequestMapping 定义的 uri
- List<String> paramNameList = new ArrayList<>();// 保存方法参数的集合
- for(Parameter parameter : method.getParameters()){
- if(parameter.isAnnotationPresent(RequestParam.class)){// 把有被 RequestParam 注解的参数添加入集合
- paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
- }
- }
- String[] params = paramNameList.toArray(new String[paramNameList.size()]);// 把参数集合转为数组, 用于反射
- MappingHandler mappingHandler = new MappingHandler(uri, method, cls, params);// 反射生成 MappingHandler
- mappingHandlerList.add(mappingHandler);// 把 mappingHandler 装入集合中
- }
- }
- }
完成上面四步后, 我们在框架启动的时候就获得了一个 MappingHandler 集合, 当请求来到时, 我们只要根据请求的 uri 从集合中找到对应的 MappingHandler, 就可以通过反射调用对应的处理方法, 到此也就完成了框架请求分发的功能.
控制反转和依赖注入
完成了请求分发功能后, 进一步想这么一个问题:
假设现在处理一个请求需要创建 A,B,C 三个对象, 而
A 有个字段 D
B 有个字段 D
C 有个字段 B
如果按照顺序创建 ABC 的话,
首先要创建一个 D, 然后创建一个 A;
接着先创建一个 D, 然后创建一个 B;
接着先创建一个 D, 然后创建一个 B, 才能创建出一个 C
总共创建了一个 A, 两个 B, 一个 C, 三个 D.
上述是我们编写程序的一方创建对象的方式, 可以看到由于对象不能被重复引用, 导致创建了大量重复对象.
为了解决这个问题, spring 提出了 bean 这么个概念, 你可以把一个 bean 理解为一个对象, 但是他对比普通的对象有如下特点:
不像普通对象一样朝生暮死, 声明周期较长
在整个虚拟机内可见, 不像普通对象只在某个代码块中可见
维护成本高, 以单例形式存在
为了制作出上述的 bean, 我们得有个 bean 工厂, bean 工厂的原理也很简单: 在框架初始化的时候创建相关的 bean(也可以在用到的时候创建), 当需要使用 bean 的时候直接从工厂中拿. 也就是我们把创建对象的权力交给框架, 这就是控制反转
有了 bean 工厂后按顺序创建 ABC 的过程如下:
首先创建一个 D, 把 D 放入工厂, 然后创建一个 A, 把 A 放入工厂;
接着从工厂拿出一个 D, 创建一个 B, 把 B 也放入工厂;
接着从工厂拿出一个 B, 创建一个 C, 把 C 也放入工厂;
总共创建了一个 A, 一个 B, 一个 C, 一个 D
达到了对象重复利用的目的
至于创建出一个 D, 然后把 D 设置为 A 的一个字段这么个过程, 叫做依赖注入
所以控制反转和依赖注入的概念其实很好理解, 控制反转是一种思想, 而依赖注入是控制反转的一种具体实现.
实操
首先在 bean 包下创建 @Bean 和 @AutoWired 两个注解, 同样是用于框架解析类的.
接着在 bean 包下创建 BeanFactory,BeanFactory 要能提供一个根据类获取实例的功能, 这就要求他要有一个静态的 getBean()方法, 和一个保存 Bean 的映射集合.
为了初始化 Bean, 要有一个根据类文件集合解析出 bean 的方法. 该方法会遍历集合中所有的类, 把有注解的, 属于 bean 的类提取出来, 创建该类的对象并放到静态集合中.
在这里有个有意思的点 -- 按什么顺序创建 bean? 在本文给出的源码中, 用了一个循环来创建 bean, 如果该 bean 没有依赖其他的 bean 就直接创建, 如果有依赖其他 bean 就看其他 bean 有没被创建出来, 如果没有就跳过当前的 bean, 如果有就创建当前的 bean.
在循环创建 bean 的过程中可能出现一种 bean 之间相互依赖的现象, 源码中暂时对这种现象抛出异常, 没作处理.
- public class BeanFactory {
- // 保存 Bean 实例的映射集合
- private static Map<Class<?>, Object> classToBean = new ConcurrentHashMap<>();
- /**
- * 根据 class 类型获取 bean
- * @param cls
- * @return
- */
- public static Object getBean(Class<?> cls){
- return classToBean.get(cls);
- }
- /**
- * 初始化 bean 工厂
- * @param classList 需要一个. class 文件集合
- * @throws Exception
- */
- public static void initBean(List<Class<?>> classList) throws Exception {
- // 先创建一个. class 文件集合的副本
- List<Class<?>> toCreate = new ArrayList<>(classList);
- // 循环创建 bean 实例
- while(toCreate.size() != 0){
- int remainSize = toCreate.size();// 记录开始时集合大小, 如果一轮结束后大小没有变证明有相互依赖
- for(int i = 0; i <toCreate.size(); i++){// 遍历创建 bean, 如果失败就先跳过, 等下一轮再创建
- if(finishCreate(toCreate.get(i))){
- toCreate.remove(i);
- }
- }
- if(toCreate.size() == remainSize){// 有相互依赖的情况先抛出异常
- throw new Exception("cycle dependency!");
- }
- }
- }
- private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
- // 创建的 bean 实例仅包括 Bean 和 Controller 注释的类
- if(!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)){
- return true;
- }
- // 先创建实例对象
- Object bean = cls.newInstance();
- // 看看实例对象是否需要执行依赖注入, 注入其他 bean
- for(Field field : cls.getDeclaredFields()){
- if(field.isAnnotationPresent(AutoWired.class)){
- Class<?> fieldType = field.getType();
- Object reliantBean = BeanFactory.getBean(fieldType);
- if(reliantBean == null){// 如果要注入的 bean 还未被创建就先跳过
- return false;
- }
- field.setAccessible(true);
- field.set(bean, reliantBean);
- }
- }
- classToBean.put(cls, bean);
- return true;
- }
- }
有了 bean 工厂之后, 凡是用到 bean 的地方都能直接通过 bean 工厂拿了
最后我们可以写一个小 Demo 测试一下自己的框架是否能正确地处理请求完成响应. 相信整个迷你框架撸下来, Spring 的核心功能, 以及控制反转, 依赖控制等名词在你脑海中不再只是概念, 而是一行行清晰的代码了.
来源: https://www.cnblogs.com/tanshaoshenghao/p/11433999.html