大家好我是初晨,之前写了很多关于SpringBoot的文章,相信大家已经感受到了SpringBoot相对于传统Spring带来的便捷,那么本篇文章我们就来分析一下SpringBoot带来的便捷到底便捷在哪
不知道大家有没有注意到,当我们创建一个springboot项目时,都会用到如下的启动类
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }复制代码
从代码上来看,显然注解@SpringBootApplication和SpringApplication类和他的run方法为核心。
那么本篇文章我们先来分析一下@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。下面我们来分别看一下。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { }复制代码
我们发现他是应用了@Configuration。
@Configuration这个注解用于以JavaConfig的方式来定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。也是SpringBoot社区推荐使用的配置形式。
@Configuration与传统xml配置文件的区别:
传统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" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd" default-lazy-init="false"> </beans>复制代码
采用@Configuration:
@Configuration public class TestConfig { }复制代码
传统xml:
<bean id="testService" class="TestServiceImpl"> </bean>复制代码
采用@Configuration:
@Configuration public class TestConfig { @Bean public TestService testService(){ return new TestServiceImpl(); } }复制代码
所以任何一个java类上使用了@Configuration,都代表他是一个配置类。
任何一个标注了@Bean的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。
这个注解在spring中很重要,如果你理解了@ComponentScan,你就理解了spring。
大家都知道spring是一个依赖注入的框架,所有的内容都是围绕bean定义及其依赖关系。
但是spring并不知道你定义了哪些个bean,除非你告诉spring可以从哪里找到你定义的那些bean。
而@ComponentScan的作用就是告诉Spring可以从哪里找到定义的bean
通过basePackages属性可以控制@ComponentScan自动扫描的范围,如果没有指定@ComponentScan的扫描范围,那么默认的扫描范围是从声明@ComponentScan所在类的package进行扫描。
所以我们可以发现,创建的springboot项目目录结构一般是这样的
启动类一直在项目的根目录下,这样才能在不配置扫描范围的情况下扫描到所有定义的bean
@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 {}; }复制代码
这里重要的注解有两个,分别是@AutoConfigurationPackage和@Import
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({Registrar.class}) public @interface AutoConfigurationPackage { }复制代码
这里发现他使用@Import注解引入了一个类Registrar
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { Registrar() { } public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()); } public Set determineImports(AnnotationMetadata metadata) { return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata)); } }复制代码
那么这个类是用来干嘛的呢?我们断点跟一下,看一下registerBeanDefinitions方法是做什么的。
这里发现 new PackageImport(metadata).getPackageName()
返回的返回了当前主程序类的同级以及子级的包组件。
这也证明了@ComponentScan默认扫描其所在类的package。
接下来我们具体看一看AutoConfigurationImportSelector这个类
public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!this.isEnabled(annotationMetadata)) { return NO_IMPORTS; } else { AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader); AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } }复制代码
这个方法调用了getAutoConfigurationEntry方法
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { if (!this.isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else { AnnotationAttributes attributes = this.getAttributes(annotationMetadata); List configurations = this.getCandidateConfigurations(annotationMetadata, attributes); configurations = this.removeDuplicates(configurations); Set exclusions = this.getExclusions(annotationMetadata, attributes); this.checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this.filter(configurations, autoConfigurationMetadata); this.fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions); } }复制代码
这里我们重点看一下getCandidateConfigurations方法,先断点看一下方法的返回值
该方法返回的是需要实例化的类信息列表,有了他spring就可以通过类加载器将需要实例化的类加载到jvm中。
现在我们看一下该方法的代码,发现他是借用了SpringFactoriesLoader类的方法
protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List 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; }复制代码
再来看一下loadFactoryNames方法
public static List loadFactoryNames(Class factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); } private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { Enumeration urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); //....省略 cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } }复制代码
发现他读取了一个名为spring.factories的文件
比如我们找一下redis的配置
点击去看一下
@Configuration @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { ...... }复制代码
@ConditionalOnClass({RedisOperations.class}) 表示必须存在RedisOperations这个类,否则不解析该注解修饰的配置类。
再看看RedisProperties这个类
public class RedisProperties { private int database = 0; private String url; private String host = "localhost"; private String password; private int port = 6379; private boolean ssl; private Duration timeout; private RedisProperties.Sentinel sentinel; private RedisProperties.Cluster cluster; private final RedisProperties.Jedis jedis = new RedisProperties.Jedis(); private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce(); ......省略 }复制代码
是不是眼熟了!这个就是我们在使用redis时写在application.properties里面,需要配置redis的信息
对应的配置应为:
spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.timeout=0 spring.redis.password=复制代码
同时也可以看到redis默认为我们配置了host和port属性。所以在配置redis时,如果端口号围为默认的6379,我们也可以不写的原因。
所以@EnableAutoConfiguration的大致原理就是从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。
@SpringBootApplication我们分析完了,下篇文章我们分析SpringApplication这个类
对于springBoot还不了解的朋友可以看我的SpringBoot系列教程