自动配置原理分析
SpringBoot
就是对Spring
的一种高度封装,我们也需要深入的学习其原理,方便后续的使用和拓展
Condition
Condition
是在Spring4.0
增加的条件判断功能,通过这个功能可以实现选择性的创建Bean
操作(满足条件则创建,不满足则不创建)
案例:在Spring
的IOC
容器中有一个User
的Bean
,导入Jedis
坐标后,就加载该Bean
,如果没有导入,则不加载
在
domain
中创建一个实体类:javapackage com.jlc.springbootcondition.domain; public class User {}
再编写一个配置类:用于创建
User
相关的Bean
javapackage com.jlc.springbootcondition.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import package com.jlc.springbootcondition.domain.User; import org.springframework.context.annotation.Conditional; import com.jlc.springbootcondition.condition.ClassCondition; @Configuration public class UserConfig { @Bean @Conditional(ClassCondition.class) public User user() { return new User(); } }
@Conditional
是核心的条件注解(需要添加条件注解的实现类),其接口Condition
中有一个方法matches
,其返回值是一个布尔类型的值,如果返回的是True
,那么被这个注解修饰的对象将会被Spring
容器所创建,反之,不会创建编写一个条件注解的实现类:
javapackage com.jlc.springbootcondition.condition; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; public class ClassCondition implements Condition { /** @param context 上下文对象,用于获取环境,IOC容器,ClassLoader @param metadata 注解元对象,可以用于获取注解定义的属性值 */ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 导入Jedis坐标后,就加载该Bean,如果没有导入,则不加载 // 导入Jedis坐标后,redis.clients.jedis.Jedis文件就存在 boolean flag = true; try { Class<?> cls = Class.forName("redis.clients.jedis.Jedis"); } catch (ClassNotFoundException e) { flag = false; } return false; } }
pom.xml
配置文件导入jedis
坐标:xml<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
获取
User
相关的Bean
javapackage com.jlc.springbootcondition; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication public class SpringbootConditionApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取user相关的Bean Object user = context.getBean("user"); System.out.println(user); } }
我们可以对上述的功能进行优化,将类的判断定义为动态的,判断哪个字节码文件存在可动态的指定
我们需要定义一个注解Annotation
package com.jlc.springbootcondition.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented // 引入Conditional中的三个源注解
@Conditional(ClassCondition.class)
public @interface ConditionOnClass {
String[] value();
}
创建User
相关的Bean
,同时使用我们自己定义的条件注解
package com.jlc.springbootcondition.config;
import org.springframework.context.annotation.Bean;
import com.jlc.springbootcondition.condition.ConditionOnClass;
import package com.jlc.springbootcondition.domain.User;
import com.jlc.springbootcondition.condition.ClassCondition;
@Configuration
public class UserConfig {
@Bean
@ConditionOnClass("redis.clients.jedis.Jedis") // 可以动态的指定字节码文件
public User user() {
return new User();
}
}
编写一个条件注解的实现类:
package com.jlc.springbootcondition.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import com.jlc.springbootcondition.condition.ConditionOnClass;
public class ClassCondition implements Condition {
/**
@param context 上下文对象,用于获取环境,IOC容器,ClassLoader
@param metadata 注解元对象,可以用于获取注解定义的属性值
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 导入通过注解属性值value指定坐标,如果指定的坐标存在,则创建Bean,反之,不创建
// 获取注解属性值 value
Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
String[] value = (String[]) map.get("value");
boolean flag = true;
try {
for (String className : value) {
Class<?> cls = Class.forName(className);
}
} catch (ClassNotFoundException e) {
flag = false;
}
return false;
}
}
通过上述的两个案例,我们就可以知道SpringBoot
是如何创建哪个具体的Bean
的,如SpringBoot
是如何知道要创建RedisTemplate
的
系统提供的常用条件注解
ConditionalOnProperty
:判断配置文件中是否有对应属性和值,如果有,初始化Bean
java@Bean @ConditionalOnProperty(name = "Jlc", havingValue = "jlc") public User user() { return new User(); }
配置文件中有对应的值和键,才会去初始化对应的
Bean
propertiesJlc=jlc
配置完后,就可以初始化对应的
Bean
ConditionalOnClass
:判断环境中是否有对应的字节码文件,如果有,初始化Bean
ConditionalOnMissingBean
:判断环境中没有对应的Bean
才初始化Bean
切换内置web
服务器
SpringBoot
的Web
环境中默认使用tomcat
作为内置服务器,其实SpringBoot
提供了4种内置服务器供我们选择(Jetty
、Netty
、Tomcat
和Undertow
),我们可以进行选择切换(我们只需导入不同服务器Web
的坐标,就可以实现服务器的动态切换)
使用tomcat
作为内置服务器是需要引入相应的坐标的:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
切换其他内容Web
服务器的方式:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--排除tomcat依赖-->
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!--引入jetty的依赖-->
<dependency>
<artifactId>spring-boot-starter-jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
@Enable*
注解
在SpringBoot
引导类中@SpringBootApplication
注解的本质就是Configuration
,意为着通过该注解修饰的类是一个配置类,是可以直接定义Bean
的
@Enable*
注解表示以@Enable
开头的一类注解
问题思考:SpringBoot
工程是否可以直接获取jar
包(第三方)中定义的Bean
?(不能直接获取)
SpringBoot
工程不能直接获取在其他工程中定义的Bean
,原因是@SpringBootApplication
注解只能扫描当前引导类所在的包及其子包,对于第三方包的简单构建:
package com.jlc.domain;
public class User {}
package com.jlc.config;
import com.jlc.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
@Bean
public User user() {
return new User();
}
}
在另一个工程中对这个Bean
进行使用,首先需要在pom.xml
中进行对第三方包的引入:
<dependency>
<groupId>com.jlc</groupId>
<artifactId>springboot-enable-other</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
对于不能直接获取在其他工程中定义的Bean
问题,这种情况有以下几种解决方式:
使用
@ComponentScan
注解进行扫描我们要加载配置类所在的包javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication @ComponentScan("com.jlc.config") public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Object user = context.getBean("user"); System.out.println(user); } }
但是这种解决方法是不严谨的,是比较麻烦的,我们以后使用其他第三方的包是不可能使用这种方式的
使用
@Import
注解去加载类,这些类都会被Spring
创建,并放入到IOC
容器中javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication @Import(UserConfig.class) public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Object user = context.getBean("user"); System.out.println(user); } }
但是这样的方式也不是特别方便,我们需要记住类的具体名字
可以对
@Import
注解进行封装,在提供Bean
的第三方中进行封装javapackage com.jlc.config; import org.springframework.context.annotation.Import; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(UserConfig.class) public @interface EnableUser {}
在第三方包中封装完后,我们在其他项目中通过
@EnableUser
注解配置,直接使用即可javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication @EnableUser public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Object user = context.getBean("user"); System.out.println(user); } }
在
SpringBoot
中提供了很多以Enable
开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用@Import
注解导入一些配置类,实现Bean
的动态加载
@Import
注解
@Enable*
注解底层依赖于@Import
注解导入一些类,使用@Import
注解导入的类会被Spring
加载到IOC
容器中
@Import
提供了四种用法(导入方式):
直接导入
Bean
javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication @Import(User.class) public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Object user = context.getBean(User.class); System.out.println(user); // 获取这个Bean的具体信息 Map<String, User> map = context.getBeansOfType(User.class); System.out.println(map); } }
导入配置类(会将配置类中定义的所有
Bean
导入到IOC
容器中)javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication @Import(UserConfig.class) public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Object user = context.getBean(User.class); System.out.println(user); } }
导入
importSelector
接口的实现类,一般用于加载配置文件中的类在第三方包中编写
importSelector
接口的实现类,供后续导入使用:javapackage com.jlc.config; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; public class MyImportSelector implements importSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{"com.jlc.domain.User"}; } }
导入
importSelector
实现类javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication @Import(MyImportSelector.class) public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Object user = context.getBean(User.class); System.out.println(user); } }
这种方式进行导入,可以同时导入多个,而且可以将导入的内容写到配置文件中,进行动态的修改
系统提供的
@SpringBootApplication
是使用这种导入方式导入
ImportBeanDefinitionRegistrar
接口的实现类在第三方包中编写
ImportBeanDefinitionRegistrar
接口的实现类,供后续导入使用:javapackage com.jlc.config; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.type.AnnotationMetadata; public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition(); registry.registerBeanDefinition("user", beanDefinition); } }
导入
importSelector
实现类javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication @Import(MyImportBeanDefinitionRegistrar.class) public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Object user = context.getBean(User.class); System.out.println(user); } }
@EnableAutoConfiguration
注解
@EnableAutoConfiguration
注解是SpringBoot
自动配置的核心注解
@EnableAutoConfiguration
注解内部使用@Import(AutoConfigurationImportSelector.class)
来加载配置类。配置文件位置:META-INF/spring.factories
,该配置文件中定义了大量的配置类,当SpringBoot
应用启动时,会自动加载这些配置类,初始化Bean
不是所有的Bean
都会被初始化,在配置类中使用Condition
来加载满足条件的Bean
自定义起步依赖
需求:自定义redis-starter
,要求当导入redis
坐标时,SpringBoot
自动创建Jedis
的Bean
实现步骤:
创建
redis-sping-boot-autoconfigure
模块(Modules
模块,用于编写核心的自动配置类)在
redis-spring-boot-autoconfigure
模块中初始化Jedis
的Bean
,并定义META-INF/spring.factories
文件在
pom.xml
中引入jedis
依赖:xml<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
编写一个核心的自动配置类:
javapackage com.jlc.redis.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ConditionalOnClass; import org.springframework.context.annotation.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.Jedis; @Configuration @ConditionalOnClass(Jedis.class) // Jedis.class存在的时候才加载 @EnableConfigurationProperties(RedisProperties.class) public class RedisAutoConfiguration { // 提供Jedis的bean @Bean @ConditionalOnMissingBean(name = "jedis") // 如果没有以jedis为名字的Bean时,我们才提供 public Jedis jedis(RedisProperties redisProperties) { return new Jedis(redisProperties.getHost(), redisProperties.getPort()); } }
编写一个实体类,和配置文件进行绑定
javapackage com.jlc.redis.config; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "redis") public class RedisProperties { // 如果用户没有配置,默认使用本机的Ip和端口 private String host = "localhost"; private int port = 6379; public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } }
在配置文件
application.properties
中定义配置:propertiesredis.host=localhost redis.port=6379
在
resources
文件夹中创建META-INF
文件夹,内部新建spring.factories
文件,其内容为:propertiesorg.springframework.boot.autoconfigure.EnableAutoConfiguration=com.jlc.redis.config.RedisAutoConfiguration
创建
redis-spring-boot-starter
模块(进行依赖的整合),依赖redis-spring-boot-autoconfigure
模块对于该模块的依赖
pom.xml
,系统创建的依赖只留下核心的依赖即可:xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
同时引入我们自定义的
configure
xml<!--引入自定义的redis的starter--> <dependency> <groupId>com.jlc</groupId> <artifactId>redis-spring-boot-autoconfigure</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
在测试模块中引入自定义的
redis-starter
依赖,测试获取Jedis
的Bean
,操作redis
javapackage com.jlc.springbootenable; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.SpringApplication; @SpringBootApplication public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootInitApplication.class, args); // 获取Bean Jedis jedis = context.getBean(Jedis.class); System.out.println(jedis); jedis.set("name", "jlc"); String name = jedis.get("name"); System.out.println(name); } }