Spring Boot是由Pivotal团队提供的全新框架,其设计目的用来简化新Spring应用初始化搭建以及开发过程,该框架使用了我写的方式进行配置,从而开发人员不再需要定义样板化的配置,通过这种方式,Spring Boot将致力于在蓬勃发展的快速应用开发领域(Rapid Application Developoment)成为领导者。
Spring Boot 的特点如下:
- 创建独立的Spring 应用程序。
- 嵌入的Tomcat ,无须部署WAR文件。
- 简化Maven配置
- 提供生产就绪型功能,如指标,健康检查和外部配置。
- 绝对没有代码生成,以及对XML没有配置要求。
当然,这样的介绍似乎太过于官方化,好像没有什么帮助我们理解 Spring Boot 到底做了什么,我们不妨通过一个小小的例子来快速的了解Spring Boot .
首先我们搭建一个Maven工程,pom如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-study</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-study</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后我们创建一个Controller类
@RestController
public class TestController {
@RequestMapping("/home")
public String home(){
return "hello world";
}
}
@SpringBootApplication
public class SpringBootStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootStudyApplication.class, args);
}
}
以上就是我们要准备的示例的所有内容,最后我们尝试启动main函数并在浏览器中输入localhost:8080 ,发现浏览器显示如下图所示:
测试结果:
这一切都似乎完全超出我们的预料,按照之前的经验,如果要构建这样一套MVC体系,似乎是非常麻烦的,至少要引入一大堆的pom依赖,同时,最为神奇的是整个过程是我们似乎根本没有启动过Tomcat,但是当我们运行函数的时候Tomcat居然自动起起来了,而且还能通过浏览器访问,这一切都是那样的顺其自然,这里留下悬念,后面我们再来探索。
当然,如果你认为Spring Boot仅仅是封装了Tomcat那就大错特错了,一个流行的框架一定是有他的理念和创新,它绝对不是一个简简单单的封装就能搞定的。
可以认为 它是当前独立业务开发模块对外暴露可以直接调用的接口。
public interface HelloService {
String sayHello();
}
我们对这个接口做一个简单的实现,返回hello 字符串。
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello() {
return "hello !!";
}
}
以上的实现为了尽量屏蔽Spring Boot基础理论以外的东西,把演示设计处尽量简单,如果是真实业务,这个接口以及接口实现可能会非常复杂,甚至还会间接的依赖于非常多的其他bean,它基本上就是一个独立的业务模块,当然这个模块并不是自己部署的,而是运行在依赖它的主函数中,如果我们开发到这种程度,想要主函数感知的话,也并不是不可以,但是至少要让主工程知道当前业务的bean路径并加入到scan列表中,否则在Spring启动的过程中没有办法反cLient中所有的bean载入Spring容器,逻辑也就没法生效了,但是,随着业务的增长,模块也会越来越多,越来越分散,大量的配置在主函数中维护,这会造成主函数非常的臃肿及冲突严重,而且根据职责划分原则,以上的例子中主模块只关心自己是否使用外部依赖的模块以及对应的接口就好了,再让主模块感知对应的路径等细节信息显然是不合适的,于是,在Spring出来之前我们会尝试把Scan等配置项写入到XML里面。然后让主函数直接引用配置项,这样,主函数知道的事情就进一步减少了,但是还有没有有更好的解决方式呢?或者,还有没有更好的办法能让主函数做更少的事情呢?Spring Boot 做到这一点,继续追加代码,添加自动配置项。
读者会发现,我们刚开发的功能使用起来非常的方便,除了pom中引入了依赖,什么都没有做就直接使用模块内部的接口注入了。
@Autowired
private HelloService helloService;
这给模块给开发带来非常大的方便,同时也会后续的模块拆分提供了便利,因为当业务逐渐复杂的时候我们会引入大量的中间件,而这些中间件的配置,依赖,以及初始化是非常麻烦的,现在有了这种模式,它帮我们做了只关注逻辑的本身,那么Spring Boot是如何做到的呢?
探索SpringApplication启动Spring
我们找到主函数入口SpringBootDemoApplication,发现这个入口的启动还是比较奇怪的,这也是Spring Boot启动的必要做法,那么,这也可以作为我们分析Spring Boot 的入口。
@SpringBootApplication
public class SpringBootStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootStudyApplication.class, args);
}
}
当顺着SpringApplication.run方法进入的时候我们己经找到了SpringApplication的一个看似核心逻辑的方法。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//为Spring boot项目准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
//Spring boot启动banner打印
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
首先我们来看看环境配置这一块。
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
//获取或者创建环境变量,如果没有则创建,有则获取
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配制环境变量configureEnvironment(environment, applicationArguments.getSourceArgs());
//这一行代码很重要,我们的application.yml就是在这行代码中解析的
准备环境这一块,我们先来看最简单的方法getOrCreateEnvironment。
private ConfigurableEnvironment getOrCreateEnvironment() {
//如果环境存在,则直接返回,不存在则创建
if (this.environment != null) {
return this.environment;
}
//如果是web环境,就创建StandardServletEnvironment,否则创建普通的StandardEnvironment
if (this.webEnvironment) {
return new StandardServletEnvironment();
}
return new StandardEnvironment();
}
那什么环境下是web环境呢?我们来看看源码。在SpringApplication类中有一个initialize()方法,方法中有一行代码this.webEnvironment = deduceWebEnvironment();,对webEnvironment赋值。
private boolean deduceWebEnvironment() {
for (String className : " { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" }") {
if (!ClassUtils.isPresent(className, null)) {
return false;
}
}
return true;
}
最终得出结论。如果存在ConfigurableWebApplicationContext类,则使用StandardServletEnvironment环境,如果不存在,则使用StandardEnvironment环境。显然在web应用中肯定存在ConfigurableWebApplicationContext类,所以使用StandardServletEnvironment环境,其实在以前的博客中分析Spring 源码时也分析过StandardServletEnvironment这个类,但是为了大家不要那么麻烦,这里就再来分析一遍。我们进入StandardServletEnvironment类。
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
propertySources.addLast(new StubPropertySource("servletContextInitParams"));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource("jndiProperties"));
}
super.customizePropertySources(propertySources);
}
@Override
public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) {
WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}
}
好像有点可惜,因为new StandardServletEnvironment()好像没有做任何事情嘛?真的吗?那先来看一下类结构。
从类结构中,发现他继承了StandardEnvironment,实际上是对StandardEnvironment功能的扩展,扩展的内容就是为Spring环境中加入更多的环境参数,例如servletConfigInitParams,servletContextInitParams,和jndiProperties。我们到父类StandardEnvironment中去看看。
public class StandardEnvironment extends AbstractEnvironment { @Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new MapPropertySource("systemProperties", getSystemProperties())); propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", getSystemEnvironment())); }
}
好像也没有什么发现,也就是标准环境参数中设置了系统环境属性及变量,不到黄河心不死,我们继续看父类。
public abstract class AbstractEnvironment implements ConfigurableEnvironment { private final MutablePropertySources propertySources = new MutablePropertySources(this.logger); public AbstractEnvironment() {
customizePropertySources(this.propertySources); if (logger.isDebugEnabled()) {
logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
}
}
省略...
}
test api-web.jar >/dev/null 2>&1 & 】命令来启动Spring boot项目,其中spring.profiles.active参数就是来区分不同的环境(dev 为开发环境,test为测试环境,online为线上环境),而spring.profiles.active参数的获取就是在System.getProperties()方法中。先来举个例子,在idea 中配置环境变量test。
在系统属性中,我们看到了spring.profiles.active=test,那么只需要启动时配置active就能决定读取哪个配置文件中的配置了吗?细心的读者可能会想,如果我们在application.yml配置了spring.profiles.active=dev,同时又在项目启动时设置了参数-Dspring.profiles.active=test,那么Spring boot会使用哪个active呢?疑问先留在这里,我们继续来跟进源码。
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { configurePropertySources(environment, args); //设置active文件configureProfiles(environment, args);}
configurePropertySources上述两个方法,第一个方法主要是将SpringApplication中配置的默认defaultProperties加入到环境中,同时将args也加入到环境中。代码如下
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources();
//如果配置了defaultProperties
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
//默认的配置添加到sources的最后,起到兜底r的作用,因为getProperty方法获取属性, //先遍历sources,取到了,则直接返回了,当最终都没有找到属性,就只能到默认的defaultProperties中获取属性了
sources.addLast(
new MapPropertySource("defaultProperties", this.defaultProperties)); } if (this.addCommandLineProperties && args.length > 0) {
String name = "commandLineArgs"; if (sources.contains(name)) { //如果环境中己经存在名字为commandLineArgs的对象,则创建一个新的包装类CompositePropertySource, //替换掉原来的SimpleCommandLinePropertySource对象 //而CompositePropertySource中propertySources属性中存储多个SimpleCommandLinePropertySource对象
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource(
name + "-" + args.hashCode(), args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
//如果环境中没有commandLineArgs,则创建SimpleCommandLinePropertySource对象存储到环境中 //命令行配置的参数放到sources第一位,在获取属性时优先被获取到
SimpleCommandLinePropertySource也是一个数据源PropertySource ;但是它主要是存放命令行属性;例如启动参数Args;中的属性就会保存在这个对象中; 并且SimpleCommandLinePropertySource会被放入到Environment中; 所以也就可以通过{@link Environment#getProperty(String)}来获取命令行的值了
可能有人感觉上面的代码commandLineArgs这一块难以理解。那我们继续跟进代码。
public SimpleCommandLinePropertySource(String... args) { super(new SimpleCommandLineArgsParser().parse(args));}
看到了parse()方法,我们进入这个方法看看
class SimpleCommandLineArgsParser { //创建CommandLineArgs对象
public CommandLineArgs parse(String... args) { CommandLineArgs commandLineArgs = new CommandLineArgs(); for (String arg : args) {
//进循环,判断是否以--开头
if (arg.startsWith("--")) {
//如果是就截取,截取长度是参数长度,如果命令是--spring,那么就从s开始截取,包括s
String optionText = arg.substring(2, arg.length());
String optionName;
String optionValue = null;
//如果匹配到了=号,截取=号左边做optionName,右边做optionValue
if (optionText.contains("=")) {
optionName = optionText.substring(0, optionText.indexOf('='));
optionValue = optionText.substring(optionText.indexOf('=')+1, optionText.length());
}
else {
//如果没有=号,optionName 直接就是截取的optionText
optionName = optionText;
}
if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) {
throw new IllegalArgumentException("Invalid argument syntax: " + arg);
}
//这里将解析的参数添加到上面创建的CommandLineArgs对象中,该对象中有一个Map<String, List>来存放
commandLineArgs.addOptionArg(optionName, optionValue);
}
else {
//不是--开头就是直接添加到非选项参数
commandLineArgs.addNonOptionArg(arg);
}
}
//最后返回对象
那我们来看看网上怎么说,Spring对应用程序运行的命令行参数进行了抽象,这个抽象是类CommandLineArgs。
CommandLineArgs类将命令行参数分为两类:
- option 参数
- 以 --开头
- 可以认为是name/value对参数
- 例子 : --foo, --foo=bar
- 非 option 参数
- 不以 --开头
- 可以认为是只提供了value的参数(具体怎么理解这个值,看业务逻辑的需求)
从代码中得知,以–开头的参数设置到了CommandLineArgs对象的optionArgs参数中,将不以–开头的参数添加到CommandLineArgs的nonOptionArgs参数中。说了这么多,我们先来看一个例子,假如我们要在项目启动时设置分页默认参数,默认为第一页,每页10行。
测试结果,我们看到pageNum和pageSize设置到CommandLineArgs的optionArgs参数中。
我们说了那么多,关于CommandLineArgs和defaultProperties的设置,我们分析了那么久,那么使用场景是什么呢?我们在做后台管理系统时,经常遇到的问题是分页问题,分页最关键的两个参数就是pageNum和pageSize,如果前端传了分页参数,则使用前端的分页参数,如果不传,则使用我们默认的分页参数,比如产品经理有需求变动时,需要将原来每页10条变成每页20条,一种办法,就是在代码中写死,直接修改代码提交,合并,发布,还有另一种更加灵活的方法,就是直接将默认的分页参数以命令行的形式配置,当产品需要调整参数时,只需要重新启动项目,修改启动配置参数即可,为了避免运维在启动项目时,忘记加分页参数了,我们设置一个兜底的参数,这样即使运维忘记配置启动参数,程序也不会报错了。来看下面示例。
- 编写业务测试类
@RequestMapping("/query")public String home(Integer pageNum, Integer pageSize) { if (pageNum == null) { ApplicationContext applicationContext = SpringContextUtils.getApplicationContext();
Environment environment = applicationContext.getEnvironment();
pageNum = Integer.parseInt(environment.getProperty("pageNum"));
pageSize = Integer.parseInt(environment.getProperty("pageSize"));
System.out.println("前端没有设置pageNum,pageSize ,使用系统默认的配置参数");
}
System.out.println("====pageNum==" + pageNum + ", pageSize = " + pageSize);
return "SUCESS";
}
- 设置兜底参数
@SpringBootApplicationpublic class SpringBootStudyApplication { public static void main(String[] args) { SpringApplication springApplication = new SpringApplication(SpringBootStudyApplication.class);
Map<String, Object> defaultProperties = new HashMap<>();
defaultProperties.put("pageNum",1);
defaultProperties.put("pageSize",20);
//设置兜底参数
springApplication.setDefaultProperties(defaultProperties); springApplication.setBannerMode(Banner.Mode.CONSOLE); springApplication.run(args); //SpringApplication.run(SpringBootStudyApplication.class, args);
}
}
测试1: 运维设置了pageNum=1和pageSize=18,但是前端没有传pageNum和pageSize
测试结果:
测试2:运维运维忘记配置pageNum和pageSize,但是前端没有传pageNum和pageSize,使用默认的兜底参数
再来看一下getProperty()方法的源码。
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { //遍历所有的propertySources
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
//调用每个propertySource的Property方法
Object value = propertySource.getProperty(key);
//如果value不为空,转化为泛型类型
从上述代码中可以看到,越在前的propertySource,优先获取到,因此defaultProperties只有做为兜底参数。
我想我们分析了那么多环境为一块,感觉还是少了点东西,那是什么呢?
我们继续来分析configureProfiles()方法,在这个方法中,设置了active区分线上,开发环境。
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) { environment.getActiveProfiles();
在这一行代码中environment.getActiveProfiles();确保了spring.profiles.active的存在。那是如何确保的呢?我们来看代码。
public String[] getActiveProfiles() { return StringUtils.toStringArray(doGetActiveProfiles());}protected Set doGetActiveProfiles() { synchronized (this.activeProfiles) { if (this.activeProfiles.isEmpty()) { String profiles = getProperty("spring.profiles.active"); if (StringUtils.hasText(profiles)) { //profiles以逗号隔开
我们一般情况下指定一个配置文件,但是有个时候,我们需要每个环境指定多个配置文件,不同的配置文件中配制相关的业务。先来举个例子吧。在开发环境中配置两个配置文件application-dev.yml,application-dev1.yml,需要同时使用两个配置文件中的内容。
//test1.profile配置在application-dev1.yml并且配置了yyy @Value("${test1.profile}") private String profile1; //test.profile配置在application-dev.yml并且配置了xxx
配置了两个配置文件。
测试结果
从上述测试结果来看,Spring Boot读取了两个配置文件application-dev.yml,application-dev1.yml 并且将test1.profile和test.profile设置到环境中。
那又有读者会问了,application.yml文件又是在何时被解析的呢?又和propertySources有什么关系呢?在茫茫代码中,不知道何处去寻,那就等吧,我们在setActiveProfiles()方法中打一个断点。
发现竟然是在监听器中解析的。超出我的意料。
这里是一个关键点,就是在初始化MutablePropertySources的时候依赖的一个变量environment,Environment是Spring所有配置文件转换KV的基础,而后续的一系列操作都是在environment基础上做的进一步封装,那么我们就再来探索一下environment的实例路径。如下图所示
可知,environment初始化过程并不是之前通过在PostProcessor类型的扩展口上做扩展的,而是通过ConfigFileApplicationListener监听机制完成的,当然这里面的重点步骤load方法,它是整个流程的核心点:
public void load() { this.propertiesLoader = new PropertySourcesLoader(); this.activatedProfiles = false; this.profiles = Collections.asLifoQueue(new LinkedList<Profile>()); this.processedProfiles = new LinkedList<Profile>(); //通过profile标识不同的环境,可以通过设置spring.profile.active和spring.profiles.default, //如果设置了active,default就失去了作用,如果这两个设置都没有,那么带profiles的bean都不会生成
Set<Profile> initialActiveProfiles = initializeActiveProfiles();
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
if (this.profiles.isEmpty()) {
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
if (!this.profiles.contains(defaultProfile)) {
this.profiles.add(defaultProfile);
}
}
}
//支持不添加任何profile注解的bean的加载
this.profiles.add(null);
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
//Spring Boot默认从4个位置查的application.properties文件就是从getSearchLocations() //方法中返回的, //1.当前目录下的/config //2.当前目录 //3.类路径下的/config目录 //4.类路径的根目录
for (String location : getSearchLocations()) { if (!location.endsWith("/")) { //如果目录不以/结尾,则表示路径本身就是一个文件,如路径为/Users/quyixiao/Desktop, //则Spring boot 就会去解析Desktop文件,而不会到/Users/quyixiao/Desktop/目录下寻找了。 //如果路径为/Users/quyixiao/Desktop/,则会到目录下寻找相应的文件
load(location, null, profile); } else { //如果没有配置则默认从application.properties中加载,约定大于配置
for (String name : getSearchNames()) { load(location, name, profile); } } } this.processedProfiles.add(profile); } addConfigurationProperties(this.propertiesLoader.getPropertySources());
}
我们之前不是愁怎样找到application文件吗?看到getSearchLocations()方法后,我们就知道了在哪个路径下找文件了。我们进入getSearchLocations方法。
private Set<String> getSearchLocations() { //项目启动时是否配置了spring.config.location参数,如果配置该参数,则到该目录下寻找
Set<String> locations = new LinkedHashSet<String>();
if (this.environment.containsProperty("spring.config.location")) { for (String path : asResolvedSet( this.environment.getProperty("spring.config.location"), null)) { if (!path.contains("$")) { path = StringUtils.cleanPath(path); if (!ResourceUtils.isUrl(path)) { path = "file:"+ path; } } locations.add(path); } } //默认到classpath:/,classpath:/config/,file:./,file:./config/下寻找
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
"classpath:/,classpath:/config/,file:./,file:./config/")); return locations;}
getSearchLocations()方法的原理也很简单,看环境中是否配置了spring.config.location参数,如果配置了,则到spring.config.location下寻找配置文件,如果没有配置,则默认到classpath:/,classpath:/config/,file:./,file:./config/目录下寻找配置文件,
文件路径确定了,那文件名又该如何确定呢?我们进入getSearchNames()方法中看看。
private Set<String> getSearchNames() { if (this.environment.containsProperty("spring.config.name")) { return asResolvedSet(this.environment.getProperty("spring.config.name"), null); } return asResolvedSet(ConfigFileApplicationListener.this.names, "application");}
getSearchNames()方法和getSearchLocations()原理一样,如果环境中配置了spring.config.name属性,则找spring.config.name文件名作解析,如果没有则,使用系统默认的文件名application。
可能会有读者问了,Spring Boot 这么意图是什么呢?我猜想,一方面Spring为了提供配置的灵活性,你想用什么路径,什么文件名,就用什么,提供配置的灵活性,同时也是有应用场景的,一般为了安全我们不会将线上的数据库配置文件也写到项目中,有一种办法就是在项目打包的时候,写一个脚本,先将配置文件考呗到项目中再进行打包,显然不是那样的优雅,那该如何使用呢?在项目启动时nohup /usr/local/jdk1.8.0_74/bin/java -jar -Dspring.profiles.active=test spring.config.location=/home/admin/web/ spring.config.name=application api-web.jar >/dev/null 2>&1 &
这样,我们只需要在项目启动时指定配置文件的路径和文件名,而不需要在打包之前,将配置文件拷呗到项目中再进行打包了。接下来,我们模似线上环境发布,测试一下效果。
- 在idea中模似线上环境
- 修改config-name
- 在我的桌面添加线上配置文件
- 在配置文件中添加配置
- 创建测试方法,并测试
- 上述过程中我们需要注意的一点是,因为默认的配置路径是classpath:/,classpath:/config/,file:./,file:./config/,因此,为了配置文件统一存放在一个目录下,我们也可以创建一个config目录,将配置文件放在config目录下,更加好统一管理。如:
那我们找到了配置文件的路径和文件名,那我们怎样来解析yml或properties文件呢?我们接着之前的代码继续分析。
private void load(String location, String name, Profile profile) { String group = "profile=" + (profile == null ? "" : profile); if (!StringUtils.hasText(name)) { //如果配置的路径没有后缀 / 的情况 loadIntoGroup(group, location, profile); } else { //遍历所有的PropertySourceLoader 文件后缀名 //PropertiesPropertySourceLoader支持properties和xml的解析 //YamlPropertySourceLoader支持"yml", "yaml"的解析 for (String ext : this.propertiesLoader.getAllFileExtensions()) { if (profile != null) { loadIntoGroup(group, location + name + "-" + profile + "." + ext, null); for (Profile processedProfile : this.processedProfiles) { if (processedProfile != null) { loadIntoGroup(group, location + name + "-" + processedProfile + "." + ext, profile); } } //对application-dev.yml文件解析loadIntoGroup(group, location + name + "-" + profile + "." + ext, profile); } //对application.yml配置文件解析loadIntoGroup(group, location + name + "." + ext, profile); } }}
从配置文件的解析顺序,我们得出一个结论,如果相同的配置,application-dev.yml中也配置了,application.yml中也配置了,如启动端口,则以application-dev.yml中的配置为准。因为其先被解析。而getProperty的逻辑是,遍历propertySources,越早被解析的配置文件,配置的属性优先被使用。举个例子,在application-dev.yml中配置了server.port=8888,在application中配置了server.port=8502,那最终项目启动占用的端口是8888。
我们接着继续分析配置文件又是如何解析的呢?进入loadIntoGroup方法。
private PropertySource<?> loadIntoGroup(String identifier, String location, Profile profile) { try { return doLoadIntoGroup(identifier, location, profile); } catch (Exception ex) { throw new IllegalStateException( "Failed to load property source from location '" + location + "'", ex); }
}
private PropertySource<?> doLoadIntoGroup(String identifier, String location, Profile profile) throws IOException { Resource resource = this.resourceLoader.getResource(location); PropertySource<?> propertySource = null; StringBuilder msg = new StringBuilder(); if (resource != null && resource.exists()) { String name = "applicationConfig: [" + location + "]";
String group = "applicationConfig: [" + identifier + "]";
propertySource = this.propertiesLoader.load(resource, group, name, (profile == null ? null : profile.getName())); if (propertySource != null) { msg.append("Loaded "); handleProfileProperties(propertySource); } else {
msg.append("Skipped (empty) ");
}
}
else {
msg.append("Skipped ");
}
msg.append("config file ");
msg.append(getResourceDescription(location, resource));
if (profile != null) {
msg.append(" for profile ").append(profile);
}
if (resource == null || !resource.exists()) {
msg.append(" resource not found");
this.logger.trace(msg);
}
else {
this.logger.debug(msg);
}
return propertySource;
}
上述方法中最重要的就是load方法了。我们继续跟进load方法。
public PropertySource<?> load(Resource resource, String group, String name, String profile) throws IOException { if (isFile(resource)) { String sourceName = generatePropertySourceName(name, profile); for (PropertySourceLoader loader : this.loaders) { if (canLoadFileExtension(loader, resource)) { PropertySource<?> specific = loader.load(sourceName, resource, profile); addPropertySource(group, specific, profile); return specific; } } }
return null;
}
上述代码选资源加载器PropertySourceLoader就像我们人一样,老板拿了一个活过来,问大家,谁有能力去做这个事情,每个人都问一遍,始终有人会做,就让这个会做的人,将结果给老板即可。
对于后缀的文件,会使用YamlPropertySourceLoader来解析。接下来,我们来看其load方法的实现。
public PropertySource<?> load(String name, Resource resource, String profile) throws IOException { if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) { Processor processor = new Processor(resource, profile); Map<String, Object> source = processor.process(); if (!source.isEmpty()) { return new MapPropertySource(name, source);
}
}
return null;
}
上述需要注意的是,当前类环境下必需有org.yaml.snakeyaml.Yaml类,才能解析yml文件,解析结果得到一个map。为了方便理解,我们自己来手写一个yml文件用例看看。
public class YmlTest { public static void main(String[] args) throws Exception{ ResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource("application.yml"); Processor processor = new Processor(resource, null); Map<String, Object> source = processor.process(); System.out.println(source);
}
private static class Processor extends YamlProcessor {
Processor(Resource resource, String profile) {
if (profile == null) {
setMatchDefault(true);
setDocumentMatchers(new SpringProfileDocumentMatcher());
}
else {
setMatchDefault(false);
setDocumentMatchers(new SpringProfileDocumentMatcher(profile));
}
setResources(resource);
}
@Override
protected Yaml createYaml() {
return new Yaml(new StrictMapAppenderConstructor(), new Representer(),
new DumperOptions(), new Resolver() {
@Override
public void addImplicitResolver(Tag tag, Pattern regexp,
String first) {
if (tag == Tag.TIMESTAMP) {
return;
}
super.addImplicitResolver(tag, regexp, first);
}
});
}
public Map<String, Object> process() {
final Map<String, Object> result = new LinkedHashMap<String, Object>();
process(new MatchCallback() {
@Override
public void process(Properties properties, Map<String, Object> map) {
result.putAll(getFlattenedMap(map));
}
});
return result;
}
}
}
【测试结果】
我相信很多人都不知道yml是怎样解析的,一般都要到百度上找找,其实,我们直接在Spring Boot源码中也能找到解析代码。就是上面的测试用例。
关于整个环境的配置文件解析,我觉得己经很清楚了,就告一段落。
这里面涉及我们经常使用的profile机制的实现,profile机制是Spring提供的一个用来标明当前运行环境的注解,我们正常开发过程中经常遇到这样的问题,开发环境是一套环境,QA测试是一套环境,线上部署又是另外一套环境,从开发到测试再到部署,会对程序中的配置修改多次,特别是QA到上线这个环节,经过QA测试的也不敢保证改了哪个配置后能不能在线上运行。
为了解决上面的这个问题,我们一般会用一种方法,配置文件,然后通过不同的环境读取不同的配置文件,从而在不同的场景中运行我们的程序。
Spring中的profile机制作用就体现这里,在Spring使用DI来注入的时候,能够根据当前制定的运行环境来注入相应的bean,最常见的就是使用不同的环境对应不同的数据源。
这个机制的实现就是load(location,name,profile)这段代码中来控制,这里只会加载当前设置的profile对应的配置文件。
我们花了大量的篇幅来分析Spring Boot环境变量这一块,下面来看看源码中的小技巧。
StopWatch stopWatch = new StopWatch();
stopWatch.start();
省略…
stopWatch.stop();
Spring这样做的目的是什么呢?我们发现stopWatch最终传入了logStarted()方法中,我们跟进代码。
public void logStarted(Log log, StopWatch stopWatch) { if (log.isInfoEnabled()) { log.info(getStartedMessage(stopWatch)); }}private StringBuilder getStartedMessage(StopWatch stopWatch) { StringBuilder message = new StringBuilder(); message.append("Started "); message.append(getApplicationName()); message.append(" in ");
message.append(stopWatch.getTotalTimeSeconds()); try { double uptime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000.0; message.append(" seconds (JVM running for " + uptime + ")"); } catch (Throwable ex) { } return message;
}
当看到getTotalTimeSeconds()方法,我相信读者恍然大悟。原来是统计Spring启动的时间的,是否是我们猜想的那样呢?那我们来看一个例子吧。
public static void main(String[] args) throws Exception{ StopWatch stopWatch = new StopWatch(); stopWatch.start(); Thread.sleep(2300);
【测试结果】
根据结果我可以得知,StopWatch就是用来统计容器加载启动时间的,并且获得所花的秒值。按我们一般人来写,直接在加载之前start = System.currentTimeMillis(); 加载完之后,end = System.currentTimeMillis();
启动时间是 end - start 即可。我们不得不感叹Spring 团队写代码就是优雅。
接下来,我们继续来看一些好玩的事情。
那上面的图案是哪里打印的呢?从字面意思理解就是printBanner()方法打印的,那真是这样吗?我们进入方法看看。
private Banner printBanner(ConfigurableEnvironment environment) { //如果banner的模式是 Banner.Mode.OFF,则不打印banner
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader
: new DefaultResourceLoader(getClassLoader());
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
resourceLoader, this.banner);
//bannerMode是Mode.LOG则会在服务器上打印banner, //如果不是,则只有在本地开发时打印,在服务器上不会打印
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
//默认情况下bannerMode是Banner.Mode.CONSOLE,因此只有在本地打印启动banner,服务器上不打印banner
从上述方法中,我们可以看到,banner是否打印,以及打印方式和bannerMode有关系,那bannerMode又该如何设置呢?在代码中寻寻觅觅,没有找到合适的地方进行设置,没有办法,只能以不太优雅的操作来修改banner的打印方式。如下:
public static void main(String[] args) { SpringApplication springApplication = new SpringApplication(SpringBootStudyApplication.class); //当设置bannerMode为Banner.Mode.OFF,关闭banner打印,设置bannerMode为Banner.Mode.LOG,则服务器上也能打印banner
springApplication.setBannerMode(Banner.Mode.LOG);
springApplication.run(args);
//SpringApplication.run(SpringBootStudyApplication.class, args);
接下来,我们继续来看,既然能修改打印方式,那打印的内容是否能修改呢?想想Spring的灵活性,不可能不给我们考虑到,因此肯定能修改,那该怎样修改呢?我们继续跟进源码。
public Banner print(Environment environment, Class<?> sourceClass, PrintStream out) { Banner banner = getBanner(environment, this.fallbackBanner); banner.printBanner(environment, sourceClass, out); return new PrintedBanner(banner, sourceClass);}private Banner getBanner(Environment environment, Banner definedBanner) { Banners banners = new Banners(); banners.addIfNotNull(getImageBanner(environment)); banners.addIfNotNull(getTextBanner(environment)); if (banners.hasAtLeastOneBanner()) { return banners; } if (this.fallbackBanner != null) { return this.fallbackBanner; } return new SpringBootBanner();}
private Banner getImageBanner(Environment environment) { //如果环境变量中配置了banner.image.location, //则取该位置的图片做为打印的banner
String location = environment.getProperty("banner.image.location"); if (StringUtils.hasLength(location)) { Resource resource = this.resourceLoader.getResource(location); return (resource.exists() ? new ImageBanner(resource) : null); } // 如果当前resource下有banner.gif或banner.jpg,或banner.png,则作为打印的banner
for (String ext : { "gif", "jpg", "png" }) {
Resource resource = this.resourceLoader.getResource("banner." + ext);
if (resource.exists()) {
return new ImageBanner(resource);
}
}
return null;
}
private Banner getTextBanner(Environment environment) { //如果环境中配置了banner.location,则取之文本作为banner打印, //如果没有则取当前resource下的banner.txt作为默认的banner
String location = environment.getProperty("banner.location", "banner.txt"); Resource resource = this.resourceLoader.getResource(location); if (resource.exists()) { return new ResourceBanner(resource); } return null;}
既然源码都写得这么清楚了,那就按源码的意思测试一把。
- 在resources下添加banner.txt
- 【开始测试】
- 如果想自定义自己文本图案,下面地址就是绘制字符图案。
http://patorjk.com/software/taag/#p=display&f=Graffiti&t=XXXQ - 我们将banner.txt换成banner.jpg试试,那我就用我的女神刘亦菲来测试一把
很遗憾,测试结果失败,Spring Boot 不让女神陪我们开发 ,那我到网上找了一张其他次一点的图片。
测试结果出来了,显然可以个性化指定一些图片来作为项目启动的banner。但是需要注意的一点是SpringBoot的版本。
刚刚开始,使用1.5.12.RELEASE版本时,无论哪张图片都不支持,后面使用了2.1.5.RELEASE版本,才打印出2020的图案,因此,图片banner的设置还和Spring Boot的版本有关系,所以在设置时当发现设置图片banner无效时,可以看一下你的版本是否过低。不过为了方便起见,我还是用1.5.12.RELEASE来做源码分析,越高版本的代码,可能封装程度越高,但是对于阅读源码者来说,不一定是好事,反而早期的代码原理理解起来更加方便一点。
在这里,我们发现其他几个关键的字眼。
context = createApplicationContext();
refreshContext(context);
afterRefresh(context, applicationArguments);
如果读者看过之前的内容,就会知道,我们曾经介绍过Spring的初始化方案,其中最核心的就是SpringContext的创建,初始化,刷新等,那么我们可以直接进入查看其中的逻辑,同时Spring作为一个全球都使用的主流框架,会有非常多的需要考虑的问题,我们在阅读源码的过程中只需要关系核心的主流程,了解其工作原理,并阅读的过程中感受它的代码风格以及设计理念就好了,如果真的追求理解每一行代码真是非常耗时一个件事情。
SpringContext创建
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);
}
};
这个函数似乎没有什么特别的地方,无非就是实例化一个ApplicationContext,因为ApplicationContext昌Spring存在的基础,而对应的SpringContext候选类如下;
SERVLET = new AnnotationConfigServletWebServerApplicationContext();
REACTIVE = new AnnotationConfigReactiveWebServerApplicationContext();
default = new AnnotationConfigApplicationContext();
这里有个关键的判断webApplicationType,如果读者没有看过代码很容易忽略,但是这里将成为在前面提到过的Spring如何自动化启动Tomcat的关键,我们将在后面继续研究。
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment); postProcessApplicationContext(context); applyInitializers(context); listeners.contextPrepared(context); bootstrapContext.close(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); } if (this.lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } // Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
这里面的load函数是我们比较感兴趣的,代码如下:
protected void load(ApplicationContext context, Object[] sources) { if (logger.isDebugEnabled()) { logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources)); } BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources); if (this.beanNameGenerator != null) { loader.setBeanNameGenerator(this.beanNameGenerator); } if (this.resourceLoader != null) { loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
loader.load();}
相信读者看到BeanDefinitionLoader这个类的时候,基本上己经知道后续的逻辑了,bean的加载作为本书的最核心的部分在之前就己经分析过了。虽然己经解析过了,但是还是要看一下。
public int load() { int count = 0; for (Object source : this.sources) { count += load(source); } return count;}private int load(Object source) { Assert.notNull(source, "Source must not be null"); //source是class类型
if (source instanceof Class<?>) {
return load((Class<?>) source); } //source是xml类型
if (source instanceof Resource) {
return load((Resource) source); } //source是包类型
if (source instanceof Package) {
return load((Package) source); } if (source instanceof CharSequence) { return load((CharSequence) source); } throw new IllegalArgumentException("Invalid source type " + source.getClass());}
之前有很多的博客都是围绕着xml及类的解析,xml我们都知道,通过loadBeanDefinitions将xml中所有的bean配置都解析成BeanDefinition,而类的解析就是对类的配置的注解解析,解析成beanDefinition。而在Spring Boot中,主要是类的解析,很少再用xml,因此,在这里,我们跟进类的解析。
private int load(Class<?> source) { if (isGroovyPresent()) { if (GroovyBeanDefinitionSource.class.isAssignableFrom(source)) { GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class); load(loader); } } if (isComponent(source)) { this.annotatedReader.register(source); return 1; } return 0;}public void register(Class<?>... annotatedClasses) { for (Class<?> annotatedClass : annotatedClasses) { registerBean(annotatedClass); }}public void registerBean(Class<?> annotatedClass) { registerBean(annotatedClass, null, (Class<? extends Annotation>[]) null);}public void registerBean(Class<?> annotatedClass, String name, Class<? extends Annotation>... qualifiers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
return;
}
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); if (qualifiers != null) { for (Class<? extends Annotation> qualifier : qualifiers) { if (Primary.class == qualifier) { abd.setPrimary(true); } else if (Lazy.class == qualifier) { abd.setLazyInit(true); } else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } } }}
看到上面的代码,我相信如果读者看过我之前的博客的小伙伴,肯定很熟悉了,这不就是使用ASM技术将Class的注解解析成BeanDefinition吗?博客如下
但是要理解resolveScopeMetadata方法,需要对类字节码结构有一定了解的小伙伴,才能读懂里面的代码,之前也己经分析过,这里就不再赘述,但是对ASM解析类字节码的原理,我觉得不了解的话,还是去研究一下,因为Spring很多的地方都有他的影子,如Spring MVC中Controller方法中参数注入,MyBatis中Mapper.java中的方法参数和Mapper.xml中变量映射,都用到了ASM,如果这一块不了解,可能去解析源码,也是有缺憾的。
Spring 扩展属性的加载
protected void refresh(ApplicationContext applicationContext) { Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext); refresh((ConfigurableApplicationContext) applicationContext);}
对于Spring的扩展属性的加载则更加简单,因为这些都是Spring本身原有的东西,Spring Boot仅仅是使用refresh激活下而已,如果读者想回顾refresh的详细逻辑,可以看之前的博客。
总结 :
分析下来,Spring Boot的启动并不是我们想像的那样神秘,按照约定大于配置的原则,内置了Spring原有启动类,并在启动的时候及刷新 ,仅此而已。
org.springframework.context.annocation.AnnotationConfigApplicationContext
Starter自动化配置原理
我们己经知道了Spring Boot如何启动Spring的,但上目前为止我们并没有揭开Spring Boot的面纱,究竟Starter是如何生效的呢?这些逻辑现在看来只能体现在注解上SpringBootApplication本身了。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration
@EnableAutoConfiguration
这其中我们更加关注SpringBootApplication上的注解内容,因为注解具有传递性,EnableAutoConfiguration是个非常特别的注解,它是Spring Boot的开关,如果把这个注解去掉,则一切Starter都会失效,这就是约定大于配置的潜规则了,那么Spring Boot的核心很可能就藏在这个注解里面;
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
AutoConfigurationImportSelector作为Starter自动化导入关键选项终于浮现出来,那么Spring是怎样识别这个注解起作用的呢?我们来看这个类中只有一个办法,那么只要看一看到底是哪个方法调用它,就可以顺藤摸瓜找到最终调用的地方。
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { protected boolean isEnabled(AnnotationMetadata metadata) { if (getClass() == AutoConfigurationImportSelector.class) { return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true); } return true; }}
spring.factories 的加载
顺着思路反向查找,看一看空间谁在那里调用了isEnabled函数,强大的编译器很容器帮我们定位到AutoConfigurationImportSelector类的方法:
public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}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的函数。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), 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;}
从上面的函数中我们可以看到META-INF/spring.factories,在我们之前演示的环节,按照约定大于配置的原则,Starter如果要生效则必需在META-INF文件下建立spring.factories文件,并把相关的配置类声明在里面,虽然这仅仅是一个报错异常提示,但是其实我们己经可以推断出来这一定就是逻辑的处理之处,继续进入SpringFactoriesLoader类。
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { ClassLoader classLoaderToUse = classLoader; if (classLoaderToUse == null) { classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); } String factoryTypeName = factoryType.getName(); return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());}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("META-INF/spring.factories"); 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 [" +
"META-INF/spring.factories" + "]", ex); } return result;}
至此,我们终于明白了为什么Starter的生效必需要依赖于配置META-INF/spring.factories文件,因为在启动的过程中有一个硬编码的逻辑就是会扫描各个包的对应文件,并把配置捞取出来,但是捞取出来后又是怎样Spring整合的呢?或者说AutoConfigurationImportSelector.selectImport方法后把加载的类委托给谁来处理的呢?
factories调用时序图
META-INF/spring.factories中的配置文件是如何与Spring整合的呢?其路径还是比较深的,这里就不大段的话代码了,可以通过一个图去理解逻辑。
梳理了从EMbeddedWebApplicationContext到AutoConfigurationImportSelector的调用链路,当然这个链路还有非常多的额外分支被忽略,不过至少从上图中我们可以很清晰的看到AutoConfigurationImportSelector与Spring的整合过程,在这个调用链中最核心的就是Spring Boot的使用了Spring提供的BeanDefinitionRegistryPostProcessor扩展并实现了ConfigurationClassPostProcessor类,从而实现了spring之上的一系列逻辑扩展,让我们看一下ConfigurationClassPostProcessor的继承关系。
当然Spring还提供了非常多的不同阶段的扩展点,读者可以通过前面的博客的内容获取详细的扩展点以及实现原理。
配置类的解析
截止到目前为止我们知道了Starter为什么要默认自身入口配置写在META-INF文件中的spring.factorries文件中,以及AutoConfigurationImportSelector的上下方调用链路,但是通过AutoConfiguuurationImportSelector.selectImports方法返回后的配置类又是如何进一步处理的呢?对照ConfigurationClassParser的processDeferredImportSelectors方法代码查看:
private static final Comparator<DeferredImportSelectorHolder> DEFERRED_IMPORT_COMPARATOR = new Comparator<ConfigurationClassParser.DeferredImportSelectorHolder>() { @Override public int compare(DeferredImportSelectorHolder o1, DeferredImportSelectorHolder o2) { return AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector()); } };private void processDeferredImportSelectors() { List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors; this.deferredImportSelectors = null; Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR); for (DeferredImportSelectorHolder deferredImport : deferredImports) { ConfigurationClass configClass = deferredImport.getConfigurationClass(); try { String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata()); processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
其中
String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
也就是说在Spring启动的时候会扫描所有的JAR中的spring.factories定义的类,而这些对于用户来说如果不是通过调试信息可能根本就感知不到。
processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
这个逻辑其实还是非常复杂的,其内部包含了各种分支的处理,我们不妨先通过时序图从全局的角度了解一下它的处理全貌 。
在图14-5基础上更细粒度的突出解析过程的时序图,从时序图中我们大致看到Spring全局的处理流程。
- ConfigurationClassPostProcessor作为Spring扩展点是Spring Boot 一系列的功能基础入口。
- ConfigurationClassParser 作为解析职责的基本处理类,包含了各种解析处理的逻辑,如@Import,@Bean,@ImportResource,@PropertySource,@ComponentScan等注解都是在这个注解类中完成的,而这个类对外开放的函数入口就是Parse方法,对应时序图中的步骤3。
- 在完成步骤3后,所有的解析结果己经通过3.2.2步骤放在了parse的configurationClasses属性中,这个时候对这个属性进行统一的Spring Bean硬编码注册,注册逻辑统一委托给ConfigurationClassBeanDefinitionReader,对外的接口是loadBeanDefinitions,对应步骤4 。
- 当然,在parse中的处理最复杂的是,parse中首先会处理自己本身的扫描的bean注册逻辑,然后才会处理Spring.factories定义的配置,处理spring.factores定义的配置首先就是要加载配置类,这个时候,EnableAutoConfigurationImportSelector提供的selectImports就被派上用场了,它返回的配置类需要进一步的解析,因为这些配置类中可能对应不同的类型,如@import,@Bean,@importResource,@PropertySource,@ComponentScan,而这些类又有不同的处理逻辑,例如ComponentScan,我们就能猜到这里面除了解析外一定还会有递归的解析处理逻辑,因为很有可能通过ComponentScan又扫描出了另一个ComponentScan配置。
Componentscan的切入点
这里重点讲解一下doProcessConfigurationClass函数,我们熟悉了很多注解逻辑实现都在这里。
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { processMemberClasses(configClass, sourceClass); for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } else { logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment");
}
}
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
//对扫描出来的类进行过滤
if (ConfigurationClassUtils.checkConfigurationClassCandidate(
holder.getBeanDefinition(), this.metadataReaderFactory)) {
//将所有扫描出来的类委托到parse方法中递归处理
parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
而以上的数据中传递过来的参数ConfiurationClass configClass就是spring.factories中定义的配置类,这里我们重点关注一下ComponentScan注解的实现逻辑,首先通过代码。
Set<AnnocationAttributes> componnetScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(),ComponentScans.class,ComponentScan.class);
获取对应的注解的信息。也就是对应的@ComponentScan("{com.spring.study.module}")中最主要的扫描信息,然手委托给ComponentScanAnnocationParser的parse进一步扫描
parse(holder.getBeanDefinition().getBeanClassName(),holder.getBeanName());
当然顺着思路继续跟进parse()方法,这里还会有一些额外的处理分支。我们顺着主流程一层层跟进,直到进入一个核心的解析类ComponentScanAnnocationParser的函数中。
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) { Assert.state(this.environment != null, "Environment must not be null"); Assert.state(this.resourceLoader != null, "ResourceLoader must not be null"); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
BeanUtils.instantiateClass(generatorClass));
//scopeProxy属性构造
ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
scanner.setScopedProxyMode(scopedProxyMode);
}
else {
Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
}
//resourcePattern 属性构造
scanner.setResourcePattern(componentScan.getString("resourcePattern"));
for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
//excludeFilters 属性设置
而上面提到的最为核心的工具类ClassPathBeanDefinitionScanner就是Spring原生的解析类,这里Spring核心解析类,它通过字节码扫描的方式,效率要比通常我们用的反射机制效率要高得多,如果读者日常工作中有扫描路径下类的需求,哪怕脱离了Spring环境也可以直接使用这个工具类,不知道读者是否还清楚,在介绍整个MyBatis的博客中动态扫描就是封装类似的类,有兴趣回头看看。
Spring提供了一个更能用的基于条件的bean的创建,使用@Conditional注解,@Conditional根据满足的某一个特定的bean,当某一个JAR包在一个类路径下的时候,会自动配置一个或多个bean,或者只有某个Bean被创建后才会创建另外一个bean,总的来说,就是根据特定的条件来控制bean的创建行为,这样我们就可以利用这个特性进行一些自动配置,当然,Conditional注解有非常多的使用方式,我们仅仅通过ConditionOrProperty来深入探讨它的运行机制,我们通过下面的一个示例来详细了解。
@Configuration@ComponentScan({"com.example.springbootstudy"})
@ConditionalOnProperty(prefix = "study",name="enable",havingValue = "true")
上面的声明想要的逻辑是如果配置性中显示的声明study.enable=true,则当前整套体系才生效,我们可以进行验证。
Conditional原理
好,了解了ConditionalOnProperty的使用后,我们继教深入探索它的内部实现机制,继续按照之前的思路,如果想反推ConditionalOnProperty的实现机制,那么代码中必然会存在ConditionalOnProperty.class的调用,于是我们搜索ConditionalOnProperty.class,如下图所示
发现所有的调用都出现了一个类OnPropertyCondition中,于是进入这个类,如下图所示 ,好在其中仅仅有一个public方法,这会大大减少我们分析的范围。
OnPropertyCondition类的getMatchOutcome方法如下:
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap( metadata.getAllAnnotationAttributes( ConditionalOnProperty.class.getName())); List<ConditionMessage> noMatch = new ArrayList<ConditionMessage>(); List<ConditionMessage> match = new ArrayList<ConditionMessage>(); for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) { ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment()); (outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage()); } if (!noMatch.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
}
return ConditionOutcome.match(ConditionMessage.of(match));
}
按照通常的设计,这里应该返回的是否配置了boolean值,但是现在返回了ConditionOutcome这样一个对象,这是什么道理呢?我们看一下这个数据结构 。
public class ConditionOutcome { private final boolean match; private final ConditionMessage message; ...}
这里面除了大量的方法外还有一个比较重要的属性字段,就是这个类型为boolean的match字段,根据直觉,大致可以断定这个属性很重要,再来看。
ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
对应的构造逻辑。
public static ConditionOutcome noMatch(ConditionMessage message) { return new ConditionOutcome(false, message);}
以及
ConditionOutcome.match(ConditionMessage.of(match));
public static ConditionOutcome match(ConditionMessage message) { return new ConditionOutcome(true, message);}
差别仅仅是这个属性的初始化值,那么根据这个信息可以断定 ,getMatchOutcome方法中noMatch这个属性的逻辑一定是整个逻辑的核心。
我们重新再去分析getMatchOutcome函数的逻辑。
List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap( metadata.getAllAnnotationAttributes( ConditionalOnProperty.class.getName()));
这句代码要扫描出ConditionalOnProperty的注解信息,例如我们刚才配置的。
@ConditionalOnProperty(prefix = “study”,name=“enable”,havingValue = “true”)
通过上面的断点信息,我们可以看到name对应的enabled属性已经被读取,如下图 所示,那么,现在核心的验证逻辑就应该在ConditionOutcome outcome = determineOutcome(annotationAttributes,context.getEnvironment())中了,顺着函数继续进行一步的探索。
private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) { Spec spec = new Spec(annotationAttributes); List<String> missingProperties = new ArrayList<String>(); List<String> nonMatchingProperties = new ArrayList<String>(); spec.collectProperties(resolver, missingProperties, nonMatchingProperties); if (!missingProperties.isEmpty()) { return ConditionOutcome.noMatch( ConditionMessage.forCondition(ConditionalOnProperty.class, spec) .didNotFind("property", "properties") .items(Style.QUOTE, missingProperties)); } if (!nonMatchingProperties.isEmpty()) { return ConditionOutcome.noMatch( ConditionMessage.forCondition(ConditionalOnProperty.class, spec) .found("different value in property", "different value in properties") .items(Style.QUOTE, nonMatchingProperties)); } return ConditionOutcome.match(ConditionMessage .forCondition(ConditionalOnProperty.class, spec).because("matched"));}
这个逻辑表明,不匹配有两种情况,missingProperties对应的属性缺失的情况,missingProperties对应的属性缺失的情况,nonMatchingProperties对应不匹配的情况,而这两个属性的初始化都在spec.collectProperties(resolver,missingProperties,nonMatchingProperties)中,于是进入这个函数。
private void collectProperties(PropertyResolver resolver, List missing, List nonMatching) { if (this.relaxedNames) { resolver = new RelaxedPropertyResolver(resolver, this.prefix); } for (String name : this.names) { String key = (this.relaxedNames ? name : this.prefix + name); if (resolver.containsProperty(key)) { if (!isMatch(resolver.getProperty(key), this.havingValue)) { nonMatching.add(name); } } else { if (!this.matchIfMissing) { missing.add(name); } }
终于,我们找到了对应的逻辑,这个函数尝试使用PropertyResolver来验证对应的属性是否存在,如果不存在则验证不通过,因为PropertyResolver中包含了所有的配置属性信息,而PropertyResolver的初始化以及相关的属性加载我们在下后面介绍 。
调用切入点
那么现在的问题是,OnPropertyCondition.getMatchOutcome方法是谁去调用呢?或者说这个类是如何与Spring整合在一起的呢?它又是怎样影响bean的加载逻辑的呢?我们再从全局的角度来梳理一下Conditional的实现逻辑,读者可以继续看图14-8中的bean的parse解析链路,processConfigurationClass步骤中主要的逻辑是对即将解析的注解做预处理,如下图所示
很清晰展示了Spring整个配置类解析及加载的全部过程,那么通过分析代码定位到原来整个判断逻辑切入点在processConfigurationClass中,代码如下。
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; } ConfigurationClass existingClass = this.configurationClasses.get(configClass); if (existingClass != null) { if (configClass.isImported()) { if (existingClass.isImported()) { existingClass.mergeImportedBy(configClass); } // Otherwise ignore new imported config class; existing non-imported class overrides it. return; }
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
for (Iterator<ConfigurationClass> it = this.knownSuperclasses.values().iterator(); it.hasNext(); ) {
if (configClass.equals(it.next())) {
it.remove();
}
}
}
}
// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);
this.configurationClasses.put(configClass, configClass);
}
代码的第一行就是整个Conditional逻辑生效的切入点,如果验证不通过则会直接忽略掉后面的逻辑,那么这个类属性以及componentScan之类的配置也自然不会得到解析了,这个方法会拉取所有的condition属性,onConditionProperty就是在这里拉取的。
public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) { if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { return false; } if (phase == null) { if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } List conditions = new ArrayList(); for (String[] conditionClasses : getConditionClasses(metadata)) { for (String conditionClass : conditionClasses) { Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); } } AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } if (requiredPhase == null || requiredPhase == phase) {
if (!condition.matches(this.context, metadata)) { return true; } } } return false;}
这个代码中有几个比较关键的地方。
- condition 的获取
通过上面的代码getConditionClasses(metadata)调用,因为代码走到这里己经是对某一特定的类解析,metadata中包含了完全的配置类信息,只要通过metadata.getAllAnnotationAttributes(Conditional.class.getName(),true)即可获取,所以这一步的逻辑并不复杂。
- condition 的运行配置
通过代码condition.matches(this.context,metadata)调用,因为我们配置为@ConditionalOnProperty(prefix=“study”,name = “enabled”,havingValue=“true”)
所以此时condition对应的运行态类为OnPropertyCondition。
属性自动化配置实现
通过上面的介绍,我们来测试一下Spring Boot会读取配置拼装成study.enabled并作为key,然后尝试使用PropertyResolver来验证对应的属性是否存在,如果不存在则验证不通过,自然也就不会继续后面的解析流程,因为PropertyResolver中包含了所有的配置信息。
@ConditionalOnProperty(prefix = “study”,name=“enable”,havingValue = “true”)
那么PropertyResolver又是如何被初始化的呢?同样,这样一个功能并不仅仅供Spring内部使用,在现在的Spring中,我们也可以通过Value注解直接将属性赋值给类的变量,这两个问题都涉及Spring的属性处理逻辑,我们在研究它的属性处理逻辑前先体验一下通过Value注解注入属性的样例。
在studyweb中的application.yml加入study.testStr=哈哈 ,如下图所示
运行后显示我们配置结果的属性,证明属性生效,如下图所示
原理
同样,要探索它的实现原理,按照之前的思路,我们首先定位关键字然后反推代码逻辑。我们通过搜索Value.class进行反推。
找到了一个看起来像调用点的地方,进入QualifierAnnotationAutowireCandidateResolver这个类查看代码。
private Class<? extends Annotation> valueAnnotationType = Value.class;
这是一个属性定义,那么进一步查看使用属性的地方。
protected Object findValue(Annotation[] annotationsToSearch) { AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes( AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType); if (attr != null) { return extractValue(attr); } return null;}
然后我们设置断点看一下系统在启动的时候是否在此停留,进而验证我们的判断,如下图所示。
果然,尝试运行代码后程序在断点处住 ,而尝试evaluate断点处的方法看到返回的就是我们@Value("${study.testStr}")中配置的值,因为属性注解己经找到,所以获取注解中的属性就比较简单了。
protected Object extractValue(AnnotationAttributes attr) { Object value = attr.get(AnnotationUtils.VALUE); if (value == null) { throw new IllegalStateException("Value annotation must have a value attribute"); } return value;}
现在要解决两个疑问。
- 表达式对应的值是在哪里被替换的。
- 表达式替换后的值又是如何与原有的bean整合的。
带着这两个疑问,我们顺着调用栈继续找线索,发现当前有获取的Value表达式属性后程序进入DefaultListableBeanFactory类的resolverEmbeddedValue方法,并且在尝试evaluate后发现返回的值正是属性替换后的值,如下图所示;
现在问题就比较清晰了,替换的逻辑一定在resolveEmbeddedValue方法中:
public String resolveEmbeddedValue(String value) { if (value == null) { return null; } String result = value; for (StringValueResolver resolver : this.embeddedValueResolvers) { result = resolver.resolveStringValue(result);
通过代码逻辑我们可以看到,对于属性的解析己经委托给StringValueResolver对应的实现类,接下来我们来分析一下这个StringValueResolver是如何初始化的。
StringValueResolver功能实现依赖Spring的切入点是PropertySourcesPlaceholderConfigurer,我们来一下它的依赖结构,如下图所示,它的关键的实现了BeanFactoryPostProcessor接口,从而利用实现对外扩展函数postProcessBeanFactory来进行对Spring的扩展。
继续通过对postProcessBeanFactory函数入口的分析来详细了解StringValueResolver初始化的全部过程,如下图所示,初始化的逻辑可以实现PropertySourcesPlaceholderConfigurer类的postProcessBeanFactory作为函数的入口。
1. 初始化MutablePropertySources
首先会通过this.environment来初始化MutablePropertySources,这里面有几点要说明的,environment是Spring属性加载的基础,里面包含了Spring己经加载的各个属性,而这所以使用MutablePropertySources封装,是因为MutablePropertySources还能实现单独加载自定义的额外属性功能。
2.初始化PropertySourcesPropertyResolver
使用PropertySourcesPropertyResolver对MutablePropertySources的操作进行进一步的封装。使得操作多个广播属性对外部不感知,当然PropertySourcesPropertyResolver还提供了一个重要的功能是对变量的解析,例如,它是初始化过程中包含这样的设置:
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);propertyResolver.setValueSeparator(this.valueSeparator);
而对应的变量定义如下:
propertyResolver.setPlaceholderPrefix("${");propertyResolver.setPlaceholderSuffix("}");propertyResolver.setValueSeparator(":");
4. StringValueResolver注册
最后将StringValueResolver实例注册到单例ConfigurableListableBeanFactory中,也就是在真正的解析变量时使用StringValueResolver实例。
Tomcat启动
截止到目前,我们己经完成了对Spring Boot基本功能的分析,包括SpringBoot的启动,属性自动化配置,conditional实现以及starter运行模式的原理,那么在之前的理论基础上再来分析Spring Boot是如何集成Tomcat会更加简化。
分析了tomcat嵌入原理首先找到扩展入口,我们可以从启动信息开始。
当然,为了整个说明的连贯性我们不是从入口说起。在之前讲过springContext创建的时候我们曾经提到过一段代码。
protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { contextClass = Class.forName(this.webEnvironment ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS); } catch (ClassNotFoundException ex) { throw new IllegalStateException( "Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);}
其中,下述代码就是默认的配置
public static final String DEFAULT_WEB_CONTEXT_CLASS = “org.springframework.”
+ “boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext”;
这也是Web扩展的关键,在我们曾经花了很大的篇幅讲解AbstractApplicationContext的一个函数refresh(),它是Springcontext扩展的关键,再来回顾一下:
public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { //准备刷新上下文环境
prepareRefresh();
//初始化BeanFactory,并进行XML文件的读取
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//对beanFactory进行各个功能填充
prepareBeanFactory(beanFactory);
try {
//子类覆盖方法做额外的处理
postProcessBeanFactory(beanFactory);
//激活各种BeanFactory处理器
invokeBeanFactoryPostProcessors(beanFactory);
//注册拦截Bean的创建Bean处理器,这里只是注册真正的调用是getBean函数
registerBeanPostProcessors(beanFactory);
//为上下文初始化message源,即不同的语言的消息体,国际化处理
initMessageSource();
//初始化应用消息的广播器,并放入"applicationEventMulticaster"bean中
initApplicationEventMulticaster();
//留给子类初始化其他的bean
onRefresh(); //所有注册的bean中查找Listener bean,注册到消息入手
registerListeners();
//初始化剩下的单实例(非惰性的)
finishBeanFactoryInitialization(beanFactory);
//完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
//在所有注册的bean中查找Listener bean,注册所有的广播器中
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
//
而EmbeddedWebApplicationContext类对于Tomcat嵌入的一个关键就是onRefresh()函数重写
protected void onRefresh() { super.onRefresh(); try { createEmbeddedServletContainer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start embedded container", ex); }}private void createEmbeddedServletContainer() { EmbeddedServletContainer localContainer = this.embeddedServletContainer; ServletContext localServletContext = getServletContext(); if (localContainer == null && localServletContext == null) { EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
EmbeddedServletContainerFactory是服务器启动上层抽象,无论是tomcat还是Jetty都要通过这个类实现对Spring服务器的注册,现在我们通过断点来看看返回结果:
正如我们料,它返回的就是Tomcat对应的子类实现,于是我们找到了TomcatEmbeddedServletContainerFactory来查看它的实现逻辑,但是却发现这个类既没有打一些Spring注册的注解,也没有配置任何配置文件中,那么它是如何注入Spring容器中实现的呢?
带着疑问,我们搜索代码,看一看是否会有其他的地方对这个类进行了硬编码的注册呢?
果然,发现EmbeddedServletContainerAutoConfiguration这个类进行了调用,这是Spring自动化整合各种服务器注册非常关键的入口类:
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@Configuration@ConditionalOnWebApplication@Import(BeanPostProcessorsRegistrar.class)public class EmbeddedServletContainerAutoConfiguration { /** * Nested configuration if Tomcat is being used. */ @Configuration @ConditionalOnClass({ Servlet.class, Tomcat.class }) @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat { @Bean public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() { return new TomcatEmbeddedServletContainerFactory(); } } /** * Nested configuration if Jetty is being used. */ @Configuration @ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
WebAppContext.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
@Bean
public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
return new JettyEmbeddedServletContainerFactory();
}
}
/**
* Nested configuration if Undertow is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedUndertow {
@Bean
public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
return new UndertowEmbeddedServletContainerFactory();
}
}
/**
* Registers a {@link EmbeddedServletContainerCustomizerBeanPostProcessor}. Registered
* via {@link ImportBeanDefinitionRegistrar} for early registration.
*/
public static class BeanPostProcessorsRegistrar
implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
registerSyntheticBeanIfMissing(registry,
"embeddedServletContainerCustomizerBeanPostProcessor",
EmbeddedServletContainerCustomizerBeanPostProcessor.class);
registerSyntheticBeanIfMissing(registry,
"errorPageRegistrarBeanPostProcessor",
ErrorPageRegistrarBeanPostProcessor.class);
}
private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry,
String name, Class<?> beanClass) {
if (ObjectUtils.isEmpty(
this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(name, beanDefinition);
}
}
}
}
这个类中包含了Tomcat,Jetty,Undertow 3 种类型的服务器自动注册逻辑,而选择条件则是通过@ConditionalOnClass注解控制,我们之前讲过ConditionalOnProperty注解的实现逻辑,而@ConditionalOnClass实现逻辑与之相似,对应的classpath目录下存在时,才会去解析对应的配置文件,这也就解释之所以Spring默认会启动Tomcat下是由于在启动的类目录下存在Servlet.class,tomcat.class,而这个依赖是由Spring自己在spring-boot-starter-web中默认引入 ,所示
按照代码的逻辑,如果我们默认的服务器不希望使用Tomcat而是希望使用Jettry,那么我们只需要将Tomcat对应的jar从Spring-boot-starter-web中排除掉,然后将jetty依赖即可。
public EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null ? this.baseDirectory : createTempDir("tomcat")); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } //异步启动tomcat
prepareContext(tomcat.getHost(), initializers);
return getTomcatEmbeddedServletContainer(tomcat);
本文的github地址
https://github.com/quyixiao/spring-boot-study
参考文章
修改SpringBoot项目的启动banner(超个性)
Spring Boot启动命令参数详解及源码分析
Spring命令行参数封装CommandLineArgs