赞
踩
springboot集成第三方组件,是非常容易的事情,以redis为例,pom文件中添加redis配置,
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
在application.properties配置文件中配置redis的连接信息(默认配置)
spring.redis.host=192.168.216.128
启动类
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ca=SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
在需要使用redis的地方,注入RedisTemplate 并使用,仅此而已:
package com.gupaoedu.example.springbootdemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@GetMapping("/say")
public String say(){
return redisTemplate.opsForValue().get("name");
}
}
在上面能够实现注入的前提是IOC存在RedisTemplate 的实例,上一篇文章中,可以知道,SpringIOC Bean的装载有三种方式:
xml
Configuration配置类
EnableXXX 模块驱动 -》 springboot自动装配完成bean的自动装载
我们可以猜想一下RedisTemplate注入的实现过程:
我们加入了spring-data-redis 的jar包依赖,RedisTemplate 必然是存在于这个jar包中的,那么这个jar包里面是否存在一个配置类,通过如下方式把RedisTemplate 加载到IOC容器中:
@Configuration
public class RedisConfiguration{
@Bean
public RedisTemplate redisTemplate(){
return new RedisTemplate();
}
}
那么问题又来了:
- spring如何知道上面这个配置类在哪里
2.如果pom添加了很多组件的依赖,这些组件的配置是如何批量扫描的
这里需要了解到spring的动态Bean的装载方式(根据条件或者上下文动态装载一些配置)
package com.gupaoedu.example.demo02;
public class GpRedisTemplate {
}
package com.gupaoedu.example.demo02;
import org.springframework.context.annotation.Bean;
/**
* 配置类声明
* 通过分包模拟不同的三方组件的引入
**/
@Configuration
public class RedisConfiguration {
@Bean
public GpRedisTemplate gpRedisTemplate(){
return new GpRedisTemplate();
}
}
第二个包下的配置类:
package com.gupaoedu.example.demo03;
public class GpSqlSessionFactory {
}
package com.gupaoedu.example.demo03; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 配置类声明 * 用 不同的包模拟不同的jar包中的配置如何导入 **/ @Configuration public class MybatisConfiguration { @Bean public GpSqlSessionFactory gpSqlSessionFactory(){ return new GpSqlSessionFactory(); } }
自定义ImportSelector的具体实现
package com.gupaoedu.example.demo04; import com.gupaoedu.example.demo02.RedisConfiguration; import com.gupaoedu.example.demo03.GpSqlSessionFactory; import com.gupaoedu.example.demo03.MybatisConfiguration; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; /** * 模拟jar包的批量扫描 **/ public class GpDefineImportSelector implements ImportSelector{ @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 通过某种机制去完成指定路径的配置类的扫描就行?—— 约定优于配置 -》 //package.class.classname 把各种的配置类的路径告诉spring行了 ——》标准:classpath:META-INF/spring.fatcories文件中配置 // springboot只需要扫描classpath*:/META-INF/spring.factories文件即可 // 告诉spring当前的配置类在哪里 //在这里去加载所有的配置类(通过某种机制完成指定的配置类的扫描),然后传入这个数组,就可以进行自动装配了 return new String[]{MybatisConfiguration.class.getName(), RedisConfiguration.class.getName()}; } }
自定义Enable注解
package com.gupaoedu.example.demo04;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(GpDefineImportSelector.class)
public @interface EnableConfiguration {
}
通过EnableConfiguration ,最终实现Bean的自动装载:
package com.gupaoedu.example.springbootdemo; import com.gupaoedu.example.demo02.GpRedisTemplate; import com.gupaoedu.example.demo03.GpSqlSessionFactory; import com.gupaoedu.example.demo04.EnableConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ComponentScan; @EnableConfiguration // @SpringBootApplication public class SpringBootDemoApplication { public static void main(String[] args) { ConfigurableApplicationContext ca=SpringApplication.run(SpringBootDemoApplication.class, args); System.out.println(ca.getBean(GpRedisTemplate.class)); System.out.println(ca.getBean(GpSqlSessionFactory.class)); } }
不加EnableConfiguration注解,获取不到GpRedisTemplate这个bean, 因为它不在SpringBootDemoApplication这个启动类的同级目录下
加上该注解后,通过@Import(GpDefineImportSelector.class) 动态导入了GpRedisTemplate 和MybatisConfiguration这两个配置类,
从而加载了GpRedisTemplate和GpSqlSessionFactory 这两个bean ,这就是ImportSelector 动态装载bean的实现原理;
上面整个过程中,关键的一个步骤就是GpDefineImportSelector 中把配置类添加到数组里面返回,那么是否有一种机制,能够让spring在importSelector里面能够加载所有的配置类呢? 显然是有的,spring中,在classpath:META-INF/spring.fatcories文件中配置了各种配置类,再回到springboot的@SpringBootApplication注解,它是一个复合注解,包含了@EnableAutoConfiguration注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; /** * Exclude specific auto-configuration classes such that they will never be applied. * @return the classes to exclude */ Class<?>[] exclude() default {}; /** * Exclude specific auto-configuration class names such that they will never be * applied. * @return the class names to exclude * @since 1.3.0 */ String[] excludeName() default {}; }
在@Import注解里面,我们看到了AutoConfigurationImportSelector 这个自动装载的实现:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry(); private static final String[] NO_IMPORTS = {}; private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class); private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude"; private ConfigurableListableBeanFactory beanFactory; private Environment environment; private ClassLoader beanClassLoader; private ResourceLoader resourceLoader; private ConfigurationClassFilter configurationClassFilter; @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } @Override public Predicate<String> getExclusionFilter() { return this::shouldExclude; } private boolean shouldExclude(String configurationClassName) { return getConfigurationClassFilter().filter(Collections.singletonList(configurationClassName)).isEmpty(); } /** * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata} * of the importing {@link Configuration @Configuration} class. * @param annotationMetadata the annotation metadata of the configuration class * @return the auto-configurations that should be imported */ protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); } …… // 其他代码省略 }
上面selectImports(AnnotationMetadata annotationMetadata) 就是导入各种自动配置类,具体的实现在getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) 方法中,我们在List configurations = getCandidateConfigurations(annotationMetadata, attributes); 这一行代码处打上断点,然后启动项目,可以看到加载了很多的配置类名:
其中当然也包含org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration 这个配置类,查看这个配置类的源码,赫然发现,redisTemplate 这个Bean是在这个配置类里面定义的!!!
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }
所以,springboot自动装配的原理就是:
Spring官方自有的自动装配类是放在spring-boot-autoconfigure-2.3.1.RELEASE.jar 这个jar包里面的spring.factories中的:(忽略版本号差异)
第三方组件的自动装配jar包是放在 XXX-spring-boot-autoconfigure-xxx.jar包中的META-INF/spring.factories中的:
@ConditionalOnClass注解
从上文中我们知道spring的官方包中,所有配置类都是放在spring-boot-autoconfigure-XXX.jar中的,而三方包的自动配置类才是放在META-INF/spring.factories文件中,那么针对于spring官方包中不按套路来的配置类,是如何加载的?以RedisAutoConfiguration为例
它有一个@ConditionalOnClass(RedisOperations.class)注解,当RedisOperations这个类在classpath中存在时,才会加载RedisAutoConfiguration这个配置类,而RedisOperations这个类又是在 spring-data-redis-2.3.1.RELEASE.jar 这个jar包中的,
也就是说,如果没有加入spring-data-redis的依赖,ConditionalOnClass条件不满足,spring就不会去扫描加载RedisAutoConfiguration这个配置类;
因此,spring官方包,配置类是预先配置在spring-boot-autoconfigure-XXX.jar中的,通过条件来触发是否加载这个配置类,只有第三方的包,才需要在META-INF/factories中配置自动配置类的信息,告诉spring从哪里去扫描自动配置类
通过AutoConfigurationMetadataLoader实现条件装配
它的实现是,在META-INF目录下创建一个名字为
spring-autoconfigure-metadata.properties的配置文件,配置文件内容是,配置类全路径名.ConditionalOnClass = 触发条件装配的类的全路径名 ,比如:
com.gupaoedu.autoconfiguration.demo.GupaoConfiguration.ConditionalOnClass=com.gupaoedu.DemoClass
下面通过源码来说明如何实现的:
在org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry 方法中,可以看到这样一行代码
configurations = getConfigurationClassFilter().filter(configurations);
这个filter方法是AutoConfigurationImportSelector的静态内部类ConfigurationClassFilter中的,
这个内部类中有这样一个赋值:
this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader);
查看该代码,就可以发现它是去加载META-INF/spring-autoconfigure-metadata.properties文件中的内容:
所以,如果Springboot单单全部加载spring.factories里的内容,那会在启动的时候会报很多类找达不到的错误,因为spring.factories是一个自动装配配置的全集,在我们没有加入某些jar包的情况下肯定会报相关类找不到的错误,而autoconfiguremetadata就是根据环境和配置剔除掉一部分spring.factories里的组件,所以在加载的时候不会报错!
参考文档:Springboot自动装配之spring-autoconfigure-metadata.properties
在AutoConfigurationImportSelector 这个类中,
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
这行代码返回所有的配置类,它是使用了SpringFactoriesLoader 来扫描classpath下的spring.factories中定义的配置类:
/** * Return the auto-configuration class names that should be considered. By default * this method will load candidates using {@link SpringFactoriesLoader} with * {@link #getSpringFactoriesLoaderFactoryClass()}. * @param metadata the source metadata * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation * attributes} * @return a list of candidate configurations */ protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), 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; }
SpringFactoriesLoader 就是SPI机制的一种体现。什么是SPI?
以JDK的数据库驱动为例,JDK中定义了java.jdbc.Driver这个接口,针对不同的数据库厂商,每个厂商所实现的数据库驱动都不相同,但是它们都遵循相同的规范,这个规范就是java.jdbc.Driver接口,接口的实现由第三方来完成,如果想要使用不同的实现,只需要更换引入的jar包即可, 这就是SPI 扩展点;
代码示例:
(1)新建一个工程:
定义一个接口,然后将这个工程安装到本地maven仓(需要把本地maven仓中的_remote.repositories文件删掉,否则依赖这个jar包时会有问题)
public interface DataBaseDriver {
String buildCOnnect(String host);
}
(2)新建另外一个工程,来实现上面这个接口
<dependency>
<groupId>com.gupao.spi</groupId>
<artifactId>DataBaseDriver</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
public class MysqlDriver implements DataBaseDriver {
@Override
public String buildCOnnect(String s) {
return "Mysql的驱动实现:"+s;
}
}
新建一个项目,引入上述两个工程的依赖
<dependency>
<groupId>com.gupao.spi</groupId>
<artifactId>DataBaseDriver</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.gupao.spi.mysql</groupId>
<artifactId>mysql-driver</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
通过ServiceLoader来加载扩展点:
public class App
{
public static void main( String[] args )
{
ServiceLoader<DataBaseDriver> serviceLoader=ServiceLoader.load(DataBaseDriver.class);
for(DataBaseDriver dbd:serviceLoader){
System.out.println(dbd.buildCOnnect("Test"));
}
}
}
JAVA SPI机制的使用条件:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。