Spring 优雅注册 Bean 的方式

1、Spring 注册 Bean

这篇先说明用法,下篇分析以下场景是如何将 Bean 注册进 IOC容器的。

1.1、使用 @Bean 注解

这种用法在项目中是非常常见的,基本上是必有。我们来看下用法:

@Configuration
public class TestConfig {

    @Bean
    public TestBean testBean(){
        return new TestBean();
    }
    public static class TestBean{}
}

复制代码

这样一个 Bean 就注册进 IOC 容器了,Bean 的名称默认是方法名,并且是不会转换大小写的,也就是假如你的方法名是 TestBean() ,那么 Bean 的名称就是 TestBean 。当然我们也可以使用 name 或者 value 指定 Bean 的名称,比如 @Bean(value = "testBean"),如果二者同时存在则会报错。

我们来看下其他属性:

autowireCandidate:默认值是 true 。如果设置为 false 的话,那么通过 byType 的方式获取 Bean 就会报错,当然我们可以使用 Resource 注解获取。

initMethod:在 Bean 实例化后调用的初始化方法,值是 Bean 类中的方法名。

destroyMethod:在 Bean 要销毁时调用的清理方法,值是 Bean 类中的方法名。

@Bean 注解只能定义在 @Configuration 类下吗? NO NO NO,它可以定义在任意能被 IOC 扫描的注解下,比如 @Component注解,至于区别,下篇再讲。

1.2、使用 @ComponentScan 注解组件扫描

先讲普通用法:

@ComponentScan(basePackages = "com.rookie.spring.source.run.component")
@Configuration
public class TestConfig {
}
复制代码

使用 @ComponentScan 组件扫描方式,它会扫描指定包下(包括子包)下的所有类,只要包含了 @Component、@Configuration 等 Spring 的声明注解,就会将 Bean 加入到 IOC 容器中。

深度用法:

ComponentScan 注解中有两个这样的属性:includeFilters 与 excludeFilters,前一个是只包含规则,后一个是排除包含规则,他们的值是一个 @Filter 注解的形式,Filter 中的 type 有 5 中类型,分别如下。

1、ANNOTATION

第一种是以注解的形式包含或不包含,比如:

@ComponentScan(basePackages = "com.rookie.spring.source.run.component",
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class),
        useDefaultFilters = false)
复制代码

这里边要配置useDefaultFilters = false 禁用默认规则,因为默认规则是扫描所有,配只包含就没用了。这里的意思只扫描 Configuration 注解。

2、ASSIGNABLE_TYPE

这种是包含我们给定的类型,不管是给定的类型和子类都会被包含进 IOC 容器。

public class TestBean1 extends TestBean {
}
public class TestBean {
}

@ComponentScan(basePackages = "com.rookie.spring.source.run.component",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class),
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,classes = TestBean.class)},
        useDefaultFilters = false)
@Configuration
public class TestConfig {
}
复制代码

然后我们发现 testBean 注册进去了,为什么我们不标注 @Component 这样的注解实例也会被注册进 IOC 呢?因为 ComponentScan 会扫描包下所有文件,只要符合我们定义的过滤规则,它就会将 Bean 注册进 IOC 容器中。

3、ASPECTJ

ASPECTJ 是使用 aspectj 表达式

4、REGEX

REGEX 是使用正则表达式

5、CUSTOM

这种呢就是我们 SpringBootApplication 注解用到的方式了,我来解释一下具体规则:这种方式是可以自己自定义扫描规则,它接受一个实现 TypeFilter 接口的类。

public class MyTypeFilter implements TypeFilter {
    /**
     *
     * @param metadataReader 当前类的信息
     * @param metadataReaderFactory 可以获取其他类的信息
     * @return 匹配结果
     * @throws IOException 异常
     */
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
//        获取当前扫描类信息
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        return classMetadata.getClassName().contains("com.rookie.spring.source.run.component.TestBean");

    }
}

@ComponentScan(basePackages = "com.rookie.spring.source.run.component",
        includeFilters = {@ComponentScan.Filter(type = FilterType.CUSTOM,classes = MyTypeFilter.class)},
        useDefaultFilters = false)
@Configuration
public class TestConfig {
}
复制代码

当它扫描类的时候扫描到了 TestBean,然后符合了我的匹配规则(也就是返回true)就注册进去了。

1.3、使用 @Import 注解

下面的例子中,我们直接看 Spring 源码的实现比较具有代表性一点。

我们点进 @EnableTransactionManagement 注解中,发现了这个 @Import(TransactionManagementConfigurationSelector.class),它的作用就是将类导入,类会被注册进 IOC 容器中。

这个注解放置的位置要是 Spring 能扫描到的地方,不然 Spring 也不会主动去解析这个注解。

如果我们自己要使用注解的话,我们可以做个类似于 EnableTransactionManagement 的功能插拔式导入配置类,这样就可以实现动态开启一些 Bean 了。

1.4、实现 ImportSelector 接口

public interface ImportSelector {

	/**
	 * Select and return the names of which class(es) should be imported based on
	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

}

复制代码

我们还是来看下 TransactionManagementConfigurationSelector 这个类,看下它的继承关系发现它间接性的实现了 ImportSelector 接口,主要看它实现的这个方法:

@Override
	protected String[] selectImports(AdviceMode adviceMode) {
		switch (adviceMode) {
			case PROXY:
				return new String[] {AutoProxyRegistrar.class.getName(),
						ProxyTransactionManagementConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {determineTransactionAspectClass()};
			default:
				return null;
		}
	}
复制代码

这个方法的作用就是根据你返回的类全限定名(org.springframework.context.annotation.AutoProxyRegistrar)数组来创建 Bean 。

实现了 ImportSelector 的类也是需要使用 @Import 导入。

1.5、实现 ImportBeanDefinitionRegistrar 接口

public interface ImportBeanDefinitionRegistrar {

	/**
	 * Register bean definitions as necessary based on the given annotation metadata of
	 * the importing {@code @Configuration} class.
	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
	 * registered here, due to lifecycle constraints related to {@code @Configuration}
	 * class processing.
	 * @param importingClassMetadata annotation metadata of the importing class
	 * @param registry current bean definition registry
	 */
	void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);

}
复制代码

这个我们来看下 @MapperScan (org.mybatis.spring.annotation)导入的 MapperScannerRegistrar 发现它实现了 ImportBeanDefinitionRegistrar:

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        if (mapperScanAttrs != null) {
            this.registerBeanDefinitions(mapperScanAttrs, registry);
        }

    }
复制代码

它的作用是拿到 BeanDefinitionRegistry Bean 的定义信息,然后往里面加 BeanDefinition 就会将相应的对象注册进去,它更深入的就不说了,实际上就是解析下注解属性,然后扫描相应的包下的类注册 Bean。我们自己搞个简单的。

registry.registerBeanDefinition("testBean",new RootBeanDefinition(TestBean.class));
复制代码

这样就注册了一个 Bean 名称是 testBean 类型是 TestBean 类型的 Bean 了。

如果注册的是一个有参构造器呢?那就这样:

BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class);
        beanDefinitionBuilder.addConstructorArgValue(1);
        registry.registerBeanDefinition("testBean",beanDefinitionBuilder.getBeanDefinition());
复制代码

addConstructorArgValue 根据构造器参数的顺序去添加。

实现了 ImportBeanDefinitionRegistrar 的类也是需要使用 @Import 导入。

1.6、实现 FactoryBean 接口

public class MyFactoryBean implements FactoryBean<TestBean> {

    @Override
    public TestBean getObject() throws Exception {
        return new TestBean();
    }

    @Override
    public Class<?> getObjectType() {
        return TestBean.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

@Import(MyFactoryBean.class)
@Configuration
public class TestConfig {
}

复制代码

然后 TestBean 就注册进去了,打印的时候我们发现 Bean 的名称是 MyFactoryBean 的全限定名,但是它的类型是 TestBean 类型的,如果想要获取 MyFactoryBean 类型的 Bean 的话,通过 Bean 名称为 &myFactoryBean 就能获取到。

1.7、使用 spring.factories 配置

在我们的Spring Boot项目中,一般都是只扫描主类下的所有类,然后将一些被特定注解标注的类加载到IOC容器,但是如果我们将包分离,我们又如何更加方便的将其他包的类加载进来呢? spring boot提供了一种类似于Java的SPI(服务发现)机制spring.factories,只要在resources目录下创建META-INF文件夹,再创建 spring.factories文件,然后再里面配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.jylcloud.jyl.common.commondcs.config.RedissonManagerConfig

复制代码

这样在导入当前包的就会自动扫描spring.factories文件,解析后将里面的一些类加载到IOC容器中。具体的实现代码在spring-core的SpringFactoriesLoader类中。

1.8、使用 @Component、@Repository、@Service、@Controller、@RestController

这些就不讲了。

2、总结

就不总结了,看着用。还有其他注册 Bean 的方式放置在其他地方讲。

原文地址