SpringBoot如何批量注入Bean到Spring容器中?

本文档展示了如何模仿Mybatis的MapperScan功能,实现在Spring环境中批量注册Bean。通过创建自定义注解@ListenerScan,配合ClassScanner工具类扫描指定包下的类,并使用BeanDefinitionRegister实现Bean的注册。最终,即使不使用@Component等注解,也能将类注册到Spring容器中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

需求

实现

项目结构

待注入类

ClassScanner工具类

ListenerScan注解

BeanDefinitionRegister

配置文件

启动类

结果


需求

标题有些许拗口

简单来说,就是实现mybatis框架中的MapperScan功能(接口扫描与代理对象注册)

在Spring环境中,我们常通过XML文件 或者 @Controller、@Component、@Service、@Bean等注解进行bean的注册。

这么做比较简单便捷,但同时我们自己的代码与服务与Spring框架紧密耦合,只要SpringBoot启动,我们的类就会被加载、注册进容器,无法进行策略的替换。

比如某个场景下,我需要注册package1中的bean;另一个场景下,我需要注册package2中的bean。

当然,用SpringBoot神奇的@Conditional注解,也可以达到条件注册的目的。但这种方法不符合“批量”这一特征。

在Mybatis中也有类似的需求。Mybatis与SpringBoot集成时,Mapper接口(其实注入容器的是代理对象)是如何被批量注册进Bean容器的?

通过MapperScan注解指定包名,将该包下的所有类加载到内存中,生成代理对象后注册进spring容器。

源码有兴趣的可以自己阅读一下,下面仿造mybatis写一个bean批量注册的功能。

实现

项目结构

大概结构如下

待注入类

 listener包下有两个测试类,是我们需要注入的对象。

UserInfo无关紧要,只是用来测试Autowired注解后续能否生效的,忽略即可。

public class HelloListener {

    @Autowired
    UserInfo userInfo;

    public void hello() {
        System.out.println("hello");
    }
}

public class WorldListener {

    public void world() {
        System.out.println("world");
    }
}

ClassScanner工具类

比较关键的工具类,用于加载指定package下的class

public class ClassScanner {

    private final String packageName;

    private final String packageUrl;

    private final ClassLoader classLoader;

    private final Charset charset;

    private final Set<Class<?>> classes = new HashSet<>();

    public ClassScanner(String packageName, ClassLoader classLoader) {
        this.packageName = packageName;
        this.classLoader = classLoader;
        this.charset = StandardCharsets.UTF_8;
        this.packageUrl = packageName.replace(".", File.separator);
    }

    public ClassScanner(String packageName, ClassLoader classLoader, Charset charset) {
        this.packageName = packageName;
        this.classLoader = classLoader;
        this.charset = charset;
        this.packageUrl = packageName.replace(".", File.separator);
    }

    public Set<Class<?>> scan() {
        try {
            Enumeration<URL> resources = classLoader.getResources(packageUrl);
            EnumerationIter<URL> urls = new EnumerationIter<>(resources);
            for (URL url : urls) {
                scanFile(new File(URLDecoder.decode(url.getFile(), charset.name())));
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        return classes;
    }

    private void scanFile(File file) {
        if(file == null) {
            return;
        }
        if(file.isFile()) {
            final String fileName = file.getName();
            if(fileName.endsWith(".class")) {
                String className = packageName + "." + fileName.substring(0, fileName.lastIndexOf('.'));
                addClass(className);
            }
        }else if(file.isDirectory()) {
            if(file.listFiles() == null){
                return;
            }

            for (File listFile : file.listFiles()) {
                scanFile(listFile, packageName);
            }
        }
    }

    /**
     * 嵌套扫描
     * @param file 文件
     * @param basePackage 基础包名
     */
    private void scanFile(File file, String basePackage) {
        if(file == null) {
            return;
        }

        if(file.isFile()) {
            final String fileName = file.getName();
            if(fileName.endsWith(".class")) {
                String className = basePackage + "." + fileName.substring(0, fileName.lastIndexOf('.'));
                addClass(className);
            }

        }else if(file.isDirectory()) {
            if(file.listFiles() == null){
                return;
            }
            basePackage = basePackage + "." + file.getName();

            for (File listFile : file.listFiles()) {
                scanFile(listFile, basePackage);
            }
        }
    }

    public void addClass(String className) {
        Class<?> aClass = null;
        try {
            aClass = classLoader.loadClass(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        classes.add(aClass);
    }
}

迭代器

public class EnumerationIter<E> implements Iterator<E>, Iterable<E>, Serializable {
    private static final long serialVersionUID = 1L;

    private final Enumeration<E> e;

    public EnumerationIter(Enumeration<E> enumeration) {
        this.e = enumeration;
    }

    @Override
    public boolean hasNext() {
        return e.hasMoreElements();
    }

    @Override
    public E next() {
        return e.nextElement();
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Iterator<E> iterator() {
        return this;
    }
}

ListenerScan注解

仿造@MapperScan,定义一个@ListenerScan

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({BeanDefinitionRegister.class})
public @interface ListenerScan {

    @AliasFor("basePackages")
    String[] value()  default {} ;

    @AliasFor("value")
    String[] basePackages() default {};
}

BeanDefinitionRegister

批量注册Bean需要集成Spring提供的ImportBeanBeanDefinitionRegistrar接口。

(mybatis-spring也是这么做的)

public class BeanDefinitionRegister implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(ListenerScan.class.getName()));
        if(annoAttrs != null) {
            String[] stringArray = annoAttrs.getStringArray("value");
            for (String packageName : stringArray) {
                ClassScanner classScanner = new ClassScanner(packageName, Thread.currentThread().getContextClassLoader());
                Set<Class<?>> scan = classScanner.scan();
                for (Class<?> aClass : scan) {
                    if(aClass.isInterface() || aClass.isAnonymousClass() || aClass.isEnum()) {
                        continue;
                    }
                    registry.registerBeanDefinition(aClass.getName(), new RootBeanDefinition(aClass));
                }
            }
        }
    }
}

配置文件

配置文件引入我们自定义的Register

@Import({BeanDefinitionRegister.class})
@Configuration
@ConditionalOnBean
public class CustomerConfig {
}

启动类

启动类加上我们的注解,并尝试获取容器中的对象。

    @ListenerScan("com.dayrain.springbootdemo.listener")
    @SpringBootApplication
    public class SpringbootDemoApplication {

        public static void main(String[] args){
            ConfigurableApplicationContext context = SpringApplication.run(SpringbootDemoApplication.class, args);
            Map<String, HelloListener> helloListenerMap = context.getBeansOfType(HelloListener.class);
            Map<String, WorldListener> worldListenerMap = context.getBeansOfType(WorldListener.class);
            System.out.println(helloListenerMap);
            System.out.println(worldListenerMap);
        }

    }

结果

可以看到,即使我们没加@Component之类的注解,HelloListener、WorldListener也被注册进spring容器了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值