用300行代码手写Spring核心原理

本文将带你深入了解Spring框架的核心原理,通过300行代码的迷你版本来展示Spring最核心的特性:IoC(控制反转)、DI(依赖注入)和MVC(模型-视图-控制器)模式的实现。

mini版Spring实现思路

实现过程

自定义注解

在Spring框架中,注解是非常重要的组成部分。我们的迷你版也实现了几个关键注解

java 复制代码
// 控制器注解,标记控制器类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SevenController {
    String value() default "";
}

// 服务注解,标记服务类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SevenService {
    String value() default "";
}

// 请求映射注解,可用于类或方法
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SevenRequestMapping {
    String value() default "";
}

// 参数映射注解,用于方法参数
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SevenRequestParam {
    String value() default "";
}

// 自动装配注解,用于依赖注入
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SevenAutowired {
    String value() default "";
}

这些注解的实现非常简单,但它们构成了整个框架的基础。通过运行时保留策略,我们可以在运行时通过反射机制来识别这些注解并执行相应的操作。

  • @SevenController 和 @SevenService 用于标识需要被IoC容器管理的类
  • @SevenAutowired 用于标识需要自动注入的依赖
  • @SevenRequestMapping 用于映射请求URL到具体的方法
  • @SevenRequestParam 用于映射请求参数到方法参数

这些自定义注解的设计思路完全模仿了Spring框架中的标准注解,如@Controller、@Service、@Autowired、@RequestMapping

IoC容器和DI依赖注入的实现

IoC(控制反转)和DI(依赖注入)是Spring框架的核心特性。

IoC容器的初始化过程:IoC容器本质上是一个Map集合,用来存储所有被管理的对象实例。在SevenDispatcherServlet中,我们定义了一个简单的IoC容器:

java 复制代码
//传说中的IOC容器,我们来揭开它的神秘面纱
//为了简化程序,暂时不考虑ConcurrentHashMap
// 主要还是关注设计思想和原理
private Map<String,Object> ioc = new HashMap<String,Object>();

IoC容器的初始化分为四个关键步骤:

  1. 加载配置文件 - 读取application.properties文件
  2. 扫描相关类 - 扫描指定包下的所有类
  3. 实例化类 - 创建被注解标记的类的实例
  4. 依赖注入 - 将依赖关系注入到对象中
  • 加载配置文件
java 复制代码
//加载配置文件
private void doLoadConfig(String contextConfigLocation) {
    //直接从类路径下找到Spring主配置文件所在的路径
    //并且将其读取出来放到Properties对象中
    InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
    try {
        contextConfig.load(is);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(null != is){
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 扫描相关类
java 复制代码
//扫描出相关的类
private void doScanner(String scanPackage) {
    //scanPackage = com.seven.minispringsourcecod.demo ,存储的是包路径
    URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));
    
    //转换为文件路径,实际上就是把.替换为/就OK了
    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; }
            classNames.add(scanPackage + "." + file.getName().replace(".class", ""));
        }
    }
}
  • 实例化类并存入容器
java 复制代码
private void doInstance() {
    if(classNames.isEmpty()){return;}

    try {
        for (String className : classNames) {
            Class<?> clazz = Class.forName(className);

            //什么样的类才需要初始化呢?
            //加了注解的类,才初始化
            if(clazz.isAnnotationPresent(SevenController.class)){
                Object instance = clazz.newInstance();
                String beanName = toLowerFirstCase(clazz.getSimpleName());
                //key-value
                //class类名的首字母小写
                ioc.put(beanName,instance);
            } else if(clazz.isAnnotationPresent(SevenService.class)) {
                //1、默认就根据beanName类名首字母小写
                String beanName = toLowerFirstCase(clazz.getSimpleName());

                //2、使用自定义的beanName
                SevenService service = clazz.getAnnotation(SevenService.class);
                if(!"".equals(service.value())){
                    beanName = service.value();
                }

                Object instance = clazz.newInstance();
                ioc.put(beanName,instance);

                //3、根据包名.类名作为beanName
                for (Class<?> i : clazz.getInterfaces()) {
                    if(ioc.containsKey(i.getName())){
                        throw new Exception("The beanName is exists!!");
                    }
                    //把接口的类型直接当成key了
                    ioc.put(i.getName(),instance);
                }
            } else {
                continue;
            }
        }
    } catch (Exception e){
        e.printStackTrace();
    }
}

//工具方法:将类名首字母转为小写
private String toLowerFirstCase(String simpleName) {
    char [] chars = simpleName.toCharArray();
    //之所以加,是因为大小写字母的ASCII码相差32,
    // 而且大写字母的ASCII码要小于小写字母的ASCII码
    //在Java中,对char做算学运算,实际上就是对ASCII码做算学运算
    chars[0] += 32;
    return String.valueOf(chars);
}
  • 依赖注入(DI)
java 复制代码
private void doAutowired() {
    if(ioc.isEmpty()){return;}

    for (Map.Entry<String,Object> entry : ioc.entrySet()) {
        //拿到实例的所有的字段
        Field[] fields = entry.getValue().getClass().getDeclaredFields();
        for (Field field : fields) {
            if(!field.isAnnotationPresent(SevenAutowired.class)){
                continue;
            }
            SevenAutowired autowired = field.getAnnotation(SevenAutowired.class);
            //如果用户没有自定义beanName,默认就根据类型注入
            String beanName = autowired.value().trim();
            if("".equals(beanName)){
                //获得接口的类型,作为key待会拿这个key到ioc容器中去取值
                beanName = field.getType().getName();
            }

            //如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
            //反射中叫做暴力访问
            field.setAccessible(true);

            //反射调用的方式
            //给entry.getValue()这个对象的field字段,赋ioc.get(beanName)这个值
            try {
                field.set(entry.getValue(),ioc.get(beanName));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}

依赖注入的过程:

  1. 遍历IoC容器中的每个对象
  2. 获取对象的所有字段
  3. 检查字段是否标注了@SevenAutowired注解
  4. 如果有注解,则从IoC容器中查找对应的依赖对象
  5. 使用反射将依赖对象注入到当前对象的字段中

通过这种方式,我们就实现了控制反转------对象不再需要手动创建依赖,而是由容器负责创建和注入。

MVC模式的实现

MVC(Model-View-Controller)模式是Web开发中的经典架构模式。在我们的迷你版Spring框架中,MVC的实现主要体现在HandlerMapping的建立和请求处理两个方面。

HandlerMapping的初始化

HandlerMapping是MVC模式中的核心组件,它建立了URL请求与控制器方法之间的映射关系:

java 复制代码
//初始化url和Method的一对一对应关系
private void doInitHandlerMapping() {
    if(ioc.isEmpty()){return;}

    for (Map.Entry<String,Object> entry : ioc.entrySet()) {
       Class<?> clazz = entry.getValue().getClass();
       if(!clazz.isAnnotationPresent(SevenController.class)){ continue; }

        //保存写在类上面的@SevenRequestMapping("/demo")
       String baseUrl = "";
       if(clazz.isAnnotationPresent(SevenRequestMapping.class)){
           SevenRequestMapping requestMapping = clazz.getAnnotation(SevenRequestMapping.class);
           baseUrl = requestMapping.value();
       }

        //默认获取所有的public方法
        for (Method method : clazz.getMethods()) {
            if(!method.isAnnotationPresent(SevenRequestMapping.class)){continue;}

            SevenRequestMapping requestMapping = method.getAnnotation(SevenRequestMapping.class);

            //  无斜杠:demoquery
            //  多个斜杠://demo//query
            String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/");
            handlerMapping.put(url,method);

            System.out.println("Mapped " + url + "," + method);
        }
    }
}

这段代码的执行逻辑如下:

  1. 遍历IoC容器中的所有对象
  2. 找到被@SevenController注解标记的类
  3. 提取类上的@SevenRequestMapping注解作为基础路径
  4. 遍历类中的所有方法
  5. 找到被@SevenRequestMapping注解标记的方法
  6. 将基础路径和方法路径组合成完整的URL路径
  7. 建立URL路径与方法的映射关系并存储在handlerMapping中

请求处理过程

当HTTP请求到达时,系统会根据URL找到对应的方法并执行:

java 复制代码
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replaceAll(contextPath,"").replaceAll("/+","/");

    if(!this.handlerMapping.containsKey(url)){
        resp.getWriter().write("404 Not Found!!");
        return;
    }

    Method method = this.handlerMapping.get(url);

    Map<String,String[]> paramsMap = req.getParameterMap();

    //实参列表
    //实参列表要根据形参列表才能决定,首先得拿到形参列表
    Class<?> [] paramterTypes = method.getParameterTypes();

    Object [] parameValues = new Object[paramterTypes.length];
    for (int i = 0; i <paramterTypes.length; i ++){
        Class paramterType = paramterTypes[i];
        if(paramterType == HttpServletRequest.class){
            parameValues[i] = req;
            continue;
        } else if(paramterType == HttpServletResponse.class){
            parameValues[i] = resp;
            continue;
        } else if(paramterType == String.class){
            Annotation[][] pa = method.getParameterAnnotations();
            for (int j = 0; j < pa.length; j ++){
                for (Annotation a : pa[i]) {
                    if(a instanceof SevenRequestParam){
                        String paramName = ((SevenRequestParam) a).value();
                        if(!"".equals(paramName.trim())){
                            String value = Arrays.toString(paramsMap.get(paramName))
                                    .replaceAll("\\[|\\]","")
                                    .replaceAll("\\s",",");
                            parameValues[i] = value;
                        }
                    }
                }
            }
        }
    }

    String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
    method.invoke(ioc.get(beanName),parameValues);
}

这个请求处理过程包含了以下几个重要步骤:

  1. URL解析:提取请求的URI并去除上下文路径
  2. 方法查找:根据URL在handlerMapping中查找对应的方法
  3. 参数解析:解析HTTP请求参数并匹配到方法参数
  4. 方法调用:使用反射调用找到的方法并传递参数

参数解析是其中比较复杂的部分,需要处理三种情况:

  • HttpServletRequest和HttpServletResponse类型的参数直接传递请求和响应对象
  • String类型的参数需要检查是否有@SevenRequestParam注解来获取具体的参数值

这种设计充分体现了Spring MVC的灵活性和易用性,开发者只需要通过注解就能轻松地处理各种请求。

分发器Servlet的核心实现

整个迷你版Spring框架的核心是SevenDispatcherServlet,它继承自HttpServlet,实现了Spring MVC的前端控制器模式。让我们来看看它的完整实现

java 复制代码
public class SevenDispatcherServlet 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>();

    //保存url和Method的对应关系
    private Map<String,Method> handlerMapping = new HashMap<String,Method>();

    @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 {
        //6、根据url调用method
        try {
            doDispatch(req,resp);
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("500 Exception,Detail: " + Arrays.toString(e.getStackTrace()));
        }
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        //工厂类  GPApplicationContext  IOC、DI

        //=========== IOC ===========

        //1、加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
        
        //2、扫描相关的类
        doScanner(contextConfig.getProperty("scanPackage"));
        
        //3、初始化扫描到的类,并且放入到IOC容器之中
        doInstance();

        //=========  DI =========

        //4、完成自动化的依赖注入
        doAutowired();

        //======= MVC =============
        
        //5、初始化HandlerMapping
        doInitHandlerMapping();

        System.out.println("GP Spring framework is init.");
    }
}

SevenDispatcherServlet的初始化过程严格按照以下顺序执行:

  1. 初始化配置:读取配置文件,获取需要扫描的包路径
  2. 类扫描:扫描指定包路径下的所有类
  3. 实例化:创建被注解标记的类的实例并放入IoC容器
  4. 依赖注入:自动注入各个对象间的依赖关系
  5. 映射初始化:建立URL与方法的映射关系

这个顺序非常重要,因为每一步都依赖于前一步的结果:

  1. 必须先扫描到类才能实例化
  2. 必须先有实例才能进行依赖注入
  3. 必须先有实例和依赖关系才能建立URL映射

通过这个迷你版的实现,我们可以清楚地看到Spring框架的核心机制并不神秘,而是基于Java的基本特性(反射、注解、动态代理等)实现的。

总结

通过这个300行代码的迷你版Spring框架,我们成功实现了Spring的三大核心特性之二

核心设计思想

  • 控制反转(IoC):通过容器管理对象的生命周期,将对象创建的控制权从程序员转移到容器,降低了代码的耦合度。
  • 依赖注入(DI):通过反射机制自动将依赖关系注入到对象中,无需手动创建依赖对象,使代码更加简洁和易于维护。
  • 单一职责原则:每个组件都有明确的职责,如SevenDispatcherServlet负责请求分发,IoC容器负责对象管理等。

实现亮点

  • 注解驱动:通过自定义注解实现了声明式的组件注册和配置,这是Spring框架的一大特色。
  • 反射机制:大量使用反射来实现动态的对象创建、依赖注入和方法调用,体现了框架的灵活性。
  • 约定优于配置:通过约定的命名规则(如类名首字母小写)减少了配置的复杂性。
  • 分层架构:清晰地分离了IoC/DI和MVC的功能,便于理解和扩展。

通过手写这个迷你版Spring框架,我们不仅掌握了Spring的核心原理,更重要的是学会了如何运用设计模式和编程技巧来解决实际问题。这种从简到繁的学习方式有助于我们更深入地理解复杂框架的设计思想,为我们进一步学习和使用Spring框架奠定了坚实的基础。

实际上,真正的Spring框架比这复杂得多,包含了更多高级特性如AOP、事务管理、缓存、安全等,但其核心思想与这个迷你版是一致的。掌握这些基本原理后,再学习完整的Spring框架就会事半功倍。

相关推荐
小垣2 小时前
java调用yolo26n.onnx模型输出图像推理检测
java·人工智能·深度学习·onnx
8Qi82 小时前
微服务通信:同步 vs 异步与MQ选型指南
java·分布式·微服务·云原生·中间件·架构·rabbitmq
晨港飞燕2 小时前
Idea识别Freemarker语法并高亮显示
java·ide·intellij-idea
九皇叔叔2 小时前
001-SpringSecurity-Demo 创建项目
java·springboot·springsecurity
不会kao代码的小王2 小时前
Linux 下 Tomcat 结合内网穿透 实现 Web 应用公网访问
java
ooseabiscuit2 小时前
springboot下使用druid-spring-boot-starter
java·spring boot·后端
青衫码上行2 小时前
【从零开始学习JVM】内存模型+堆栈的区别
java·jvm·学习·面试
迈巴赫车主2 小时前
蓝桥杯 19717 挖矿java
java·开发语言·数据结构·算法·职场和发展·蓝桥杯
yaaakaaang2 小时前
四、建造者模式
java·建造者模式