目录
Bean的加载方式
1.配置文件 + 标签
2.配置文件扫描 + 注解定义bean
3.注解方式声明配置类
导入XML格式配置的bean
proxyBeanMethods属性
4.使用@Impore注入bean
5.编程形式注册bean
6.导入实现了ImportSelector接口类
7.导入实现了ImportBeanDefinitionRegistrar接口的类
8.导入实现了BeanDefinitionRegistryPostProcessor接口的类
Conditional - bean的加载控制
bean的依赖属性配置管理
自动配置原理(工作流程)
所以第一种方式就是给出bean的类名,至于内部嘛就是反射机制加载成class,然后,就没有然后了,拿到了class你就可以搞定一切了。
这里可以使用的注解有@Component以及三个衍生注解@Service、@Controller、@Repository。
@Component("tom")
public class Cat {
}
@Service
public class Mouse {
}
@Component
public class DbConfig {
@Bean
public DruidDataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}
}
@ComponentScan({"com.itheima.bean","com.itheima.config"})
public class SpringConfig3 {
@Bean
public DogFactoryBean dog(){
return new DogFactoryBean();
}
}
这里使用了FactoryBean接口
补充一个小知识,spring提供了一个接口FactoryBean,也可以用于声明bean,只不过实现了FactoryBean接口的类造出来的对象不是当前类的对象,而是FactoryBean接口泛型指定类型的对象。如下列,造出来的bean并不是DogFactoryBean,而是Dog。
public class DogFactoryBean implements FactoryBean {
@Override
public Dog getObject() throws Exception {
Dog d = new Dog();
//.........
return d;
}
@Override
public Class> getObjectType() {
return Dog.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
有人说,注释中的代码写入Dog的构造方法不就行了吗?干嘛这么费劲转一圈,还写个类,还要实现接口,多麻烦啊。还真不一样,你可以理解为Dog是一个抽象后剥离的特别干净的模型,但是实际使用的时候必须进行一系列的初始化动作。只不过根据情况不同,初始化动作不同而已。如果写入Dog,或许初始化动作A当前并不能满足你的需要,这个时候你就要做一个DogB的方案了。然后,就没有然后了,你就要做两个Dog类。当时使用FactoryBean接口就可以完美解决这个问题。
由于早起开发的系统大部分都是采用xml的形式配置bean,现在的企业级开发基本上不用这种模式了。 但是如果你特别幸运,需要基于之前的系统进行二次开发,这就尴尬了。新开发的用注解格式,之前开发的是xml格式。这个时候可不是让你选择用哪种模式的,而是两种要同时使用。
spring提供了一个注解可以解决这个问题,@ImportResource,在配置类上直接写上要被融合的xml配置文件名即可,算的上一种兼容性解决方案
@Configuration
@ImportResource("applicationContext1.xml")
public class SpringConfig32 {
}
@Configuration这个注解,当我们使用AnnotationConfigApplicationContext加载配置类的时候,配置类可以不添加这个注解。但是这个注解有一个更加强大的功能,它可以保障配置类中使用方法创建的bean的唯一性。为@Configuration注解设置proxyBeanMethods属性值为true即可,由于此属性默认值为true
@Configuration(proxyBeanMethods = true)
public class SpringConfig33 {
@Bean
public Cat cat(){
return new Cat();
}
}
下面通过容器再调用上面的cat方法时,得到的就是同一个对象了。注意,必须使用spring容器对象调用此方法才有保持bean唯一性的特性。
public class App33 {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig33.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
System.out.println("-------------------------");
SpringConfig33 springConfig33 = ctx.getBean("springConfig33", SpringConfig33.class);
System.out.println(springConfig33.cat());
System.out.println(springConfig33.cat());
System.out.println(springConfig33.cat());
}
}
使用扫描的方式加载bean是企业级开发中常见的bean的加载方式,但是由于扫描的时候不仅可以加载到你要的东西,还有可能加载到各种各样的乱七八糟的东西。
比如你扫描了com.itheima.service包,后来因为业务需要,又扫描了com.itheima.dao包,你发现com.itheima包下面只有service和dao这两个包,这就简单了,直接扫描com.itheima就行了。但是万万没想到,十天后你加入了一个外部依赖包,里面也有com.itheima包,这下就热闹了,该来的不该来的全来了。
所以我们需要一种精准制导的加载方式,使用@Import注解就可以解决你的问题。它可以加载所有的一切,只需要在注解的参数中写上加载的类对应的.class即可。
@Import({Dog.class,DbConfig.class})
public class SpringConfig4 {
}
用@Import注解注入配置类
除了加载bean,还可以使用@Import注解加载配置类。其实本质上是一样的,不解释太多了。
@Import(DogFactoryBean.class)
public class SpringConfig4 {
}
前面介绍的加载bean的方式都是在容器启动阶段完成bean的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载bean。通过这种方式可以实现编程式控制bean的加载。
public class App5 {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
//上下文容器对象已经初始化完毕后,手工加载bean
ctx.register(Mouse.class);
}
}
实现ImportSelector接口的类可以设置加载的bean的全路径类名,记得一点,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
//各种条件的判定,判定完毕后,决定是否装载指定的bean
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
if(flag){
return new String[]{"com.itheima.bean.Dog"};
}
return new String[]{"com.itheima.bean.Cat"};
}
}
例子中的metadata是获取导入这个selector的配置类的源信息。可以从源信息中获取到配置类上的一些信息,可以通过这个信息进行自定义的逻辑判断来决定添加什么bean。
方式六中提供了给定类全路径类名控制bean加载的形式, 其实bean的加载不是一个简简单单的对象,spring中定义了一个叫做BeanDefinition的东西,它才是控制bean初始化加载的核心。BeanDefinition接口中给出了若干种方法,可以控制bean的相关属性。说个最简单的,创建的对象是单例还是非单例,在BeanDefinition中定义了scope属性就可以控制这个。如果你感觉方式六没有给你开放出足够的对bean的控制操作,那么方式七你值得拥有。
我们可以通过定义一个类,然后实现ImportBeanDefinitionRegistrar接口的方式定义bean
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
registry.registerBeanDefinition("bookService",beanDefinition);
}
}
上述七种方式都是在容器初始化过程中进行bean的加载或者声明,但是这里有一个bug。这么多种方式,它们之间如果有冲突怎么办?谁能有最终裁定权?
BeanDefinitionRegistryPostProcessor,看名字知道,BeanDefinition意思是bean定义,Registry注册的意思,Post后置,Processor处理器,全称bean定义后处理器,干啥的?在所有bean注册都折腾完后,它把最后一道关,说白了,它说了算,这下消停了,它是最后一个运行的。
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
registry.registerBeanDefinition("bookService",beanDefinition);
}
}
@ConditionalOnClass -- 注解实现了当虚拟机中加载了com.itheima.bean.Wolf类时加载对应的bean
@Bean
@ConditionalOnClass(name = "com.itheima.bean.Wolf")
public Cat tom(){
return new Cat();
}
@ConditionalOnMissingClass -- 注解控制虚拟机中没有加载指定的类才加载对应的bean。
@Bean
@ConditionalOnMissingClass("com.itheima.bean.Dog")
public Cat tom(){
return new Cat();
}
这种条件还可以做并且的逻辑关系,写2个就是2个条件都成立,写多个就是多个条件都成立。
@Bean
@ConditionalOnClass(name = "com.itheima.bean.Wolf")
@ConditionalOnMissingClass("com.itheima.bean.Mouse")
public Cat tom(){
return new Cat();
}
@ConditionalOnWebApplication -- 判定当前容器环境是否是web环境。
@Bean
@ConditionalOnWebApplication
public Cat tom(){
return new Cat();
}
@ConditionalOnNotWebApplication -- 判定容器环境是否是非web环境。
@Bean
@ConditionalOnNotWebApplication
public Cat tom(){
return new Cat();
}
@ConditionalOnBean -- 判定是否加载了指定名称的bean
@Bean
@ConditionalOnBean(name="jerry")
public Cat tom(){
return new Cat();
}
当Spring容器中包含了id = jerry的bean,才加载
public class SpringConfig {
@Bean
@ConditionalOnClass(name="com.mysql.jdbc.Driver")
public DruidDataSource dataSource(){
return new DruidDataSource();
}
}
判定当前是否加载了mysql的驱动类,如果加载了,我就给你搞一个Druid的数据源对象出来,完美!
cartoon:
cat:
name: "图多盖洛"
age: 5
mouse:
name: "泰菲"
age: 1
然后定义一个封装属性的专用类,加载配置属性,读取对应前缀相关的属性值。
@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}
最后在使用的位置注入对应的配置即可。
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse{
@Autowired
private CartoonProperties cartoonProperties;
}
建议在业务类上使用@EnableConfigurationProperties声明bean,这样在不使用这个类的时候,也不会无故加载专用的属性配置类CartoonProperties,减少spring管控的资源数量。
public class CartoonCatAndMouse{
}
cartoon:
cat:
name: "图多盖洛"
age: 5
mouse:
name: "泰菲"
age: 1
@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse{
private CartoonProperties cartoonProperties;
}
5.当然,你也可以为当前自动配置类设置上激活条件,例如使用@CondtionOn* * * * 为其设置加载条件
@ConditionalOnClass(name="org.springframework.data.redis.core.RedisOperations")
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse {
private CartoonProperties cartoonProperties;
}
这里只是随便举例,当虚拟机中存在RedisOperations类时,加载这个配置类
springboot为我们开放了一个配置入口,在配置目录中创建META-INF目录,并创建spring.factories文件,在其中添加设置,说明哪些类要启动自动配置就可以了。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itheima.bean.CartoonCatAndMouse
总结:
springboot启动时先加载spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置项,将其中配置的所有的类都加载成bean
在加载bean的时候,bean对应的类定义上都设置有加载条件,因此有可能加载成功,也可能条件检测失败不加载bean
对于可以正常加载成bean的类,通常会通过@EnableConfigurationProperties注解初始化对应的配置属性类并加载对应的配置
配置属性类上通常会通过@ConfigurationProperties加载指定前缀的配置,当然这些配置通常都有默认值。如果没有默认值,就强制你必须配置后使用了