SpringBoot学习day6
文章目录
- spring中注入bean的几种方式
- bean的注入控制
- bean的依赖配置
- 自动配置原理
spring中注入bean的几种方式
-
通过xml来进行注入,首先我们需要通过创建一个配置文件applicationContext.xml,然后在这个配置文件中通过
<bean></bean>
来注入对应的对象。如下所示:<?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"> <!--id是对应的bean的名字,class就是bean对象对应的全路径 如果没有写id,那么对应的id默认就是它的全路径+#下标 --> <bean id="cat" class="com.demo.domain.Cat"></bean> <bean class="com.demo.domain.Dog"></bean> <bean class="com.demo.domain.Dog"></bean> </beans>
至此我们已经实现了注入bean,如果我们要获取bean时候,我们需要通过读取spring的核心配置文件,然后通过ApplicationContext对象调用getBean方法来获取对应的bean对象,其中getBean方法可以有几种方式:
- getBean(对应的Bean的字节码对象):如果通过这一种方式的话,那么就会返回对应的Bean对象,但是这时候如果在Spring容器中有多个这种类型的bean对象的时候,那么调用这个方法的时候就会发生报错,提示这种bean不唯一的错误,因为没有办法知道要拿的是哪个bean。
- getBean(String beanName):通过这一种方式,就会获取对应的beanName名称的bean对象,但是因为没有明确bean是什么类型,所以最后返回的对象是Object对象,所以这时候我们需要进行类型转换。
- getBean(String beanName,Class clazz):通过这种方式,就会获取对应的beanName名称的bean对象,并且应为有传递了这个bean对象的字节码对象,所以可以明确知道这个bean是什么类型,所以最后返回的对象就是我们需要的bean类型,因此不再需要类型转换了。
public class App1 { public static void main(String[] args) { ApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml"); String[] names = application.getBeanDefinitionNames();//获取定义的bean的名字 for(String name : names){ System.out.println(name); } } }
对应的输出为:
cat com.demo.domain.Dog#0 com.demo.domain.Dog#1
如果需要注入第三方技术的bean的时候,通过xml进行注入的时候,也是这样的道理,只是需要我们导入对应的依赖之后才可以。
-
通过注解来注入bean对象:
如果需要通过注解来注入bean对象的时候,我们可以利用一下几个注解:@Component , @Controller, @Service
, @Repository,只是@Component可以在任意层中添加到bean容器中,@Controller则是将controller层中的类添加到bean容器中,@Service,@Repository则是将service层,dao层中的类添加到bean容器中。
只是在使用这些注解的同时,我们还需要在配置文件中利用context命名空间进行组件扫描,只有添加这个语句,才可以将利用上面注解修饰的类添加到bean容器中,否则那些被上面注解修饰的类是没有办法添加到spring容器中的。
对应的applicationContext文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--利用context命名空间进行组件扫描,扫描domain包以及子包,以及config包以及子包--> <context:component-scan base-package="com.demo.domain,com.demo.config"></context:component-scan> </beans>
@Component("杰瑞") //将cat这个类添加到spring容器中,bean的名称为为杰瑞,如果没有设置,那么就是类名,并且第一个字母小写 public class Cat { } @Component public class Dog { }
利用applicationContext调用getBeanDefinitionNames的时候,就可以获取所有的bean对象的名称,对应的代码为:
public class App1 { public static void main(String[] args) { ApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml"); String[] names = application.getBeanDefinitionNames();//获取定义的bean的名字 for(String name : names){ System.out.println(name); } } }
输出为:
杰瑞 dog org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory
但是这时候我们如果将第三方技术注入到spring容器中呢?这时候我们需要利用注解@Bean,这样他就可以将方法的返回值添加到spring容器中了。但是这个方法所在的类必须也是在spring容器中,否则如果这个类是一个普通类,那么即使方法中添加了注解@Bean,spring也不会扫描这个普通类,也就不会进入到这个类的内部了。这时候我们可以利用注解@Configuration来说明这个类是一个配置类,而在@Configuration注解中,也有利用到了注解@Component,那么这时候利用注解@Configuration,也可以将类添加到spring容器中了。
对应的代码为:
//这里使用@Component也是可以的 @Configuration public class DbConfig { @Bean public DataSource dataSource(){ System.out.println("dataSource is init...."); return new ComboPooledDataSource(); } }
这时候对应的输出结果为:
dataSource is init.... 八月 30, 2022 4:43:56 下午 com.mchange.v2.log.MLog 信息: MLog clients using java 1.4+ standard logging. 八月 30, 2022 4:43:57 下午 com.mchange.v2.c3p0.C3P0Registry 信息: Initializing c3p0-0.9.5.2 [built 08-December-2015 22:06:04 -0800; debug? true; trace: 10] 杰瑞 dog dbConfig org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory dataSource
但是这时候既然@Component和@Configuration都可以将配置类添加到spring容器中,那么为什么还需要使用注解@Configuration,换句话说,为什么还需要@Configuration呢?
这时候我们的测试代码为:
public class App1 { public static void main(String[] args) { ApplicationContext application = new ClassPathXmlApplicationContext("applicationContext.xml"); DbConfig dbConfig = application.getBean("dbConfig", DbConfig.class); System.out.println(dbConfig.dog2()); System.out.println(dbConfig.dog2()); System.out.println(dbConfig.dog2()); } }
如果在DbConfig这个类上面使用的是注解@Configuration,那么对应的结果为:
dataSource is init.... 八月 30, 2022 5:15:00 下午 com.mchange.v2.log.MLog 信息: MLog clients using java 1.4+ standard logging. 八月 30, 2022 5:15:00 下午 com.mchange.v2.c3p0.C3P0Registry 信息: Initializing c3p0-0.9.5.2 [built 08-December-2015 22:06:04 -0800; debug? true; trace: 10] dog2 is init...... com.demo.domain.Dog@35e2d654 com.demo.domain.Dog@35e2d654 com.demo.domain.Dog@35e2d654
如果在DbConfig类上面使用的是注解@Component,那么对应的结果为:
dataSource is init.... 八月 30, 2022 5:12:55 下午 com.mchange.v2.log.MLog 信息: MLog clients using java 1.4+ standard logging. 八月 30, 2022 5:12:56 下午 com.mchange.v2.c3p0.C3P0Registry 信息: Initializing c3p0-0.9.5.2 [built 08-December-2015 22:06:04 -0800; debug? true; trace: 10] dog2 is init...... dog2 is init...... com.demo.domain.Dog@51e5fc98 dog2 is init...... com.demo.domain.Dog@7c469c48 dog2 is init...... com.demo.domain.Dog@12e61fe6
为什么有这样的区别呢?这是因为注解@Configuration中存在一个属性proxyMethodBean,默认值为true,这样他会将我们生成一个cglib代理对象,也即第一次调用对应的方法(这个方法需要被@Bean修饰)的时候,他会将返回值添加到spring容器中,然后下次再调用的时候,那么就会直接从spring容器中获取,而不会再次进入到这个方法内部了,如果proxyMethodBean的值为false,那么这时候它的结果和@Component的结果一样,每次调用方法的时候,都需要进入到方法内部执行。而@Component则是每次调用方法的时候,都会进入到方法内部,所以这时候得到的对象是不一样的。可以参考这个文章:@Component和@Configuration
-
使用注解之后,发现还需要applicationContext.xml这个配置文件中,利用context命名空间进行组件扫描,那么能不能指利用注解,而不再需要这个配置文件呢?答案是肯定的,我们需要利用配置类,然后再配置类的上方使用注解@ComponentScan来进行组件扫描即可。这时候因为没有了配置文件,所以在获取ApplicationContext对象的时候,我们不可以在通过ClassPathXmlApplicationContext来获取了,而是通过AnnotationConfigApplicationContext来获取,对应的代码为:
@Configuration @ComponentScan(basePackages = {"com.demo.domain","com.demo.config"}) public class SpringConfig { @Bean public DogFactoryBean factoryBean(){ return new DogFactoryBean(); } @Bean public Cat cat(){ System.out.println("Cat is init.........."); return new Cat(); } }
测试代码为:
public class App3 { public static void main(String[] args) { ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig.class); /* 在注解@Configuration中,有设置属性proxyMethodBean,默认值为true,意思是 当在配置类中创造bean对象的时候,方法返回的是一个新建的新建的对象,这时候 如果它的值为true,那么第一次调用这个方法的时候,将它的返回值添加到spring容器 中,当下次在调用这个方法的时候,不再进入方法内部执行,而是直接从spring容器中取出的,否则 如果它的值为false,那么每次都需要进入到方法内部执行代码,然后返回的对象都是新建的, 所以每次调用得到的对象都是不同的 */ SpringConfig springConfig = app.getBean("springConfig", SpringConfig.class); System.out.println(springConfig.cat()); System.out.println(springConfig.cat()); System.out.println(springConfig.cat()); } }
对应的结果为:
dataSource is init.... 八月 30, 2022 5:25:57 下午 com.mchange.v2.log.MLog 信息: MLog clients using java 1.4+ standard logging. 八月 30, 2022 5:25:58 下午 com.mchange.v2.c3p0.C3P0Registry 信息: Initializing c3p0-0.9.5.2 [built 08-December-2015 22:06:04 -0800; debug? true; trace: 10] Cat is init.......... com.demo.domain.Cat@62150f9e com.demo.domain.Cat@62150f9e com.demo.domain.Cat@62150f9e
-
手动注册bean对象:当我们获取到了AnnotationConfigApplicationContext对象之后,我们通过调用register或者registerBean方法来注册bean对象:
- register(Class clazz): 注册对应的字节码对象为bean对象,这时候bean对象的名字就是它的类名,并且第一个字母为小写。这时候对应的类可以不使用注解来将其添加到bean也可以,因为register会将其注册的,但是如果这个类已经使用对应的注解,将其添加到spring容器中,那么调用这个方法的时候,并没有生效的。
- registerBean(String beanName,Class clazz,):注册对应的bean对象,并且bean对象的名字是beanName
- registerBean(String beanName,Class clazz,constructionArags):注册对应的bean对象,并且bean对象的名字为beanName,并且有传递可变参数constructionArags,这时候,同一个beanName中多次调用这个方法,并且传递的参数不一样,那么将会以最后一个参数的为主,因为之前的已经被覆盖了。
@Configuration @ComponentScan({"com.demo.domain"}) public class SpringConfig2 { }
对应的实体类代码:
@Component("杰瑞") public class Cat { } @Component public class Dog { public Dog() { } int age; public Dog(int age) { this.age = age; } @Override public String toString() { return "Dog{" + "age=" + age + '}'; } }
测试代码:
public class App4 { public static void main(String[] args) { AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig2.class); //手动注册bean对象 app.register(Cat.class);//注册Cat类型的bean对象,并且bean的名字为全路径 app.registerBean("yellow", Dog.class,1); app.registerBean("yellow",Dog.class,2); app.registerBean("yellow",Dog.class,3); String[] names = app.getBeanDefinitionNames(); for(String name : names){ System.out.println(name); } System.out.println(app.getBean("yellow",Dog.class)); } }
输出结果为:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory springConfig2 杰瑞 dog yellow Dog{age=3}
-
定义一个类,使得这个类实现了ImportSelector接口,然后重写它的方法selectImport,方法的返回值就是要添加的bean对象。然后需要在核心配置类中利用@Import注解来引入这个类,然后就可以进行测试了,对应的代码为:
public class MyImportSelector implements ImportSelector { public String[] selectImports(AnnotationMetadata annotationMetadata) { /* 将字符串数组中的元素添加到spring容器中,对应的元素就是对应的类的全路径, 这时候获取到的bean对象的名字就是它的全路径。但是如果这个类上面使用了 @Component等注解,并且设置它的名字,那么这时候对应的bean对象的名字就是自己设置的 否则,如果没有设置它的名字,或者根本没有使用@Component等注解,那么对应的bean对象的名字 就是它的全路径 */ return new String[]{"com.demo.domain.Dog","com.demo.domain.Cat"}; } }
实体类:
@Component("杰瑞") public class Cat { } public class Dog { public Dog() { } int age; public Dog(int age) { this.age = age; } @Override public String toString() { return "Dog{" + "age=" + age + '}'; } }
核心配置类:
@Configuration @Import(MyImportSelector.class) //利用注解@Import,这样就可以导入对应的类 public class SpringConfig3 { }
测试代码:
public class App5 { public static void main(String[] args) { AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig3.class); String[] names = app.getBeanDefinitionNames(); for(String name : names){ System.out.println(name); } }
测试结果:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory springConfig3 com.demo.domain.Dog 杰瑞
-
定义一个类,让这个类实现接口ImportBeanDefinitionRegistrar,重写它的对应方法,从而注册bean
对应的代码为:
public class MyRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Cat.class).getBeanDefinition(); registry.registerBeanDefinition("Tom猫",beanDefinition); } }
配置类代码:
@Configuration @Import(MyRegistrar.class) public class SpringConfig4 { }
测试类代码:
public class App6 { public static void main(String[] args) { AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig4.class); String[] names = app.getBeanDefinitionNames(); for(String name : names){ System.out.println(name); } } }
运行结果为:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.annotation.internalCommonAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory springConfig4 Tom猫
-
定义一个类,让这个类实现接口BeanDefinitionRegistryPostProcessor接口,然后重写方法,从而将对应的bean注册。值得注意的是,因为是BeanDefinitionRegistryPostProcessor接口,根据它的名称可以知道,它是在bean定义之后,然后再处理的,所以如果这个bean之前已经存在了,那么再这个接口中重新注入相同名字的bean的时候,就会覆盖之前的。对应的代码为:
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor { public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { //注册bean,只是他是在bean定义之后才进行的, BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition(); registry.registerBeanDefinition("bookService",beanDefinition); } public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { } }
同时为了证明这种方式将会覆盖之前的bean,所以需要再次通过实现接口BeanDefinitionRegistrar来注入bean:
public class MyRegistrar1 implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition(); registry.registerBeanDefinition("bookService",beanDefinition); } } public class MyRegistrar2 implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl3.class).getBeanDefinition(); registry.registerBeanDefinition("bookService",beanDefinition); } }
对应的实体类:
public interface BookService { void check(); } @Service("bookService") public class BookServiceImpl1 implements BookService { public void check() { System.out.println("bookService 1 .. "); } } public class BookServiceImpl2 implements BookService { public void check() { System.out.println("bookService 2.... "); } } public class BookServiceImpl3 implements BookService { public void check() { System.out.println("bookService 3 ....... "); } } public class BookServiceImpl4 implements BookService { public void check() { System.out.println("bookService 4......... "); } }
对应的配置类:
@Configuration @Import({BookServiceImpl1.class, MyRegistrar2.class,MyRegistrar1.class, MyPostProcessor.class}) public class SpringConfig5 { }
测试类的代码:
public class App7 { public static void main(String[] args) { ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig5.class); BookService bookService = app.getBean("bookService", BookService.class); bookService.check(); } }
运行结果是根据上面配置类中注解@Import的值来设置的:
- @Import({BookServiceImpl1.class}):那么输出的是bookService 1…
- @Import({BookServiceImpl1.class,MyRegistar1.class})或者@Import({MyRegistar1.class,BookServiceImpl1.class}),都是输出bookService 2…,这说明了优先级是通过实现接口BeanDefinitionRegistrar注入bean这种方式的优先级更加高,如果是@Import({BookServiceImpl1.class,MyRegistar1.class,MyRegistar2.class}),那么输出的是bookService 3…,那么说明同一优先级的时候,那么是根据@Import中的顺序来配置的
- @Import({BookServiceImpl1.class,MyRegistar1.class,MyRegistar1.class,MyPostProcessor.class})还是@Import({MyPostProcessor.class,BookServiceImpl1.class,MyRegistar1.class,MyRegistar1.class}),都是输出的是bookService 4…,说明通过实现接口BeanDefinitionRegistryPostProcessor来注入bean的优先级最高.
-
在配置类的上方使用注解@Import,然后属性classes的值是我们需要导入的字节码对象,这样就可以将对应的字节码对象添加到bean中,这样就不需要在对应的类上方使用@Component等注解来注入bean了,从而降低了解耦。如果导入的是一个配置类,那么在将这个配置类导入bean的同时,这个配置类内部中使用@Bean注解修饰的方法也会注入到bean中。
bean的注入控制
如果我们需要控制bean的注入,那么上面的几种注入bean的方式中,只有以下方式可以实现bean的注入控制:
- 通过实现ImportSelector接口,然后在配置类中利用注解@Import来导入实现类。
- 通过实现BeanDefinitionRegistrar接口,然后在配置类中利用注解@Import来导入对应的实现类
- 通过实现BeanDefinitionRegistrarPostProcessor接口,然后在配置类中利用注解@Import来导入对应的实现类。
- 通过AnnotationConfigApplicationContext对象调用register方法来注册bean的时候,可以进行bean的注入控制。
所以可以拿第一种方式为例,如果能够在项目中找到mysql驱动,那么就可以注入druid数据源为bean,否则不可以注入,而判断这个类是否存在于项目中,则可以通过class.forName(xxxx)
来寻找,如果返回值为null,说明不存在,否则存在.对应的代码为:
public class MyImportSelector2 implements ImportSelector {
public String[] selectImports(AnnotationMetadata annotationMetadata) {
try {
//如果能够找到数据库驱动,那么就需要注入数据源为bean,否则不需要
Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
if(clazz != null){
return new String[]{"com.alibaba.druid.pool.DruidDataSource"};
}
} catch (ClassNotFoundException e) {
//如果clazz没有找到,那么就会发生报错,此时直接返回,不需要注入数据源
return new String[0];
}
return null;
}
}
配置类代码为:
@Configuration
@Import(MyImportSelector2.class)
public class SpringConfig6 {
}
测试类:
public class App8 {
public static void main(String[] args) {
AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig6.class);
String[] names = app.getBeanDefinitionNames();
for(String name : names){
System.out.println(name);
}
}
}
如果我们只导入了druid的依赖,而没有导入mysql-connector-java的依赖,那么这时候运行结果为:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
springConfig6
并没有注入了数据源,否则,如果我们在导入druid以来的同时,也导入了mysql-connector-java依赖,那么这时候运行结果为:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
springConfig6
com.alibaba.druid.pool.DruidDataSource
但是显然,上面方式实现bean的注入控制显然有限繁琐,所以这时候我们需要通过注解来实现bean的注入控制,主要是通过注解@Conditional,但是这个注解的代码为:
public @interface Conditional {
Class<? extends Condition>[] value();
}
也即它的属性值要求是继承了Condition的类,而Condition这个接口的代码为:
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
所以通过自定义类实现了Condition接口,然后重写方法matches,那么就可以控制注入bean,如果重写的方法中返回的是false,那么不可以注入,否则可以注入。因此我们自定义一个类MyCondition,重写matches方法从而控制druid数据源的注入.
public class MyCondition implements Condition {
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//如果返回的是false,那么使用注解@Conditional(MyConditional.class)
//修饰的类或者方法,不会注入到spring容器中,否则返回是true的时候,就可以注入
try {
Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
if(clazz != null){
return true;
}
} catch (ClassNotFoundException e) {
}
return false;
}
}
对应的配置类为:
@Configuration
public class SpringConfig7 {
@Bean
@Conditional(MyCondition.class)
public DataSource druidDataSource(){
return new DruidDataSource();
}
}
这时候测试的时候,如果我们已经导入了mysql-connector-java依赖,那么就可以将数据源注入,否则不可以。
但是通过实现Condition接口来控制bean的注入依旧很麻烦,所以这时候可以使用注解@ConditionalOnxxx来控制bean的注入。但是在使用@ConditionalOnxxxx这个注解的时候,首先我们需要先导入spring-boot-starter
依赖,然后就饿可以使用这些注解了。所以上面的配置类的代码可以修改为:
@Configuration
public class SpringConfig7 {
@Bean
//如果能够找到com.mysql.cj.jdbc.Driver这个类,那么就可以注入这个bean,否则不可以
@ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver")
public DataSource druidDataSource(){
return new DruidDataSource();
}
}
bean的依赖配置
如果我们需要绑定bean的属性的时候,可以使用注解@ConfigurationProperties
,从而可以使得这个bean绑定到了配置文件中的某一个属性了。但是使用@ConfigurationProperties
注解的前提时,这个类是一个bean对象,否则就会提示错误。并且这个类属性要含有get/set方法才可以,并且类的属性成员名要和配置文件中的属性名相同,否则是没有办法进行属性绑定的。
对应的代码如下所示:
public class Cat {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Mouse {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Mouse{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
@Component
@ConfigurationProperties(prefix = "cartoon")
public class CartoonCatAndMouse {
private Cat cat;
private Mouse mouse;
public Cat getCat() {
return cat;
}
public void setCat(Cat cat) {
this.cat = cat;
}
public Mouse getMouse() {
return mouse;
}
public void setMouse(Mouse mouse) {
this.mouse = mouse;
}
public void play(){
System.out.println("cat: " + cat.getName() + ", cat.age = " + cat.getAge() +
" mouse: name = " + mouse.getName() + ", age = "+ mouse.getAge());
}
}
对应的配置文件为:
cartoon:
cat:
name: tom
age: 5
mouse:
name: jerry
age: 3
这时候的CartoonCatAndMouse中的属性成员就可以绑定到了配置文件中的cartoon的属性了。但是这时候会存在一个问题,如果我们的配置文件中并没有存在cat这个属性或者不存在cartoon属性,那么这时候CartoonCatAndMouse就没有办法绑定到了配置文件中的属性,从而导致cat,mouse是null,这时候调用方法play的时候,就会发生空指针异常。这是我们并不想看到的结果,所以这时候我们需要再定义一个类CartoonCatAndMousePropertites,在这个类中同样存在属性cat,mouse,并且将这个类和配置文件中的属性cartoon绑定,这样CartoonCatAndMouse就可以不需要和配置文件中进行绑定了,如果需要将CartoonCatAndMouse中cat,mouse和配置文件中的属性绑定的时候,那么这时候我们只需要注入CartoonCatAndMouseProperties,通过CartoonCatAndMouseProperties调用对应的方法来给cat,mouse赋值即可。
对应的代码为:
@Component
@ConfigurationProperties(prefix = "cartoon")
public class CartoonCatAndMouseProperties {
private Cat cat;
private Mouse mouse;
public Cat getCat() {
return cat;
}
public void setCat(Cat cat) {
this.cat = cat;
}
public Mouse getMouse() {
return mouse;
}
public void setMouse(Mouse mouse) {
this.mouse = mouse;
}
}
对应的CartoonCatAndMouse代码为:
@Component
public class CartoonCatAndMouse {
private Cat cat;
private Mouse mouse;
private CartoonCatAndMouseProperties properties;
public CartoonCatAndMouse(CartoonCatAndMouseProperties properties){
this.properties = properties;
cat = new Cat();
cat.setName(properties.getCat() != null
&& !StringUtils.isEmpty(properties.getCat().getName()) ? properties.getCat().getName() : "tt");
cat.setAge(properties.getCat() != null && properties.getCat().getAge() != null ? properties.getCat().getAge() : 4);
mouse = new Mouse();
mouse.setName(properties.getMouse() != null
&& !StringUtils.isEmpty(properties.getMouse().getName()) ? properties.getMouse().getName() : "mouseTT");
mouse.setAge(properties.getMouse() != null && properties.getMouse().getAge() != null ? properties.getMouse().getAge() : 5);
}
public Cat getCat() {
return cat;
}
public void setCat(Cat cat) {
this.cat = cat;
}
public Mouse getMouse() {
return mouse;
}
public void setMouse(Mouse mouse) {
this.mouse = mouse;
}
public void play(){
System.out.println("cat: " + cat.getName() + ", cat.age = " + cat.getAge() +
" mouse: name = " + mouse.getName() + ", age = "+ mouse.getAge());
}
}
但是这时候会有一个缺陷,那就是当CartoonCatAndMouse不需要绑定配置文件的属性的时候,那么这时候是并不需要CartoonCatAndMouseProperties,但是实际上不管是否需要,都已经将CartoonCatAndMouseProperties注入到bean中,这时候我们如何解决这个问题,也即当加载CartoonCatAndMouse的时候,自动地将CartoonCatAndMouseProperties注入到spring容器中,当不需要加载的时候,就不会将其注入呢?这时候我们只需要在CartoonCatAndMouseProperties上面的@Component注解去掉,并且在CartoonCatAndMouse类的上面添加注解@EnableConfigurationProperties(CartoonCatAndMouseProperties.class),这样就可以在需要加载CartoonCatAndMouse的时候,自动将CartoonCatAndMouseProperties加载到spring容器中了,不需要的时候就不会添加。
自动配置原理
我们启动spring boot项目的时候,它是要进入到启动类的,那么这时候,这个启动类中含有注解@SpringBootApplication,这个注解的代码如下所示:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication
其中关键的注解是:@SpringBootConfiguration,@EnableAutoConfiguration以及@ComponentScan
-
@SpringBootConfiguration的代码如下所示:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration
所以注解@SpringBootConfiguration主要是依赖于注解@Configuration,而@Configuration注解作用相当于@Component(通过观察源码可以知道),用来添加bean,但是却又与@Component不同,因为@Configuration注解中含有属性proxyMethodBean,默认值是true,这样的话,如果我们在配置类中利用注解@Bean来将方法的返回值添加到spring容器中的时候,那么这时候在测试类中通过调用这个方法获取到的对象是和从spring容器中取出的是同一个对象,而如果是通过@Component的话,那么这时候每次调用这个方法取到的对象都是新建的。
-
@ComponentScan主要是用于组件扫描,而括号里面的值则是一个过滤器,表示哪些是不需要扫描的
-
@EnabelAutoConfiguration的代码如下所示:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration{ String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
通过观察代码,可以知道,@EnableAutoConfiguration注解是依赖于@AutoConfigurationPackage以及@Import注解的额,并且还有两个属性值exclude,excludeName,这两个属性值主要是说明哪些类是不需要添加到spring容器中作为bean的,因为注解@SpringBootApplication依赖于这个注解,所以同样拥有这两个属性,所以也可以通过设置这两个属性值来设置哪些类不需要添加到bean中。
那么这时候我们重点来看@EnableAutoConfiguration中的两个注解@AutoConfigrationPackage以及@Import注解。
-
@AutoConfigurationPackage的代码如下所示:
@Import({Registrar.class}) public @interface AutoConfigurationPackage
说明@AutoConfigurationPackage主要依赖于注解
@Import({Registrar.class})
,然后我们来看Registrar
这个类的源代码:static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { Registrar() { } public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0])); } public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata)); } }
因为这个类实现了接口ImportBeanDefinitionRegistrar,所以这个类是用来注入bean的,通过观察它的方法
regiserBeanDefinitions
,通过调试可以知道,(String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0])
的值是我们项目的启动类所在的包名,然后将这个包及其子包下面的类进行扫描,然后添加到spring容器中。所以可以知道注解@AutoConfigurationPackage
是用来扫描启动类所在的包以及子包下面的类,将其添加到spring容器中。 -
@Import({AutoConfigurationImportSelector.class})
我们来到AutoConfigurationImportSelector.class类中,重点看它的代码:
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> { return String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName()); }); AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(annotationMetadata); this.autoConfigurationEntries.add(autoConfigurationEntry); Iterator var4 = autoConfigurationEntry.getConfigurations().iterator(); while(var4.hasNext()) { String importClassName = (String)var4.next(); this.entries.putIfAbsent(importClassName, annotationMetadata); } } protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!this.isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else { /* 这一步获得的之是@EnableAutoConfiguration中的属性值,也即是 说attributes是我们不希望添加到spring容器中的值 */ AnnotationAttributes attributes = this.getAttributes(annotationMetadata); /* 调用方法getCandidateConfigurations,将可以得到我们将上面attributes去掉之后的 类,那么这些类是可能添加到spring容器中的 */ List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); configurations = this.removeDuplicates(configurations); Set<String> exclusions = this.getExclusions(annotationMetadata, attributes); this.checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this.getConfigurationClassFilter().filter(configurations); this.fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions); } } protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { /* 通过调用方法loadFactoryNames,那么就可以获取到可能添加到spring容器中的类的全路径名 */ List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; } public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); } private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { /* 将META-INF包下面的spring.factories文件的内容,添加到spring容器中,然后返回 */ Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); while(urls.hasMoreElements()) { URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { Entry<?, ?> entry = (Entry)var6.next(); String factoryTypeName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryImplementationName = var9[var11]; result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } }
我们的测试代码如下所示:
@SpringBootApplication public class SpringbootBeanPropertiesApplication { public static void main(String[] args) { ConfigurableApplicationContext app = SpringApplication.run(SpringbootBeanPropertiesApplication.class, args); String[] names = app.getBeanDefinitionNames(); for(String name : names){ System.out.println(name); } } }
运行结果中就会多出一些我们不知道的类,原因就是上面将META-INF包下面的spring.factories文件中的内容添加到了spring容器中了。所以如果我们需要实现自动配置,那么我们同样可以在resources包下面定义META-INF/spring.factoreis文件:
然后按照对应的格式,设置org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
的值是我们希望自动配置的类的全路径名,这样就可以将对应的类自动配置到spring容器中了。同时我们也可以利用注解@ConditionalOnxxxx
来控制bean的注入,也可以利用注解@EnableConfigurationProperties
来实现它的依赖配置。例如redis就是这样设置的. -