SpringBoot源码分析
1. SpringBoot和Spring的关系
SpringBoot是Spring最核心的产品之一(是Spring的分支项目),而SpringCloud是更进一步的产品。
传统的spring项目
- 基于xml方式;
- 依赖的第三方的模块,都在pom.xml逐个依赖,还需要在xml文件中去配置,配置的目的就是让spring去加载和管理这些bean;
- 需要依赖外部的tomcat容器去部署项目才可以进行项目发布与部署;
- 日志管理,需要额外的配置;
- 配置组件或者应用非常的麻烦和繁复;
springboot的项目
- 零配置,用注解的方式取代了传统意义上的xml文件;
- starter机制;
- 内置了tomcat容器和日志管理,开发起来只需要启动main函数,就可以项目运行起来了;
SpringBoot,它构建在Spring基础之上的一个产品,它最主要目的就是简化Spring框架中繁复的配置问题。那么,是用什么方式来解决的呢?
靠注解和类来完成
在以前的开发中,spring会使用大量的xml配置文件来加载bean到ioc容器中,而springboot的优点之一就是减少了大量配置文件的使用,它是如何做到的呢?(springboot如何加载bean到ioc容器中)
springboot不管如何使用都脱离不了ioc容器,最终的一切都会把它放入到ioc容器当中,放入到ioc容器的方式有如下几种:
- @Configuration+@Bean
- @ComponentScan+(@Component、@Controller、@Repository、@RestController、@Service)
- Import机制(Selector)
- ImportResource
注意:其中starter就是用的Configuration和Import机制
bean什么时候加载到ioc容器
@SpringBootApplication
public class SourceApplication {
public static void main(String[] args) {
SpringApplication.run(SourceApplication.class, args);
}
}
其实,在执行main函数启动的时候,就会调用类加载器去加载对应的bean,全部放入到到ioc容器中。
1、类加载器,其实就是:SpringApplication.run(AdminApplication.class, args)
中的AdminApplication.class
。这个操作传递了一个类,传递这个类的目的就是为了触发类加载器和通过反射找到主类上的注解@SpringBootApplication
;
2、可以传递别的类吗?
答案是不可以?因为传递别的类,没办法去加载项目中bean和加载starter机制的类。
因为主类上有一个注解@SpringBootApplication
,这个注解是核心注解。这个注解就去加载项目中对应的所有的bean。这些bean就包含项目@Configuration+@Bean的初始化、@Import机制(starter)那些类等等。
3、为什么要触发类加载器?
- 可以获取上下文的环境,可以能获取系统的信息
- 会加载pom.xml所有的依赖jar中编译好的类
- 会加载jdk对应api
总结:SpringBoot加载bean到ioc容器的过程,其实就是通过触发类加载和反射找到主类上的核心注解@SpringBootApplication
,然后在触发springboot中的机制,把项目中和pom.xml中的依赖(starter和其他的依赖jar)、以及jdk的api全部初始化到内存中。其中,springboot中通过@Configuration+@Bean的初始化、@SelectorImport机制(starter)和扫包+注解、@ImportResource加载配置文件都会放入到ioc容器中。
2.SpringBoot的复合注解@SpringBootApplication
@SpringBootApplication
是一个复合注解,包含了以下三个主要的注解
- @Configuration+@Bean
- @Selector、Import机制
- @ImportResource
@Target(ElementType.TYPE)//jdk注解中代表当前注解只能使用类上面,注解使用范围
@Retention(RetentionPolicy.RUNTIME)//代表该注解类的,可以通过反射获取到注解信息
@Documented//jdk注解文档
@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
1、@SpringBootConfiguration通过查看,其实就是一个@Configuration(Spring框架提供的注解)。其实,就是通过Configuration+@Bean的方式注入bean到ioc容器中;
注:@Configuration其实是@Compenont,但是这两个注解有一些区别,@Component注解在当做配置类时,并不会为其生成CGLIB代理Class。
2、@EnableAutoConfiguration,这个时开关注解,也就是Selector、Import机制来加载bean到ioc容器中,同时它也是去加载项目中starter机制的注解;
3、@ComponentScan,这个就是扫包并使用注解(@Service、@Controller、@RestController、@Component 、@Repository)加载bean到ioc容器中;
3.@Configuration+@Bean加载bean到ioc容器
注意:@Configuration+@Bean
它必须配合@ComponentScan
才能把对象的bean放入到ioc容器中!!!
why?
通过@Configuration注解的源码如下可以得知,@Configuration注解本质上是一个@Component
,它必须要扫包@ComponentScan才能加载到ioc容器中
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration
被@Configuraiton注解的类叫做配置类,一般用于初始化bean到ioc容器中,与spring开发作比较:
- 配置类xxxConfiguration相当于:applicationContext.xml
- 配置类中的@Bean节点相当于:applicationContext.xml中的< bean id=" “,class=” ">节点。
4.@ComponentScan+一系列注解的机制
默认情况下,@ComponentScan默认的扫包是:当前启动类的包作为扫描范围。
扫包的作用是:把该包下的所有的子包和子孙包下面所有的符合条件的类(@Service、@Controller、@RestController、@Component 、@Repository)和 @Configuration类全部加载到ioc容器中。
第三方的模块
第三方的模块可以使用扫包+注解的方式加入到ioc中嘛?
可以是可以,但是特别繁琐。因为第三方的包涉及到特别多的类,而且包名千变万化,这种方法不再适合加载第三方包。第三方包的加载,就用到了Import机制。
5.@Import、 @Eablexxx机制
5.1 @EnableAutoConfiguration注解
@EnableAutoConfiguration注解表示开启自动配置功能,该注解是SpringBoot最重要的注解,它主要是为了解决第三方的一些starter、实现自动化配置,弥补扫包、@Configuration的不足。
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
1、@AutoConfigurationPackage注解
查看源码可以知道,@AutoConfigurationPackage注解的本质是@Import注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
其作用是向容器中导入注册的所有组件,导入的组件由静态内部类Registrar决定。
其实,这个注解就是一个开关类,是否把第三方的一些初始化bean的信息加载到ioc容器中(加上注解则注入bean,不加则不注入)。其实,每一个starter都有**/META-INF/spring.factories**文件来存储初始化bean的配置信息。
2、@Import注解
核心代码:
@Import(AutoConfigurationImportSelector.class)
Import机制,参数是一个class。 这个class可以是:configuration类也可以是ImportSelector子类。
查看AutoConfigurationImportSelector的源码,AutoConfigurationImportSelector实现了DeferredImportSelector接口,DeferredImportSelector接口是ImportSelector的子接口。
ImportSelector
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
ImportSelector中有一个很重要的方法selectImports
,它的作用是把需要加载的bean配置到这个数组(String数组)中,然后springioc容器就会加载这个数组中配置好的bean,然后放入到ioc容器中。
3、自定义开关类实现bean的注入
定义一个订单service,没有使用任何注入bean的注解
@Slf4j
public class UserOrderService {
public void makeOrder(Integer userId, String orderId) {
log.info("用户:{},下单成功:订单id是:{}", userId, orderId);
}
}
定义一个开关类EnableUserOrder
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(UserOrderImportSelector.class)
public @interface EnableUserOrder {
}
定义一个具体的ImportSelector 的实现类
public class UserOrderImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{
//全路径
"com.kuangstudy.controller.selector.UserOrderService "
};
}
}
在springboot的启动类上加上注解@EnableUserOrder就可以把UserOrderService加载到ioc容器当中。
总结:@Import它的作用:其实就可以去灵活的加载第三方的starter配置类,和我们自定义的配置类,以及官方提供配置类。因为用这种机制,就可以脱离当前项目的管控,可以灵活通过这种机制就能够把别人写的springboot的初始化好配置类的bean,放入到我们的springboot工程中。比如,pom文件中的各种starter。
5.2 starter如何加载进ioc容器
其实,每一个starter都有一个 /META-INF/spring.factories,这个文件包含了需要加载的bean,通过类加载机制,加载到上述提到的ImportSelector 接口的selectImports方法的String数组中。
真正具体的触发和执行@Import(AutoConfigurationImportSelector.class) 这个类中selectImports
方法是在run方法中去执行调用的
1、启动主类run()
方法
@SpringBootApplication
public class SourceApplication {
public static void main(String[] args) {
//1: 写你类谁来编译的。maven -- compile --- java---class-target
//2:你项目中每天都是使用String、List。你为什么就可以用? jdk--它是怎么找到jdk的这些jar.是如何放入到jvm存储中
SpringApplication.run(SourceApplication.class, args);
}
}
2、点进去源码,run()
调用了重载
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
3、执行run()方法的重载
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
包含两部分,一部分是SpringApplication实例的初始化,另一部分是运行所有加载的bean到ioc容器中。
初始化
new SpringApplication(primarySources)
把springboot中含有/META-INF/spring.factories 所有的初始化接口和监听器接口,以及它们对应下面的所有的实现子类全部抓取出来,放入对应的位置,其实就是map中。
通过类加载器去收集springboot中含有/META-INF/spring.factories配置文件,然后解析配置文件。然后放入到Map中。
运行加载
.run(args);
运行阶段,就是把初始化阶段的信息,开始进行过滤和筛选,然后触发spring生命周期的那些机制,然后把符合条件的bean放入到ioc容器中。
当然,这个过程还会初始化许多其他的信息;比如banner、日志、上下对象、应用参数接口、运行时run接口的调用等等。
ImportSelector机制其实就是run方法这个阶段去触发执行,它会把初始化好的所有的符合条件的配置类,放入selectimports数组中,然后调用ioc初始化。
5.3 SpringApplication实例的初始化过程详解
查看SringApplication实例对象初始化的源码信息,核心代码如下:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 1: 初始化数组,集合,开关标记
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = Collections.emptySet();
this.isCustomEnvironment = false;
this.lazyInitialization = false;
this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
this.applicationStartup = ApplicationStartup.DEFAULT;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
//判断当前webApplicationType应用的类型,deduceFromCiasspath()方法用于查看
//Classpath类路径下是否存在某个特征类,从而判断当前webAplicationType类型是SERVLET
//应用(Spring5之前的传统MVC应用)还是REACTIVE应用(Spring5开始出现的WebFlux交互式应用)。
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//缓存
this.bootstrapRegistryInitializers = this.getBootstrapRegistryInitializersFromSpringFactories();
//用于设置SpringApplication应用的初始化器
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
//用于设置SpringApplication应用的监听器
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
//用于推断并设置项目main()方法启动的主程序启动类
this.mainApplicationClass = this.deduceMainApplicationClass();
}
核心代码1
this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class);
核心代码2
this.getSpringFactoriesInstances(ApplicationContextInitializer.class);
核心代码3
this.getSpringFactoriesInstances(ApplicationListener.class);
核心代码1、核心代码2、核心代码3都在调用同一个方法this.getSpringFactoriesInstances()
,只是这三个方法传入的类不同。
三个方法都在传递一个类,为什么要传递这个类呢?
是为了触发类加载器
传递类的作用
触发类加载器,通过双亲委派模型,把项目中所有的类(包括jdk、jdkext、target/classes、spring.jar、mybatis.jar)全部把他们中编译好的字节码文件中找到,并放入map中。
BootstrapRegistryInitializer
其实这个类不干具体的活的,它提前把项目中的类全部找到放入缓存map中,然后为ApplicationContextInitializer
、ApplicationListener
提供一个缓存机制,后面两者的获取对应bean就不会重复去触发类加载,频繁的扫描过滤。
详解getSpringFactoriesInstances()方法
点进去,查看源码
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return this.getSpringFactoriesInstances(type, new Class[0]);
}
重载
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
//触发类加载器
ClassLoader classLoader = this.getClassLoader();
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
使用的是类加载器AppClassLoader,
AppClassLoader --> ExtClassLoader --> BootstrapClassLoader(双亲委派模型)
双亲委派模型,把项目中所有的classes全部找到,然后放入到jvm中去,只不过springboot会进行过滤,把符合条件的bean放入到ioc容器中。哪些是符合条件的bean呢?
- 扫包 + 注解(@Service、@Controller)
- 是不是符合Import机制
- 是不是符合@Configuration + @Bean
符合的放到ioc中,不符合的放到jvm中。
过滤
开始对类加载的加载类进行过滤
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
type:interface org.springframework.boot.Bootstrapper 开始进行匹配过滤。作用其实就是一个加载缓存的目的,作用就是:它会把类加载中所有的jar文件存在:META-INF/spring.factories 文件中的内容全部找到,然后这个META-INF/spring.factories中的bean全部放入到Map中。
springboot是如何把需要的bean加载出来的呢?
我们知道类加载,把项目所有的classes统统都加载的jvm中。这么多的class,springboot是如何把需要管理和初始化、要放入到ioc容器中的classes找出来呢?springboot借鉴了java9的新特性:spi机制。这种机制其实就是把需要被加载类放入一个配置中,而springboot命名规则就是:META-INF/spring.factories。
- 而这个文件中就定义springboot需要加载的所有的bean,都放在这个文件中;
- 现在要做的事情,找到这个文件;
- 解析这个文件;
- 解析以后的bean,放入到map中;
注意:此时,只是放入了map,还没有和ioc容器产生任何关系。
初始化阶段,为什么还没放入ioc容器中?
因为初始化阶段仅仅只是一个解析和准备的工作,就把需要的bean全部找到,放入map中。
什么时候才放入到ioc容器中呢?这就是run()方法阶段的事了。
5.4 运行阶段详解
运行阶段,把初始化阶段收集的所有bean,进行过滤,然后把需要的bean放入到ioc容器中去,在经历spring生命周期,运行结束。然后,项目就被运行起来了!!!
(1)执行run()
方法
到refreshContext处停止;
(2)
this.refreshContext(context);
在此时,与ioc容器产生关系;同时,得到一个this.refresh()
(3)点进去看refresh()
方法
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}
在refresh()方法中,就是spring的生命周期相关的代码,对bean进行增强
生命周期阶段
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var10) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
}
this.destroyBeans();
this.cancelRefresh(var10);
throw var10;
} finally {
this.resetCommonCaches();
contextRefresh.end();
}
}
}
(4)
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, this.getBeanFactoryPostProcessors());
if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean("loadTimeWeaver")) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}
(5)
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
(6)
postProcessor.postProcessBeanDefinitionRegistry(registry);
(7)ConditionalOnXXX进行过滤,满足则加载到ioc容器的当中
注意:在Import注解中,参数可以是xxxConfiguration,有的starter就是使用的配置类。
本章终,源码的阅读还有很多地方需要学习,还有许多细节需要完善!!!