spring boot 程序运行入口
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
spring boot是通过@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 {...}
核心注解三个:
1、@ComponentScan
:标识扫描包的位置
2、@SpringBootConfiguration
:只是一个配置类。
3、@EnableAutoConfiguration
:自动装配的核心注解
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}
将我们包下自己定义的注解类注入到容器中
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {...}
核心为@Import(AutoConfigurationPackages.Registrar.class)
该注解原理为@Import注入组件的第三种方式。利用Registrar注入一批组件。
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
AnnotationMetadata
:表示当前注解所在类的所有注解;
getPackageNames()
:可以获取注解所在类位于的包名,此处是我们程序启动类(MainApplication)所在包名。这也解释为什么我们默认的包路径是程序启动类所在的包,所以我们要把带有注解的类都写在该包下。;
register(registry,new PackageImports...)
:把(注解所在)类所在的包下的所有组件(所有包含注解的类:@Contorller、@Component等)注册(注入容器)。@Bean只是组件注入的一个方式,所有注解注释的类都是组件。
选择性的将spring boot自带的配置类注入到容器中
该注解原理为@Import注入组件的第二种方式
AutoConfigurationImportSelector
类间接实现了ImportSelect
接口。核心代码:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
上面代码中核心为getAutoConfigurationEntry
():向容器中导入一些组件。代码:
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);
}
getCandidateConfigurations()
方法获取所有需要导入到容器的组件(配置类),这里的配置类来源于spring boot的jar包(后面会介绍)。
通过断点查看:这里的133个组件全部都是配置类。
那么是如何获取到这些配置类的呢:
getCandidateConfigurations
利用工厂加载 loadSpringFactories
:得到所有的组件。
loadSpringFactories
核心代码如下:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
核心代码为try部分。FACTORIES_RESOURCE_LOCATION值为META-INF
。
从META-INF/spring.factories
位置来加载一个文件。
默认扫描我们当前系统里面所有META-INF/spring.factories
位置的文件。
spring-boot-autoconfigure-2.6.4.RELEASE.jar包里面也有META-INF/spring.factories。
下面就是spring boot 全部需要注入的配置类xxxxAutoConfiguration(文件里面写死的,spring boot一启动就会注入容器)。
虽然,我们上面的所有配置类在自动配置启动的时候默认全部加载。xxxxAutoConfiguration。
但是,由于条件装配注解@Conditional
的作用,最终会按照条件装配规则按需配置,即基本上不会全部装配。
以RabbitAutoConfiguration
配置类为例,只有容器中存在RabbitTemplate, Channel
两个组件时才会装配。由于我们没有导入这两个类所在的包(依赖),所以不会装配。
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先。
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {....}
上面是源码的一部分,@ConditionalOnMissingBean
标识只有容器中没有时才会配置,所以,如果我们自己在配置类中@Bean
注入了CharacterEncodingFilter
类,spring boot
就不会帮我们再次配置。
SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration;
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
生效的配置类就会给容器中装配很多组件;
只要容器中有这些组件,相当于这些功能就有了;
定制化配置(两种方法);
用户直接在自己的配置类中@Bean替换底层的组件;
用户去看这个组件是获取的配置文件什么值就去修改(常用,直接去自己的application.properties文件中设置相关值)。
xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties。
下面是@EnableAutoConfiguration注解的一个流程图。
组件:即通过注解修饰的类均为组件。
表示这是一个配置类,配置类里面使用@Bean标识在方法上给容器注入组件,容器内的组件默认也是单实例的。
@Configuration
public class MyConfig {
// 组件默认名为方法名
@Bean
public User user01(){
return new User("komorebi");
}
// 可以通过Bean修改组件名
@Bean("tom")
public Pet pet01(){
return new Pet("tomcat");
}
}
验证容器内的组件默认也是单实例的。以下代码输出为true
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 1、返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
// 2、调用组件,如果有直接拿,没有创建新对象
User user01 = run.getBean("user01", User.class);
User user02 = run.getBean("user01", User.class);
System.out.println("两次调用同一组件,是否为同一对象:"+(user01 == user02));
}
}
proxyBeanMethods
属性
spring boot 2和1在@Configuration
是有很大区别的。版本2中多了一个属性:boolean proxyBeanMethods() default true;
默认为true,True
表示当代理对象调用@Configuration
配置类中@Bean
标识的方法时,总会去容器中查看有无该组件,如果有则去拿,每次取出的都是同一个对象,保持单实例。如果没有才去创建对象。False
表示不会去查看容器,直接创建对象返回,就不能保证单例,但是时间会加快。
@Configuration(proxyBeanMethods = True)
False
的使用场景:对应轻量级模式。如果我们只是向容器注入组件,别人不会使用这些组件,我们就设置为False
,加快运行速度。
True
的使用场景:对用重量级模式。如果组件后面会被使用,则设置为True
。例如组件依赖:如果user01
组件内想要使用容器内的tom
组件,就必须要设置为True
,否则会新建tom
所对应类的对象,和我们的预期就会不同。
@Bean
上面配置类使用了@Bean注解,下面介绍一些其他关于@Bean的知识点
@Bean标识的函数有参数时,参数也会去容器中找。
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
//SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
@Import只能写在类上
注意:Import不只是能导入@Bean组件,它可以导入所有注解(Controller、Component等)修饰的组件。
Import(),括号内可以填写三种类型类实现组件注入。
方式一 实体类.class
@Configuration
配置类除了@Bean
注入组件之外,还可以通过@Import
实现向容器中注入组件的功能。该注解也是加在配置类上的。
前提:注入的类必须定义无参构造函数,如果为有参,还需要填入参数,但是好像不支持。
@Import({User.class}) // 向容器中注入一个User类型的组件
// @Configuration表示这是一个配置类,配置类里面使用Bean标识在方法上给容器注入组件
@Configuration
public class MyConfig {
// 组件默认名为方法名
@Bean
public User user01(){
return new User("komorebi");
}
// 可以通过Bean修改组件名
@Bean("tom")
public Pet pet01(){
return new Pet("tomcat");
}
}
方式二: 实现了ImportSelector接口的类.class
上面使用@Import导入组件方法,我们只需要想要导入的组件名加入到Import中即可。
还有另外一种常用的方法,自定义逻辑返回要导入的组件(Spring Boot自动装配源码中大量使用了该方法)。
ImportSelector
来导入多个组件(这里导入blue、car两个组件)通过重写selectImports返回要导入的组件
class MyImportSelector implements ImportSelector{
// 返回值就是要导入的组件
// AnnotationMetadata可以获取使用@Import标记的类上所有注释信息
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.komorebi.bean.Car","com.komorebi.bean.Blue"};
}
@Override
public Predicate<String> getExclusionFilter() {
return ImportSelector.super.getExclusionFilter();
}
}
在Import中加入自定义的逻辑
@Import({User.class,MyImportSelector.class})
@Configuration
@ConditionalOnMissingBean(name="tom")
@EnableConfigurationProperties(Car.class)
public class MyConfig {
// 组件默认名为方法名
@Bean
public User user01(){
return new User("komorebi");
}
// 可以通过Bean修改组件名
@Bean("tom")
public Pet pet01(){
return new Pet("tomcat");
}
}
查看组件是否注入成功(发现blue、car成功注入)
方式三 :实现了ImportBeanDefinitionRegistrar
接口的类.class
registry里面记录了所有注册(注入)的组件,所以可以通过registry获取所有组件信息
它可以支持我们自己写的代码封装成BeanDefinition对象
;实现此接口的类会回调postProcessBeanDefinitionRegistry方法,注册到spring容器中。把bean注入到spring容器不止有 @Service @Component等注解方式;还可以实现此接口。
本质:将想要注入的类生成对应BeanDefinition对象,然后BeanDefinition注册登记,后续bean实例化时会根据BeanDefinition对象生成(参考bean的生命周期)
class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
// BeanDefinitionRegistry: BeanDefinition注册类。即Bean定义注册
// 调用registerBeanDefinitions方法实现Bean注册,Bean注册=注入Bean,注入的Bean都会有注册,都可以查到。
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(Yellow.class);
// 这里注册bean
registry.registerBeanDefinition("yellow",beanDefinition);
}
}
查看结果
下面@Import中的三个内容代表了三种不同的Import注入组件的方式
@Import({User.class,MyImportSelector.class,MyImportBeanDefinitionRegistrar.class})
@Configuration
@ConditionalOnMissingBean(name="tom")//容器中没有tom组件才注入下面的两个组件
public class MyConfig {...}
@Configuration
和@Import
都是使用在配置类上面,@Component
则是使用在实体类上面,可以实现向容器中注入实体类类型的组件。
前提:注入的类必须定义无参构造函数。