通过自定义注解向Spring容器中注入个性化Bean

本文总阅读量
本文最后更新于2 分钟前,文中所描述的信息可能已发生改变。

本文主要介绍如何通过 Spring 提供的扩展点实现通过一个注解就能向 Spring 容器中注入一个实例化好的 Bean。

场景

Mybatis 可以通过一个其提供的@Mapper就将产生的Bean实例注入到 Spring 容器中交给 Spring 管理,这是如何做到的呢?

思路

已知:

  • 实现 Spring 的FactoryBean接口我们可以通过这个工厂Bean 来自定义我们要注入容器中的 Bean。
  • 实现ApplicationContextAware接口我们可以获取到applicationContext容器。
  • 实现ImportBeanDefinitionRegistrar接口的Bean可以使用@Import注解进行注入,由于这个接口的特性,注入的并不是这个实现类本身,而是在其中registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry)方法中进行注入一些BeanDefinition
  • @Import注解可以很方便的向容器中注入一个外部Bean
  • ClassPathBeanDefinitionScanner类可以过滤出特定的包下的特定BeanDefinition

可得实现方案:

  1. 我们可以通过自定义一个注解@CustomImport来标识哪些类是我们项目中需要注入到Spring容器中的。注解中可以包含一些如何生成这个类实例的自定义信息,比如注入到容器中后的名称。 具体实现:
    java
    /**
    * Created with Intellij IDEA.
    *
    * @Author: zws
    * @Date: 2024-07-30
    * @Description:
    * 标记注解,被标记的类会被自定义产生实例并导入到容器中
    * 产生这个bean的过程是我们自定义的而不是通过Spring的方式产生
    */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CustomImport {
    	String name() default "";
    }
  2. 定义类CustomImportFactoryBean实现接口FactoryBean来帮我们产生个性化的bean,通过实现接口ApplicationContextAware来获取容器并通过其方法getAutowireCapableBeanFactory().autowireBean(instance)使用Spring容器的自动注入功能,因为在FactoryBean接口中的getObject()方法中无法使用自动注入,只能手动注入属性。 具体实现:
    java
    /**
     * Created with Intellij IDEA.
    *
    * @Author: zws
    * @Date: 2024-07-31
    * @Description: 产生不被Spring管理生命周期的自定义bean实例,自定义产生方式
    * 其自身也是一个bean,会被注入到容器,
    * 当IoC容器通过getBean方法来FactoryBean创建的实例时实际获取的不是FactoryBean 本身而是具体创建的T泛型实例
    */
    public class CustomImportFactoryBean implements FactoryBean<Object>, ApplicationContextAware {
    	//我们要注入的bean的类型
    	private Class<?> type;
    	private ApplicationContext applicationContext;
    	private String beanName;
    	@Override
    	public Object getObject() throws Exception {
    		// 创建我们的自定义的bean,可以通过获取自定义注解的一些属性来进行一些个性化的初始化工作
    		Object instance = type.getDeclaredConstructor().newInstance();
    
    		/*
    		使得容器外的Bean可以使用依赖注入,这里可以使得我们自定义的bean也可以享受Spring提供的依赖注入功能
    		* getAutowireCapableBeanFactory(): 这是从应用程序上下文中获取可进行自动装配的Bean工厂的方法。自动装配的Bean工厂是Spring IoC容器的一部分,负责解析依赖关系并将它们注入到Bean中。
    		* autowireBean(instance): 这个方法用来对指定的Bean实例 (instance) 进行自动装配。自动装配是指自动将Bean所依赖的对象注入到Bean中的过程。
    		* */
    		applicationContext.getAutowireCapableBeanFactory().autowireBean(instance);
    		return instance;
    	}
    
    	@Override
    	public Class<?> getObjectType() {
    		return this.type;
    	}
    
    	@Override
    	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    		this.applicationContext = applicationContext;
    	}
    
    	public void setType(Class<?> type) {
    		this.type = type;
    	}
    
    	public void setBeanName(String beanName) {
    		this.beanName = beanName;
    	}
    
    }
  3. 定义类CustomImportBeanDefinitionRegistry实现接口ImportBeanDefinitionRegistrarResourceLoaderAwareEnvironmentAware,来进行注册我们自己封装的BeanDefinition,后两个接口是为了配置扫描器ClassPathBeanDefinitionScanner用的。 具体实现:
    java
    /**
     * Created with Intellij IDEA.
    *
    * @Author: zws
    * @Date: 2024-07-30
    * @Description:
    * 执行注册自定义bean逻辑的地方,包括获取要扫描的包,要扫描的注解,BeanDefinition的注入
    * 其自身不会被注入到容器
    */
    public class CustomImportBeanDefinitionRegistry implements ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware {
    	private static final Logger log = LoggerFactory.getLogger(CustomImportBeanDefinitionRegistry.class);
    	private Environment environment;
    	private ResourceLoader resourceLoader;
    	@Override
    	public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
    		//参数annotationMetadata是被@Import标注的类上的全部注解信息
    
    		//判断被@Import标注的类或注解上是否有注解@EnableCustomImport
    		//没有则表明没有开启自定义注入,直接返回
    		if(!annotationMetadata.hasAnnotation(EnableCustomImport.class.getName())){
    			return;
    		}
    		//开启了自定义注入
    		//获取注解@EnableCustomImport的属性信息,其中包含了要扫描的包的信息
    		Map<String, Object> attributesMap = annotationMetadata.getAnnotationAttributes(EnableCustomImport.class.getName());
    		//转换为AnnotationAttributes对象,该对象为一个LinkHashMap
    		AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(attributesMap);
    		//使用jdk1.8新特性Optional来优雅处理null
    		/*ofNullable(annotationAttributes): 如果annotationAttributes为null则返回一个值为null的Optional
    		对象,否则返回包含原值的Optional对象。
    		optional.orElseGet(AnnotationAttributes::new): 如果optional是一个值为null的optional对象则会返回AnnotationAttributes::new
    		否则返回optional包装的值,即annotationAttributes。
    		*/
    		annotationAttributes = Optional.ofNullable(annotationAttributes).orElseGet(AnnotationAttributes::new);
    		// 获取需要扫描的包,用于注册BeanDefinition
    		String[] scannerPackagesName = getScannerPackagesName(annotationMetadata, annotationAttributes);
    		// 扫描类路径下带有注解@CustomImport的类注册到BeanDefinitionMap中,第二个参数表示不使用默认的过滤器,默认过滤器会扫描@Component、@ManagedBean、@Named 注解标注的类
    		ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false, environment, resourceLoader);
    		// 只要注册我们自定义注解标注的类
    		scanner.addIncludeFilter(new AnnotationTypeFilter(CustomImport.class));
    		for (String basePackage : scannerPackagesName) {
    			// 在包下查找符号要求的组件信息,即被我们自定义注解@CustomImport标注的类的BeanDefinition信息
    			Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
    			registryBeanDefinition(candidateComponents,registry);
    		}
    
    	}
    
    	/**
    	* 获取我们自定义要扫描的包
    	* @param annotationMetadata 被注解@EnableCustomImport标注的类或注解的全部注解信息
    	* @param annotationAttributes 一个map,注解属性信息键值对
    	* @return
    	*/
    	private String[] getScannerPackagesName(AnnotationMetadata annotationMetadata,AnnotationAttributes annotationAttributes){
    		String[] basePackages = annotationAttributes.getStringArray("basePackages");
    		if(basePackages!=null && basePackages.length>0){
    			//该属性不为空,表明指定了要扫描的包
    			return basePackages;
    		}
    		//没有指定要扫描的包则默认是添加@EnableCustomImport注解的类所在的包及其子包
    		//获取该注解标注的类的全限定名
    		String className = annotationMetadata.getClassName();
    		// 截取出包名
    		String basePackagesName = className.substring(0,className.lastIndexOf("."));
    		return new String[]{basePackagesName};
    	}
    
    	/**
    	* 注册被扫描出来的被@CustomImport标注的类的BeanDefinition
    	* @param candidateComponents Set<BeanDefinition>
    	* @param registry BeanDefinitionRegistry
    	*/
    	private void registryBeanDefinition(Set<BeanDefinition> candidateComponents,BeanDefinitionRegistry registry){
    		for (BeanDefinition beanDefinition : candidateComponents) {
    			/*
    			判断Spring是不是通过注解的方法来进行配置启动的
    			通过注解配置启动注册的BeanDefinition则为AnnotatedBeanDefinition(为BeanDefinition的一种子接口)
    			这里表示我们的目的就是注册注解配置的Bean,且通过AnnotatedBeanDefinition类型
    			的BeadDefinition还可以获取该该Bean上的注解信息
    			*/
    			if(beanDefinition instanceof AnnotatedBeanDefinition){
    				AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
    				// 获取源信息来获取注解信息,看看属性name(Bean的自定义名称)有没有值
    				AnnotationMetadata metadata = annotatedBeanDefinition.getMetadata();
    				Map<String, Object> attributes = metadata.getAnnotationAttributes(CustomImport.class.getName());
    				AnnotationAttributes annotationAttributes = Optional.ofNullable(AnnotationAttributes.fromMap(attributes)).orElseGet(AnnotationAttributes::new);
    				String className = metadata.getClassName();
    				String beanName = annotationAttributes.getString("name");
    				beanName = "".equals(beanName)?className:beanName;
    				log.info("被注入的beanName = {}",beanName);
    				log.info("被注入的className = {}",className);
    				/*
    					构建以CustomImportFactoryBean为原型的AbstractBeanDefinition
    					真正目的是让这个FactoryBean来构建出我们需要的Bean,实现了FactoryBean接口的bean实例
    					在创建过程中并不会生成其本身而是会生成getObject()方法产生的实例,可以让我们自定义产生
    					不被Spring管理生命周期的bean,在applicationContext的getBean方法中有体现
    				*/
    				AbstractBeanDefinition abstractBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(CustomImportFactoryBean.class)
    						// 设置BeanDefinition的属性,为了通过FactoryBean来产生我们自定义的不被Spring管理的Bean
    						// 我们的CustomImportFactoryBean实现类FactoryBean接口,这里设置的是该类的属性
    						// 要产生的bean在容器中的名字与类型,从自定义注解获取的,用于我们在CustomImportFactoryBean中个性化bean
    						.addPropertyValue("beanName", beanName)
    						.addPropertyValue("type", className)
    						// 设置装配模式为自动装配
    						.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
    						// 设置为基础角色
    						.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
    						.getBeanDefinition();
    				//获取bean的时候如果是FactoryBean类型的bean则是根据其getObjectType来进行类型匹配的
    				//并不是匹配的FactoryBean类型
    				registry.registerBeanDefinition(beanName,abstractBeanDefinition);
    			}
    		}
    
    
    	}
    	@Override
    	public void setEnvironment(Environment environment) {
    		this.environment = environment;
    	}
    
    	@Override
    	public void setResourceLoader(ResourceLoader resourceLoader) {
    		this.resourceLoader = resourceLoader;
    	}
    }
  4. 定义一个注解@EnableCustomImport来启动我们通过@CustomImport注解就能向Spring容器中添加Bean的功能。 具体实现:
    java
    /**
     * Created with Intellij IDEA.
    *
    * @Author: zws
    * @Date: 2024-07-30
    * @Description: 启用自定义注入bean的功能的注解
    */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(CustomImportBeanDefinitionRegistry.class)
    public @interface EnableCustomImport {
    	String[] basePackages() default {};
    }

依赖项

此处使用了SpringBoot项目的打包插件实现可执行jar包的构建

xml
<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
			<version>2.7.17</version>
		</dependency>
	</dependencies>
</dependencyManagement>
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter</artifactId>
		<scope>provided</scope>
	</dependency>
</dependencies>
<build>

	<finalName>${project.artifactId}</finalName>
	<!--资源文件的打包-->
	<resources>
		<resource>
			<directory>src/main/resources</directory>
			<includes>
				<include>**/*</include>
			</includes>
			<excludes>
				<exclude>log/*</exclude>
			</excludes>
		</resource>
	</resources>

	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
			<version>2.7.17</version>

			<configuration>
				<!--加了goal则可以不加-->
				<mainClass>com.ecjtu.AppStart</mainClass>
			</configuration>

			<!--必须加-->
			<executions>
				<execution>
					<goals>
						<goal>repackage</goal>
					</goals>
				</execution>
			</executions>
		</plugin>
	</plugins>

</build>

各类之间的类图

  • CustomImportBeanDefinitionRegistryCustomImportBeanDefinitionRegistry类图
  • CustomImportFactoryBeanCustomImportFactoryBean类图

小结

通过Spring提供的可扩展接口FactoryBeanApplicationContextAwareImportBeanDefinitionRegistrarEnvironmentAwareResourceLoaderAware结合Spring提供的@Import与自定义注解,我们可以实现将特定包下的不受Spring容器管理生命周期的个性化Bean加入到容器当中,实现与Spring框架的整合。

示例源码仓库

参考文章

基于ImportBeanDefinitionRegistrar和FactoryBean动态注入Bean到Spring容器中spring注解之@Import注解的三种使用方式

浅析Java与Spring中的SPI机制并利用SpringBoot的自动装配机制实现自己的starter依赖
多线程设计模式之两阶段终止模式
Valaxy v0.18.5 驱动 | 主题 - Yun v0.18.5
本站总访问量
本站访客数 人次
本站已运行0 天0 小时0 分0 秒后缀