SpringBoot原理与自定义starter

Easter79
• 阅读 458

1. 从SpringBootApplication开始

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Start {

        public static void main(String[] args) {
                SpringApplication.run(Start.class, args);
        }

}

通过main方法启动,进入,跟进去:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return new SpringApplication(primarySources).run(args);
}

发现是通过SpringApplication的run来启动的,创建容器的方法是createApplicationContext:

protected ConfigurableApplicationContext createApplicationContext() {
        return this.applicationContextFactory.create(this.webApplicationType);
}

最骚操作的是ApplicationContextFactory这个类,自己拿了一个自己的实现类,还是lambda表达式,所以很有迷惑性,当时我一直以为我错过了什么Java的重要特性了。

ApplicationContextFactory DEFAULT = (webApplicationType) -> {
        try {
                switch (webApplicationType) {
                case SERVLET:
                        return new AnnotationConfigServletWebServerApplicationContext();
                case REACTIVE:
                        return new AnnotationConfigReactiveWebServerApplicationContext();
                default:
                        return new AnnotationConfigApplicationContext();
                }
        }
        catch (Exception ex) {
                throw new IllegalStateException("Unable create a default ApplicationContext instance, "
                                + "you may need a custom ApplicationContextFactory", ex);
        }
};

Spring容器就创建好了,后面基本就是进入Spring模式的核心refresh了,然后就开始扫描解析BeanDefinition等等操作。

2. @SpringBootApplication注解

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

@SpringBootConfiguration:其实就是@Configuration,会通过ConfigurationClassPostProcessor处理,会在Spring处理BeanFactoryPostProcessor阶段被处理 @ComponentScan:这个我们比较熟悉了扫描Bean的

SpringBoot中的新重点是@EnableAutoConfiguration:

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

这个注解Import了AutoConfigurationImportSelector,AutoConfigurationImportSelector继承了ImportSelector,当被Import导入的时候就会执行它的selectImports。 关于Import注解的逻辑可以参考后面的Import相关的内容。

最终在AutoConfigurationImportSelector的getCandidateConfigurations方法中通过:

SpringFactoriesLoader#loadFactoryNames方法导入了配置文件META-INF/spring.factories配置的类。

spring.factories配置文件中配置了一些XXXAutoConfiguration类,这些类又会导入一些类,例如spring-boot依赖的spring-boot-autoconfigure的META-INF/spring.factories中配置了一个ServletWebServerFactoryAutoConfiguration类。

@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
        ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
        ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
        ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

    @Bean
    public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties,
            ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
        return new ServletWebServerFactoryCustomizer(serverProperties,
                webListenerRegistrars.orderedStream().collect(Collectors.toList()));
    }
}

它又会通过@Import、@Bean注解导入一些它需要的类。

有2个地方需要注意:

  1. 有@Conditional注解表示满足条件才导入,例如@ConditionalOnClass(ServletRequest.class),表示有ServletRequest这个类才导入这个类
  2. 我们在配置文件中添加server.port=8080为什么生效,就是@EnableConfigurationProperties(ServerProperties.class)配置的

具体的内容,可以看看后面的相关内容。

spring.factories不一定都是AutoConfiguration类,例如:

在spring-boot中: SpringBoot原理与自定义starter

在spring-boot-autoconfigure中: SpringBoot原理与自定义starter

3. @Conditional

关于了解一下下面的2个东西。

首先,它会在下面3个地方被处理:

  1. ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod:处理@Bean
  2. ConfigurationClassParser.processConfigurationClass(ConfigurationClass):处理@Configuration
  3. ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClass, SourceClass):处理@ComponentScan

常见的@Conditional注解,及其作用:

Conditional注解

作用

@ConditionalOnJndi

在JNDI存在的条件下查找指定的位置

@ConditionalOnBean

当容器里有指定Bean的条件下

@ConditionalOnJava

基于JVM版本作为判断条件

@ConditionalOnClass

当类路径下有指定的类的条件下

@ConditionalOnProperty

指定的属性是否有指定的值

@ConditionalOnResource

类路径是否有指定的值

@ConditionalOnExpression

基于SpEL表达式为true的时候作为判断条件才去实例化

@ConditionalOnMissingBean

当容器里没有指定Bean的情况下

@ConditionalOnMissingClass

当容器里没有指定类的情况下

@ConditionalOnWebApplication

当前项目时Web项目的条件下

@ConditionalOnOnSingleCandidate

当指定Bean在容器中只有一个,或者有多个但是指定首选的Bean

@ConditionalOnNotWebApplication

当前项目不是Web项目的条件下

关于@Conditional,可以看一下Spring Conditional原理与实例这篇文章。

4. @Import

@Import注解的参数有3种:

  1. 普通类直接注入

  2. 实现ImportSelector接口的类

  3. 实现ImportBeanDefinitionRegistrar接口的类

    public class ImportSelectorTest implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"vip.mycollege.User","vip.mycollege.Teacher"};
    }
    

    }

    public class ImportBeanDefinitionRegistrarTest implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition(Teacher.class);
        registry.registerBeanDefinition("teacher", beanDefinition);
    }
    

    }

处理Import的类是ConfigurationClassPostProcessor,这是一个BeanFactoryPostProcessor。在Spring核心流程梳理中我们介绍了这个是在BeanDefinition都解析好之后,在AbstractApplicationContext#refresh方法中通过invokeBeanFactoryPostProcessors来处理。

其中:@Import注解的处理逻辑在:ConfigurationClassParser的processImports方法中

for (SourceClass candidate : importCandidates) {
        //处理ImportSelector
        if (candidate.isAssignable(ImportSelector.class)) {
                if (selector instanceof DeferredImportSelector) {
                        this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                }else {
                        String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                        Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
                        processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
                }
        }
        //处理ImportBeanDefinitionRegistrar
        else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                Class<?> candidateClass = candidate.loadClass();
                ImportBeanDefinitionRegistrar registrar =
                                ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
                                                this.environment, this.resourceLoader, this.registry);
                configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
        }
        //处理其他类
        else {
                this.importStack.registerImport(
                                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
        }
}

5. @EnableConfigurationProperties

@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
}

EnableConfigurationPropertiesRegistrar又是一个ImportBeanDefinitionRegistrar。

主要注册了2个类:

  1. ConfigurationPropertiesBindingPostProcessor
  2. BoundConfigurationProperties

主要是ConfigurationPropertiesBindingPostProcessor是一个BeanPostProcessor,实现了postProcessBeforeInitialization,会在一个bean的属性值设置完之后,但是还没有执行init-method、 afterPropertiesSet等初始化之前被调用。

关于Spring Bean的生命周期可以看看:Spring Bean生命周期

具体处理逻辑是:

  1. 通过ConfigurationPropertiesBean的create方法找到@ConfigurationProperties注解并处理验证相关逻辑

  2. 通过ConfigurationPropertiesBindingPostProcessor自己的bind方法执行绑定操作

  3. bind的逻辑是在ConfigurationPropertiesBinder的bind方法,它会去找到并解析@ConfigurationProperties注解

    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties {

    /**
     * Server HTTP port.
     */
    private Integer port;
    

    }

所以我们在配置文件中配置的server.port=8080会生效。

6. 自定义starter

了解了上面的原理之后,自定义starter就没啥难的了。

一般官方的artifactId是spring-boot-starter-web,这种spring-boot-starter-xxx。

我们自定义的artifactId会使用:druid-spring-boot-starter,这种xxx-spring-boot-starter

只需要定义一个XXXAutoConfigure:

@Configuration
@EnableConfigurationProperties({NBProperties.class})
@Import({NBConfiguration.class})
public class NBDataSourceAutoConfigure {

    @Bean(initMethod = "init")
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        return new NBDataSourceWrapper();
    }
}

通过@Import注解添加一下默认配置需要的类,通过@Bean注入一下默认需要的类实例。

然后在resources目录下创建一个META-INF目录,创建一个spring.factories文件,把自己的XXXAutoConfigure配置进去就可以了。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
vip.mycollege.autoconfig.DruidDataSourceAutoConfigure

如果你高兴,还可以加一个XXXProperties:

@ConfigurationProperties("wo.nb")
public class NBProperties {
    private String realNB;

7. 资料参考

SpringBoot属性配置说明

Spring核心流程梳理

Spring Conditional原理与实例

Spring @Configuration流程概述

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k