浅聊向Spring Ioc容器中注册Bean的多种方式
Spring是一个非常强大的反转控制(IOC)框架,以帮助分离项目组件之间的依赖关系。因此可以说Spring容器对Bean的注册、管理可以说是它的核心内容,最重要的功能部分。
因此本文主要介绍:向Spring容器注册Bean的多种方式。
xml方式(老方式,现在使用得非常的少)
在resource类路径创建一个文件:beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.sayabc.boot2demo1.bean.Person">
<property name="name" value="cuzz"></property>
<property name="age" value="18"></property>
</bean>
</beans>
然后main函数采用ClassPathXmlApplicationContext来启动Spring容器容器:
public static void main(String[] args) {
ApplicationContext applicationContext = createNewApplicationContext();
Person bean = applicationContext.getBean(Person.class);
System.out.println(bean); //Person(name=fsx, age=18)
}
//创建、启动Spring容器
private static ApplicationContext createNewApplicationContext() {
return new ClassPathXmlApplicationContext("classpath:beans.xml");
}
从这便可以看出,这个bean就直接放到Spring容器里面了。
@Configuration @Bean配置类的方式
创建一个配置类:
@Configuration //该注解就相当于一个xml配置文件
public class MainConfig {
@Bean(value = "person")
public Person person() {
return new Person("fsx", 18);
}
}
这样我们使用AnnotationConfigApplicationContext来启动容器了:
//创建、启动Spring容器
private static ApplicationContext createNewApplicationContext() {
return new AnnotationConfigApplicationContext(MainConfig.class);
}
效果同上,同样能向容器中放置一个Bean。
@Bean若不指定value值,bean的id默认为方法名的名称。可以指定init-method,destroy-method方法。但是需要注意:单实例Bean容器是管理bean的init和destroy方法的,但是多实例bean容器只管帮你创建和init,之后Spring就不管了。
@Bean相关注解:@Scope、@Lazy等。
如果是单实例Bean,IOC容器启动时就立马创建Bean,以后获取都从容器里拿(当然你也可以加上@Lazy这个注解,让单实例Bean也懒加载)。如果是多实例Bean,Bean只有获取的时候,获取一次就创建一次。
使用@ComponentScan扫描注册组件
只要标注了注解就能扫描到,如:@Controller @Service @Repository @component
配置类中加上这个注解:
@Configuration //该注解就相当于一个xml配置文件
@ComponentScan("com.fsx")
public class MainConfig {
}
实体类上加上一个组件,让其能扫描到:
@Component
public class Person {
private String name;
private Integer age;
}
启动Spring容器输出可以看到:
//创建、启动Spring容器
private static ApplicationContext createNewApplicationContext() {
return new AnnotationConfigApplicationContext(MainConfig.class);
}
输出为:
Person(name=null, age=null)
备注:这种扫描的方式,请保证一定要有空构造函数,否则报错的。
@ComponentScan有很多属性,可以实现更加精确的扫描。比如:basePackageClasses、includeFilters、excludeFilters、lazyInit、useDefaultFilters等。需要注意的是,要使includeFilters生效,需要useDefaultFilters=false才行,否则默认还是全扫。
FilterType枚举的过滤类型,可以实现注解、正则等的精确匹配。当然也能CUSTOM自己实现接口来过滤,功能不可谓不强大。
@Conditional按照条件向Spring中期中注册Bean
/*
* @author Phillip Webb
* @author Sam Brannen
* @since 4.0
* @see Condition
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* All {@link Condition}s that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();
}
这个接口是Spirng4提供出来的。在SpringBoot底层大量的用到了这个接口来按照条件注册Bean。
从注解的属性value来看,我们可以传入Condition条件,因此我们可以传入系统自带的,也可以我们自己去实现这个接口,按照我们的需求来注册Bean。
从上图可以看出,SpringBoot工程中对此接口有大量的实现。本文通过自己的实现,来看看根据条件注册Bean的强大之处。
比如我们要实现如下功能:
如果系统是windows,给容器中注入"bill",如果系统是linux,给容器中注入"linus"。
public class WindowCondition implements Condition{
/**
* @param context 判断条件
* @param metadata 注释信息
* @return boolean
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
String property = environment.getProperty("os.name");
if (property.contains("Windows")) {
return true;
}
return false;
}
}
需要注意的是,context还有以下方法:
// 能获取ioc使用的beanfactory
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 能获取到类加载器
ClassLoader classLoader = context.getClassLoader();
// 获取到环境变量
Environment environment = context.getEnvironment();
// 获取到Bean定义的注册类
BeanDefinitionRegistry registry = context.getRegistry();
LinuxCondition类的写法略。配置类如下:
@Configuration
public class MainConfig2 {
@Conditional({WindowCondition.class})
@Bean("bill")
public Person person01() {
return new Person("Bill Gates", 60);
}
@Conditional({LinuxCondition.class})
@Bean("linux")
public Person person02() {
return new Person("linus", 45);
}
}
运行:(测试时候可以设置运行时参数:-Dos.name=linux)
结果我们会发现,注册的Bean已经按照我们的条件去注册了
@Conditonal注解不仅可以标注在方法上,还可以标注在类上。如果标注在配置类上,那么若不生效的话,这个配置类所有就将不会再生效了。
@Improt快速导入一个组件
@Improt快速导入特别重要,在SpringBoot自动装配的过程中起到了非常关键的作用。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to import.
*/
Class<?>[] value();
}
@Import可以导入第三方包,或者自己写的类,比较方便,Id默认为全类名(这个需要注意)。
比如新建一个类Color:
public class Color {
}
配置类上:
@Import({Color.class})
@Configuration
public class MainConfig2 {}
ImportSelector和ImportBeanDefinitionRegistrar
ImportSelector
从注解中的注释中可以看出,import除了导入具体的实体类外,还可以导入实现了指定接口的类。现在我们自己来实现一个,编写一个MyImportSelector类实现ImportSelector接口。
public class MyImportSelector implements ImportSelector{
// 返回值就导入容器组件的全类名
// AnnotationMetadata:当前类标注的@Import注解类的所有注解信息
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {"com.cuzz.bean.Car"};
}
}
在配置类中,通过@Import导入
@Import({Color.class, MyImportSelector.class})
@Configuration
public class MainConfig2 {}
这样子我们发现,Car类已经被导入进去了。
ImportBeanDefinitionRegistrar
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata 当前类的注解信息
* @param registry 注册类
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 查询容器
boolean b = registry.containsBeanDefinition("com.cuzz.bean.Car");
// 如果有car, 注册一个汽油类
if (b == true) {
// 需要添加一个bean的定义信息
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Petrol.class);
// 注册一个bean, 指定bean名
registry.registerBeanDefinition("petrol", rootBeanDefinition);
}
}
}
配置类:
@Import({Color.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})
@Configuration
public class MainConfig2 {}
使用FactoryBean注册组件
工厂Bean。此Bean非常的重要,因为第三方框架要和Spring整合,大都是通过实现此接口来实现的。
public interface FactoryBean<T> {
T getObject() throws Exception;
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
举个例子,我自己来实现这个Bean接口:
public class ColorFactoryBean implements FactoryBean<Color> {
// 返回一个Color对象
@Override
public Color getObject() throws Exception {
return new Color();
}
@Override
public Class<?> getObjectType() {
return Color.class;
}
// 是否为单例
@Override
public boolean isSingleton() {
return true;
}
}
通过@Bean注入到容器里:
@Test
public void test05() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
Object bean = applicationContext.getBean("colorFactoryBean");
// 工厂bean调用的是getClass()方法
System.out.println("colorFactoryBean的类型是: " + bean.getClass());
}
发现此时的bean调用的方法是getObjectType方法,输出为:class com.fsx.boot2demo1.bean.Color
如果需要获取FactoryBean本身,可以在id前面加一个“&”标识
Object bean2 = applicationContext.getBean("&colorFactoryBean");
这个时候输出的就是:com.fsx.boot2demo1.bean.ColorFactoryBean
抛出一个问题:为何不直接使用@Bean,而使用FactoryBean呢?
Spring 中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean 即 FactoryBean。FactoryBean跟普通Bean不同,其返回的对象不是指定类的一个实例,而是该FactoryBean的getObject方法所返回的对象。创建出来的对象是否属于单例由isSingleton中的返回决定。
官方解释:
FactoryBean 通常是用来创建比较复杂的bean,一般的bean 直接用xml配置即可,但如果一个bean的创建过程中涉及到很多其他的bean 和复杂的逻辑,用xml配置比较困难,这时可以考虑用FactoryBean。
我的解释:
简单的说:它是用来处理复杂的Bean,在初始化过程中需要做很多事情(比如MyBatis的SqlSessionFactoryBean等等),从而屏蔽内部实现,对调用者/使用者友好的一种解决方案。如果这种Bean用xml去配置,几乎是不可能的。当用注解驱动@Bean去做后,虽然也是可以的,但是很显然对调用很不友好的(因为我们使用MyBatis可不想去知道它初始化到底要做些啥事之类的)。因此使用这个方法是最优雅的解决方案。Spring在1.0就支持了这个接口,优秀~
总结
Spring提供了非常多的方式来向容器内注册Bean,从而来满足各式各样的需求。每种方式都有它独特的使用场景。比如@Bean是最常使用的,@Import导入Bean在SpringBoot的自动装配中得到了大量的使用。
一个成熟的框架很忌讳提供封闭的、不全的功能。而Spring在“开闭原则”上显然无疑是做得非常优秀的,值得深入学习。
参考: |