2025年11月23日

[手写 MiniSpring 框架教程] 第 08 讲:组件扫描机制

📖 本讲目标

  • 理解组件扫描的原理
  • 实现 @Component 注解
  • 实现类路径扫描功能
  • 自动注册 BeanDefinition
  • 理解包扫描的优化策略

🎯 组件扫描概述

什么是组件扫描?

组件扫描(Component Scanning) 是 Spring 框架自动发现和注册 Bean 的机制。通过扫描指定包路径下的所有类,自动识别带 @Component 注解的类,并将其注册为 Bean。

传统方式 vs 组件扫描

传统方式(手动注册):

ApplicationContext context = new ApplicationContext();
BeanDefinition bd = new BeanDefinition(UserService.class);
context.registerBeanDefinition("userService", bd);

组件扫描方式(自动注册):

@Component
public class UserService {
    // 自动被扫描并注册
}

ApplicationContext context = new ApplicationContext("com.example.demo");
// 自动扫描并注册所有 @Component 类

🏷️ @Component 注解实现

注解定义

创建 src/main/java/com/minispring/stereotype/Component.java

package com.minispring.stereotype;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Component 注解用于标记一个类为 Spring 管理的组件
 * 容器在启动时会扫描所有带此注解的类,并将其注册为 Bean
 */
@Target(ElementType.TYPE)  // 只能用于类
@Retention(RetentionPolicy.RUNTIME)  // 运行时可通过反射获取
public @interface Component {
    /**
     * Bean 的名称,如果不指定则使用类名首字母小写作为默认名称
     * 例如:UserService -> userService
     */
    String value() default "";
}

使用示例

@Component
public class UserService {
    // Bean 名称默认为 "userService"
}

@Component("myUserService")
public class UserService {
    // Bean 名称为 "myUserService"
}

🔍 类路径扫描实现

实现思路

类路径扫描的核心流程:

  1. 包路径转换:将包名转换为文件路径(com.examplecom/example
  2. 获取资源:通过 ClassLoader 获取包对应的 URL 资源
  3. 递归扫描:递归扫描目录下的所有 .class 文件
  4. 加载类:使用 Class.forName() 加载类
  5. 处理类:检查是否有 @Component 注解,如果有则注册

实现代码

创建 src/main/java/com/minispring/context/ClassPathBeanScanner.java

package com.minispring.context;

import com.minispring.beans.factory.config.BeanDefinition;
import com.minispring.context.annotation.Lazy;
import com.minispring.context.annotation.Scope;
import com.minispring.stereotype.Component;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * 类路径扫描器,负责扫描指定包下的所有类
 * 识别带有 @Component 注解的类,并将其注册为 BeanDefinition
 */
public class ClassPathBeanScanner {

    private final ApplicationContext context;

    public ClassPathBeanScanner(ApplicationContext context) {
        this.context = context;
    }

    /**
     * 扫描指定包路径下的所有类
     * @param basePackage 基础包路径,如 "com.example.demo"
     */
    public void scan(String basePackage) {
        // 1. 将包路径转换为文件路径
        String path = basePackage.replace('.', '/');

        // 2. 获取类加载器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        try {
            // 3. 获取包对应的 URL 资源
            URL resource = classLoader.getResource(path);
            if (resource == null) {
                throw new RuntimeException("找不到包路径: " + basePackage);
            }

            // 4. 将 URL 转换为 File 对象
            File directory = new File(resource.getFile());

            // 5. 递归扫描目录,获取所有 .class 文件
            List<Class<?>> classes = findClasses(directory, basePackage);

            // 6. 处理每个类,注册为 BeanDefinition
            for (Class<?> clazz : classes) {
                processClass(clazz);
            }

        } catch (Exception e) {
            throw new RuntimeException("扫描包路径失败: " + basePackage, e);
        }
    }

    /**
     * 递归查找目录下的所有类
     * @param directory 目录
     * @param packageName 包名
     * @return 类列表
     */
    private List<Class<?>> findClasses(File directory, String packageName) {
        List<Class<?>> classes = new ArrayList<>();

        if (!directory.exists()) {
            return classes;
        }

        // 获取目录下的所有文件和子目录
        File[] files = directory.listFiles();
        if (files == null) {
            return classes;
        }

        for (File file : files) {
            if (file.isDirectory()) {
                // 递归扫描子目录
                String subPackage = packageName + "." + file.getName();
                classes.addAll(findClasses(file, subPackage));
            } else if (file.getName().endsWith(".class")) {
                // 加载 .class 文件
                String className = packageName + "." +
                        file.getName().substring(0, file.getName().length() - 6);
                try {
                    Class<?> clazz = Class.forName(className);
                    classes.add(clazz);
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException("加载类失败: " + className, e);
                }
            }
        }

        return classes;
    }

    /**
     * 处理单个类,如果带有 @Component 注解,则注册为 BeanDefinition
     * @param clazz 要处理的类
     */
    private void processClass(Class<?> clazz) {
        // 1. 检查是否有 @Component 注解
        Component component = clazz.getAnnotation(Component.class);
        if (component == null) {
            return;  // 不是组件,跳过
        }

        // 2. 创建 BeanDefinition 对象
        BeanDefinition beanDefinition = new BeanDefinition();
        beanDefinition.setBeanClass(clazz);

        // 3. 确定 Bean 名称
        String beanName = component.value();
        if (beanName.isEmpty()) {
            // 如果没有指定名称,使用类名首字母小写作为默认名称
            beanName = toLowerCaseFirstLetter(clazz.getSimpleName());
        }
        beanDefinition.setBeanName(beanName);

        // 4. 处理 @Scope 注解
        Scope scope = clazz.getAnnotation(Scope.class);
        if (scope != null) {
            beanDefinition.setScope(scope.value());
        } else {
            beanDefinition.setScope("singleton");  // 默认单例
        }

        // 5. 处理 @Lazy 注解
        Lazy lazy = clazz.getAnnotation(Lazy.class);
        if (lazy != null) {
            beanDefinition.setLazy(lazy.value());
        } else {
            beanDefinition.setLazy(false);  // 默认非懒加载
        }

        // 6. 注册 BeanDefinition 到容器
        context.registerBeanDefinition(beanName, beanDefinition);

        System.out.println("注册 Bean: " + beanName +
                " [scope=" + beanDefinition.getScope() +
                ", lazy=" + beanDefinition.isLazy() + "]");
    }

    /**
     * 将字符串首字母转换为小写
     * @param str 原字符串
     * @return 首字母小写的字符串
     */
    private String toLowerCaseFirstLetter(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }
        return Character.toLowerCase(str.charAt(0)) + str.substring(1);
    }
}

🔗 集成到 ApplicationContext

更新 ApplicationContext

ApplicationContext 中添加扫描功能:

/**
 * 扫描指定包路径并初始化容器
 * @param basePackage 基础包路径(如 "com.example.demo")
 */
public ApplicationContext(String basePackage) {
    // 1. 扫描并注册 BeanDefinition
    scan(basePackage);

    // 2. 实例化所有非懒加载的单例 Bean
    preInstantiateSingletons();
}

/**
 * 扫描指定包路径,注册所有带 @Component 注解的类
 * @param basePackage 基础包路径
 */
public void scan(String basePackage) {
    ClassPathBeanScanner scanner = new ClassPathBeanScanner(this);
    scanner.scan(basePackage);
    System.out.println("完成包扫描: " + basePackage);
}

🎯 扫描优化策略

1. 过滤策略

可以添加过滤条件,只扫描需要的类:

// 跳过接口和抽象类
if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
    return;
}

// 跳过内部类
if (clazz.isMemberClass()) {
    return;
}

2. 性能优化

  • 缓存扫描结果:避免重复扫描
  • 并行扫描:使用多线程加速扫描
  • 索引文件:使用索引文件快速定位类

3. 路径处理

处理不同环境下的路径问题:

// 处理 JAR 包中的资源
if (resource.getProtocol().equals("jar")) {
    // JAR 包处理逻辑
} else {
    // 文件系统处理逻辑
}

🧪 测试示例

测试类

package com.minispring.demo;

import com.minispring.context.annotation.Scope;
import com.minispring.stereotype.Component;
import com.minispring.context.ApplicationContext;

@Component
class UserRepository {
    public String getName() {
        return "UserRepository";
    }
}

@Component("myUserService")
@Scope("singleton")
class UserService {
    @Autowired
    private UserRepository userRepository;

    public UserRepository getUserRepository() {
        return userRepository;
    }
}

public class ComponentScanTest {
    public static void main(String[] args) throws Exception {
        // 自动扫描并注册
        ApplicationContext context = new ApplicationContext("com.minispring.demo");

        // 获取 Bean
        UserService service = context.getBean("myUserService", UserService.class);
        System.out.println("获取到 Bean: " + service);
        System.out.println("依赖注入成功: " + service.getUserRepository().getName());
    }
}

📝 本讲总结

本讲我们完成了:

  1. ✅ 理解了组件扫描的原理
  2. ✅ 实现了 @Component 注解
  3. ✅ 实现了类路径扫描功能
  4. ✅ 实现了自动注册 BeanDefinition
  5. ✅ 集成了扫描功能到 ApplicationContext
  6. ✅ 理解了扫描优化策略

🎯 关键点回顾

  1. 包路径转换com.examplecom/example
  2. 递归扫描:遍历目录树查找所有 .class 文件
  3. 类加载:使用 Class.forName() 加载类
  4. 注解检查:通过反射检查 @Component 注解
  5. 自动注册:自动创建并注册 BeanDefinition

⚠️ 注意事项

  1. 路径问题:JAR 包和文件系统的路径处理不同
  2. 类加载:需要处理类加载异常
  3. 性能考虑:大量类时扫描可能较慢
  4. 过滤策略:可以添加过滤条件提高效率

🚀 下一讲预告

在下一讲中,我们将学习 AOP 基础概念,包括:

  • 什么是 AOP
  • 切面、切点、通知
  • JDK 动态代理原理
  • AOP 应用场景

准备好了吗?让我们继续 第 09 讲:AOP 基础概念


思考题

  1. 组件扫描的性能如何优化?
  2. 如何处理 JAR 包中的类扫描?
  3. 如何实现更灵活的过滤策略?

“以书为舟,遨游尘世”,
最好的免费 kindle 电子书分享站:

You may also like...

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


*