最近发现看过的东西容易忘,但是写一遍之后印象倒是会深刻的多。
总所周知springboot极大的简化了java开发繁琐性,而其最大的优势应该就是自动化配置了。比如要使用redis,我们直接引入相关的包,将redis连接信息配置下即可使用。本文主要分析下springboot自动化配置的关键。
本文分析核心过程的代码,无关性的代码略过。
1.注解与组合注解
再说自动化配置前肯定要先说下注解,java5开始引入了注解这么个东西,可以对类,成员方法,成员变量加上标记,而这些标记可以在类加载,编译运行时被读取,基本可以做到无侵入性。同时java也支持组合注解,就是注解之上可以再添加其他注解,例如我们使用springmvc框架时,使用@RestController,即可代替@Controller以及@ResponseBody注解。是因为@RestController就是一个组合注解。其已经组合了两种注解。注解和组合注解在springboot的自动化配置中起了非常大的作用
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController { @AliasFor(annotation = Controller.class) String value() default ""; }
2.@SpringBootApplication
相信用springboot开发的同学对这个注解不会陌生,其是springboot开发的入口注解。可以看下这个注解的内容
@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 { @AliasFor(annotation = EnableAutoConfiguration.class) Class<?>[] exclude() default {}; @AliasFor(annotation = EnableAutoConfiguration.class) String[] excludeName() default {}; @AliasFor(annotation = ComponentScan.class, attribute = "basePackages") String[] scanBasePackages() default {}; @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses") Class<?>[] scanBasePackageClasses() default {}; }
我们可以看到其组合了很多注解,而这些注解当然又加载了很多注解,下面是这个注解的注解组合。
可以看到一个@SpringBootApplication注解后有很多组合的注解。而这些注解在springboot进行自动化配置的时候起了很大的作用。在后面用到的时候这些注解的作用会一一说明
3.从main方法开始
相信大多数人的springboot项目都是从如下的函数代码开始,那我们就从这个代码开始入手
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } }
跟着代码一路走,会发现其主要分为两步。创建SpringApplication对象,和执行run方法。这儿两步我们分别看
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); }
4.从创建SpringApplication看spring如何完成自动化可拔插配置
这是SpringApplication的构造方法
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { ...//省略代码 //设置ApplicationContextInitializer setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); //设置ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); ...//省略代码 }
我们看setInitializers方法,即查找并设置初始化工具接口ApplicationContextInitializer.class的所有实现类。 我们可以看下spring的查找方法即getSpringFactoriesInstances
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { //当前的类加载器 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); //1.获取满足条件的类的全限定名 Set<String> names = new LinkedHashSet<>( SpringFactoriesLoader.loadFactoryNames(type, classLoader)); //2.根据全限定名进行反射生成实例 List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances; }
上面的方法主要由两步,第一步是获取系统配置的全限定名,第二步是根据全限定名反射生成实例,这儿我们主要看第一步。接着往下看
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { //缓存 防止重复加载 MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { //1.找到所有 META-INF/spring.factories Enumeration<URL> urls = (classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories")); result = new LinkedMultiValueMap<>(); //2.对spring.factories进行迭代 while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); //3.解析每个spring.factories 并将内容存入result Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { List<String> factoryClassNames = Arrays.asList( StringUtils.commaDelimitedListToStringArray((String) entry.getValue())); //这儿result中存的则是spring.factories中的东西 result.addAll((String) entry.getKey(), factoryClassNames); } } //存入缓存 cache.put(classLoader, result); return result; } }
其实这个方法可以说就揭示了springboot进行自动化配置的一个关键,其会搜寻所有的META-INF/spring.factories。然后将信息以key,values的形式存起来。我们可以看下这种文件长什么样,这儿我们看spring-boot-autoconfigure包下spring.factories文件。由于文件太长我截取部分。
# Initializers org.springframework.context.ApplicationContextInitializer= org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer, org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener # Application Listeners org.springframework.context.ApplicationListener= org.springframework.boot.autoconfigure.BackgroundPreinitializer # Auto Configuration Import Listeners org.springframework.boot.autoconfigure.AutoConfigurationImportListener= org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener # Auto Configuration Import Filters org.springframework.boot.autoconfigure.AutoConfigurationImportFilter= org.springframework.boot.autoconfigure.condition.OnClassCondition # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration= org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration, org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,
.....................//省略
这儿可以看到我们之前要查找的ApplicationContextInitializer,在这儿就有对应的两个SharedMetadataReaderFactoryContextInitializer与ConditionEvaluationReportLoggingListener。当然这儿还有很多其他类型的配置类就不一一说明了。springboot会将这些spring.factories文件一一读取,并将其中的信息存储到系统中的cache。既然已经知道了类的全限定名,那我们如果需要加载的话直接反射生成实例即可。我们常见的redis自动配置,或者mongoDB自动配置,在该文件中都可以找到
org.springframework.boot.autoconfigure.EnableAutoConfiguration= ...//省略 org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration, org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,
相信到了这儿对springboot如何做到自动化配置肯定已经有了一个初步的认识了。springboot提供了一个插件的方式来供第三方主动集成。按照其规则只要我们写出对应的文件内容并放在项目的META-INF/spring.factories中。在springboot项目中引入该依赖,spring就会去自动读取我们的配置。这儿可以举个简单的例子,比如说我们想要实现一个插件,加入了我们这个插件的依赖后springmvc中json转换由jackson换为fastjson(springmvc默认使用jackson)。那我们就可以创建项目。并创建如下类
@Configuration @AutoConfigureBefore({WebMvcAutoConfiguration.class}) public class WebMvcConfig { public WebMvcConfig() { } @Bean public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() { FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = fastJsonHttpMessageConverter.getFastJsonConfig(); SerializeFilter[] serializeFilters = fastJsonConfig.getSerializeFilters(); fastJsonConfig.setSerializerFeatures(new SerializerFeature[]{SerializerFeature.PrettyFormat}); List<MediaType> fastMediaTypes = new ArrayList(); fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes); fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig); return fastJsonHttpMessageConverter; } @Bean public HttpMessageConverters customConverters(FastJsonHttpMessageConverter fastJsonHttpMessageConverter) { return new HttpMessageConverters(new HttpMessageConverter[]{fastJsonHttpMessageConverter}); } }
然后在项目的resouce下新建META-INF/spring.factories,并加入上面的配置类信息
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.test.framework.web.mvc.config.WebMvcConfig
这样我们的springboot项目如果引入了这个插件并且引入了fastjson相关的依赖。那么系统就会自动进行替换。想下是不是基本做到了无代码侵入,可拔插。这也就是springboot自动发现并配置的关键。
这个时候当然会有人有疑惑,那我如果引入了这个插件,但是却没引入fastjson的包。那系统不是就会启动报错了吗。能不能做到,我引入了插件所依赖的包才让他生效,否则就不生效呢? spring当然考虑到了这点儿,当然这是后面在分析spring进行配置类解析的时候会讲到的。
到此其实我们就迈出了spring自动发现配置的关键了,当然了,springboot加载自动配置的原理就是如此但时机并不在这儿,这个后面的文章会讲到。