除了直接通过浏览器从 Spring 官网下载框架代码以及通过 Idea 创建外,还可以通过 Linux 下的命令行 Web 客户端下载,优点是可以结合 Bash 命令或脚本实现一些自动化功能,此外也可以在 Idea 无法创建项目框架时作为一种替代方案。
curl -G https://start.spring.io/starter.tgz -d dependencies=web,lombok -d type=maven-project -d java-version=21 -d baseDir=my-test -d packaging=jar | tar -xzvf -
可以通过虚拟机挂载 Windows 工作目录的方式,在虚拟机中直接通过命令将框架代码下载解压到 WIndows 的工作目录。
war 项目
创建一个 war 包 Spring Boot 项目:
添加 JSP:
war 包项目的 JSP 固定添加在src/main/webapp
目录。
hello.jsp
:
<%page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
添加访问这个 JSP 的控制器类:
public class HelloController {
"/hello")
( public String hello(){
return "hello";
}
}
就像前文说的,这里控制器方法hello
返回 String 类型,实际上会被返回值处理器当做 View 名称进行处理,会查找一个 JSP 当做视图返回给浏览器。
需要在配置类中指定在哪个目录下查找 JSP 文件:
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
这里的spring.mvc.view.prefix
指webapp
目录,因此这里使用/
。这样在查找视图的时候,就可以根据视图名(hello
)查找到视图文件(/hello.jsp
)。
通过外部 Tomcat 启动
我们知道 war 包是要在 Servlet 服务器上运行的,在 Idea 中,可以通过配置启动项将项目托管给外部 Tomcat 服务器运行:
添加一个运行配置,选择Tomcat 服务器> 本地
:
配置内容如下:
应用程序服务器要指定一个本地安装的 Tomcat 目录。
部署分为两种方式,这里的war exploded
指将项目目录挂载到 Tomcat 中运行,省去了打包环节。另一种就是打包成 war 包后放在 Tomcat 中运行。
需要注意的是下面的应用程序上下文,这里指将目录挂载到 Tomcat 的哪个路径下,通常设置为/
,这样访问localhost:8081/
就能对应到项目请求,否则如果设置为/war_exploded
,就需要访问localhost:8081/war_exploded/
。
启动配置好的 Tomcat 启动项:
这样就说明启动成功。
在 Spring 的 war 项目框架代码中,包含一个:
这个类的用途是让外部的 Tomcat 服务器能找到并执行 Spring 框架的入口类。
通过内部 Tomcat 启动
除了通过外部 Tomcat 启动,Spring war 项目也可以通过入口类的 main 方法启动:
此时使用的是 Spring 自带的 Tomcat 运行项目。
需要注意的是,在这种情况下通过页面访问 JSP 文件,比如http://localhost:8080/hello
,浏览器并不会加载和渲染返回的 JSP 页面,而是将其作为一个文件下载。这是因为 Spring 内置的 Tomcat 服务器缺少 JSP 解析器。
通过依赖添加:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>11.0.7</version>
</dependency>
现在访问就能看到 JSP 页面被正常加载。
如果依然下载文件,是因为浏览器缓存的问题,可以使用开发者工具禁用缓存后加载页面。
Spring 应用启动过程
Spring 应用实际启动代码为:
分为两部分:
-
构造器
new SpringApplication(...)
,执行一些准备工作 -
SpringApplication
对象的run
方法,创建并初始化容器
构造器
加载 Bean 定义来源
构造器需要准备好创建 Bean 定义的来源,对于通常基于注解的 Spring Boot 项目来说,主要来源是入口类:
用以下测试用例模拟类定义来源加载:
SpringApplication application = new SpringApplication(Config.class);
// 创建容器,打印 bean 定义
ConfigurableApplicationContext context = application.run();
String[] beanDefinitionNames = context.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println(beanDefinitionName);
}
run
方法调用后容器中才会生成 bean 定义。
当做入口类的配置类:
public static class Config{
public User user(){
return new User("Tom", 20);
}
}
运行代码会报错:
因为应用被当做 Spring web 应用启动,缺少 ServletWebServerFactory。
添加:
public ServletWebServerFactory servletWebServerFactory(){
return new TomcatServletWebServerFactory();
}
可以在 SpringApplication 在构造阶段为其添加其他 bean 定义来源:
SpringApplication application = new SpringApplication(Config.class);
application.setSources(Set.of("classpath:applicationContext.xml"));
applicationContext.xml
:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="student" class="cn.icexmoon.demo1.entity.Student"/>
</beans>
可以获取 bean 定义的来源信息:
String resourceDescription = context.getBeanFactory().getBeanDefinition(beanDefinitionName).getResourceDescription();
System.out.printf("{bean 名称:%s,来源:%s%n}", beanDefinitionName, resourceDescription);
输出:
Spring 框架内置的 Bean 定义来源为 null。
判断容器类型
构造器的另一个工作是判断应用类型,即判断当前应用是一个非 Web 应用、Reactive Web 应用还是 Spring Web 应用,以便在之后run
方法调用时创建对应类型的容器。
判断依据是类路径中是否存或不存在某个关键的类:
比如如果存在org.springframework.web.reactive.DispatcherHandler
且不存在org.springframework.web.servlet.DispatcherServlet
,就认为这是一个 Reactive Web 应用。
在测试用例中调用deduceFromClasspath
方法获取应用类型:
// 判断应用类型
Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
WebApplicationType applicationType = (WebApplicationType)deduceFromClasspath.invoke(null);
System.out.println(applicationType);
deduceFromClasspath
方法是受保护的,所以要通过反射调用。
结果是SERVLET
,表示这是一个 Spring Web 应用。
初始化器
在调用run
方法创建容器后,执行容器的refresh
方法前,会执行容器的初始化器以对容器做一些增强(通常是追加 Bean 定义)。因此,SpringApplication 的构造器中会准备一些容器的初始化器(ApplicationContextInitializer):
通过测试用例模拟为SpringApplication
添加容器的初始化器:
// 添加初始化器
application.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
public void initialize(ConfigurableApplicationContext applicationContext) {
if (applicationContext instanceof GenericApplicationContext genericApplicationContext) {
genericApplicationContext.registerBean("teacher", Teacher.class);;
}
}
});
这里通过初始化器,为容器添加了一个 Bean 定义teacher
。
示例中对
ConfigurableApplicationContext
向下转型是为了能调用registerBean
方法。
事件监听器
在 run
方法调用时,会产生一些事件,因此 SpringApplication 构造器中会添加相应的事件监听器,对这些事件进行监听和处理:
添加事件监听器:
// 添加监听器
application.addListeners(new ApplicationListener<ApplicationEvent>() {
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("产生事件:"+event.getClass());
}
});
输出:
产生事件:class org.springframework.boot.context.event.ApplicationStartingEvent 产生事件:class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent 产生事件:class org.springframework.boot.context.event.ApplicationContextInitializedEvent 产生事件:class org.springframework.boot.context.event.ApplicationPreparedEvent 产生事件:class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent // ...
获取主类
SpringApplication 的构造器最后还会记录主类(Main Class):
用测试用例模拟调用:
SpringApplication application = new SpringApplication(Config.class);
Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass");
deduceMainApplicationClass.setAccessible(true);
Class mainClass = (Class)deduceMainApplicationClass.invoke(application);
System.out.println(mainClass);
需要反射调用,该方法是私有的。
输出:
class com.intellij.rt.junit.JUnitStarter
因为是测试用例,所以主类(入口类)是 JUnit 的启动类。
run 方法
事件发布
SpringApplication 对象的 run 方法执行后,会在不同阶段发布事件,执行事件发布的是SpringApplicationRunListeners
,这是一个组合类(使用了复合模式),包含多个SpringApplicationRunListener
:
SpringApplicationRunListener
是一个接口,有多个实现:
Spring 在配置文件spring.factories
中配置了 run 方法执行时使用的SpringApplicationRunListener
实现类:
使用工具类方法SpringFactoriesLoader.loadFactoryNames
可以读取配置文件中的实现类列表:
List<String> factoryNames = SpringFactoriesLoader.loadFactoryNames(SpringApplicationRunListener.class, springApplication.getClassLoader());
然后就可以通过反射调用的方式创建消息发布对象:
Constructor<?> constructor = Class.forName(factoryName).getDeclaredConstructor(SpringApplication.class, String[].class);
constructor.setAccessible(true);
SpringApplicationRunListener runListener = (SpringApplicationRunListener)constructor.newInstance(springApplication, new String[0]);
模拟 run 方法启动后事件发布过程:
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
// 开始执行 run 方法
runListener.starting(bootstrapContext);
// 准备环境信息
runListener.environmentPrepared(bootstrapContext, new StandardEnvironment());
// 创建容器,调用容器初始化器对容器初始化
GenericApplicationContext context = new GenericApplicationContext();
runListener.contextPrepared(context);
// 加载 bean 定义
runListener.contextLoaded(context);
// 刷新容器,创建单例 bean(如果需要),执行 bean 工厂的后处理器
context.refresh();
runListener.started(context, null);
// 执行 CommandLineRunner 和 ApplicationRunner
runListener.ready(context, null);
// RUN 方法执行过程中如果有异常产生
runListener.failed(context, new RuntimeException("test"));
这些事件发布方法与实际产生的事件对象的对应关系:
准备环境信息
容器创建需要提供环境信息,这些环境信息的来源是多种多样的:
ApplicationEnvironment env = new ApplicationEnvironment();
for (PropertySource<?> propertySource : env.getPropertySources()) {
System.out.println(propertySource);
}
ApplicationEnvironment
默认的来源有:
PropertiesPropertySource {name='systemProperties'} SystemEnvironmentPropertySource {name='systemEnvironment'}
其中systemEnvironment
指系统环境变量,比如:
System.out.println(env.getProperty("JAVA_HOME"));
来源的排序是有意义的,在这里systemProperties
比systemEnvironment
的优先级高。
可以添加.properties
或.yaml
配置文件作为源:
env.getPropertySources().addLast(new ResourcePropertySource(new ClassPathResource("application.properties")));
通过
addLast
添加的源优先级是最低的,通过配置文件设置的属性的优先级最低。
这样就可以从配置文件获取属性值:
System.out.println(env.getProperty("spring.application.name"));
还可以将命令行参数作为源加入环境信息:
String[] args = new String[]{
"--spring.profiles.active=dev",
};
env.getPropertySources().addFirst(new SimpleCommandLinePropertySource(args));
显然,命令行参数的优先级是最高的,通过命令行参数修改应用的spring.profiles.active
属性相当常见。
这里是测试用例,用本地变量模拟命令行参数。
获取属性:
System.out.println(env.getProperty("spring.profiles.active"));
特殊属性源
假设配置文件中存在这么几个属性:
user.person-one=Jack
user.person_two=Tom
user.personThree=Bruce
单词拼接形式分别是连字符、下划线和驼峰形式。获取属性值的时候统一使用连字符:
System.out.println(env.getProperty("user.person-one"));
System.out.println(env.getProperty("user.person-two"));
System.out.println(env.getProperty("user.person-three"));
输出:
Jack null null
只有严格匹配的属性正常获取到值。实际上 Spring 会为环境添加一个特殊的源,以提供宽松的属性名称匹配功能:
ConfigurationPropertySources.attach(env);
该方法会将一个特殊的源加入到第一位源:
后处理器
Spring 并不需要一个个添加属性源,而是通过一些列环境后处理器(Enviroment Post Processor)进行添加:
环境后处理器ConfigDataEnvironmentPostProcessor
可以扫描类路径下的配置文件,将其添加为源:
ConfigDataEnvironmentPostProcessor configDataEnvironmentPostProcessor = new ConfigDataEnvironmentPostProcessor(new DeferredLogs(), new DefaultBootstrapContext());
printPropertySources(env, "执行前");
configDataEnvironmentPostProcessor.postProcessEnvironment(env, springApplication);
printPropertySources(env, "执行后");
结果:
环境后处理器可以添加一个特殊的随机(Random)源:
RandomValuePropertySourceEnvironmentPostProcessor postProcessor = new RandomValuePropertySourceEnvironmentPostProcessor(new DeferredLogs());
printPropertySources(env, "执行前");
postProcessor.postProcessEnvironment(env, springApplication);
printPropertySources(env, "执行后");
可以利用这个源通过属性获取的方式产生随机数:
System.out.println(env.getProperty("random.int"));
System.out.println(env.getProperty("random.int"));
System.out.println(env.getProperty("random.int"));
输出:
-533867907 404823549 1757535839
甚至是获取 UUID:
System.out.println(env.getProperty("random.uuid"));
System.out.println(env.getProperty("random.uuid"));
System.out.println(env.getProperty("random.uuid"));
输出:
0ee15ff9-fb64-4cf4-b3d2-b31ccaa5d7c1
ece1424b-798a-4df2-88f0-82fbbb8f2a2c
cd26bf05-cfec-41c5-b970-308767fced11
事件驱动
Spring 会使用哪些环境后处理器的实现取决于配置文件spring.factories
:
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.ReactorEnvironmentPostProcessor
从这个文件读取以及创建和执行环境后处理器的时机由事件进行触发和驱动。
SpringApplication springApplication = new SpringApplication();
// 添加环境后处理器的监听器
springApplication.addListeners(new EnvironmentPostProcessorApplicationListener());
ApplicationEnvironment env = new ApplicationEnvironment();
SpringApplicationRunListener publisher = getPublisher(springApplication);
// 发布事件
printPropertySources(env,"事件发布前");
publisher.environmentPrepared(new DefaultBootstrapContext(), env);
printPropertySources(env,"事件发布后");
输出:
environmentPrepared
事件发布后,多出了两个属性源。
属性绑定
环境信息准备好后,需要将 SpringApplication 需要从环境中读取的属性进行绑定。
Binder
配置文件:
user.first-name=Tom
user.middle_name=Jerry
user.lastName=Bruce
创建环境信息并添加配置文件为属性源:
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource("user", new ClassPathResource("user.properties")));
实现数据绑定:
User user = Binder.get(env).bind("user", User.class).get();
System.out.println(user);
输出:
EnvironmentTests.User(firstName=Tom, middleName=Jerry, lastName=Bruce)
Binder 对属性名的匹配也很宽松。
Binder 除了可以利用环境信息中的属性创建一个新的对象外,也可以对现有对象实现数据绑定(属性改写):
User user = new User();
Binder.get(env).bind("user", Bindable.ofInstance(user));
需要使用Bindable.ofInstance
包装被绑定对象。
SpringApplication 属性绑定
属性配置文件:
spring.main.banner-mode=off
spring.main.lazy-initialization=true
加载并进行绑定:
SpringApplication springApplication = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource("app", new ClassPathResource("app.properties")));
System.out.println(springApplication);
Binder.get(env).bind("spring.main", Bindable.ofInstance(springApplication));
System.out.println(springApplication);
可以在 debug 模式下观察 SpringApplication 对象的属性变化:
创建容器
之前说了,在 SpringApplication 的构造器中需要判断容器类型,在 run 方法中,可以根据容器类型创建相应的容器:
switch (applicationType){
case NONE -> context = new AnnotationConfigApplicationContext();
case SERVLET -> context = new AnnotationConfigServletWebApplicationContext();
case REACTIVE -> context = new AnnotationConfigReactiveWebApplicationContext();
}
执行容器的初始化器
容器在创建后,需要执行初始化器,这些初始化器是在 SpringApplication 的构造器中添加的:
// 模拟 run 方法中调用初始化器
for (ApplicationContextInitializer initializer : springApplication.getInitializers()) {
initializer.initialize(context);
}
添加 Bean 定义
容器在初始化好后,会从资源中添加 Bean 定义,同样的,这些 Bean 定义来源在构造器中指定。
添加 Bean 定义分为三种方式:
-
以注解的方式从配置类添加
-
从 XML 文件添加
-
通过包扫描添加
AnnotationConfigServletWebApplicationContext context = new AnnotationConfigServletWebApplicationContext();
// 从配置类添加
DefaultListableBeanFactory defaultListableBeanFactory = context.getDefaultListableBeanFactory();
AnnotatedBeanDefinitionReader configReader = new AnnotatedBeanDefinitionReader(defaultListableBeanFactory);
configReader.register(Config.class);
// 从 XML 文件添加
XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
xmlReader.loadBeanDefinitions("classpath:applicationContext.xml");
// 通过包扫描添加
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(defaultListableBeanFactory);
scanner.scan("cn.icexmoon.demo1.entity");
context.refresh();
String[] beanDefinitionNames = context.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
log.debug("beanDefinitionName: {}", beanDefinitionName);
}
完整示例见。
Runner
当容器初始化完毕后(执行过refresh
方法),会执行 CommandLineRunner
和 ApplicationRunner
,通常我们会利用实现了这两个接口的 Bean 添加需要在应用启动后执行的代码。
比如:
static class WebConfig{
public CommandLineRunner commandLineRunner(){
return new CommandLineRunner() {
public void run(String... args) throws Exception {
System.out.println("CommandLineRunner is called.");
}
};
}
public ApplicationRunner applicationRunner(){
return new ApplicationRunner() {
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner is called.");
}
};
}
}
模拟 run 方法中执行 runner:
String[] args = new String[0];
Map<String, CommandLineRunner> commandLineRunnerMap = context.getBeansOfType(CommandLineRunner.class);
for (CommandLineRunner commandLineRunner : commandLineRunnerMap.values()) {
commandLineRunner.run(args);
}
Map<String, ApplicationRunner> applicationRunnerMap = context.getBeansOfType(ApplicationRunner.class);
for (ApplicationRunner applicationRunner : applicationRunnerMap.values()) {
applicationRunner.run(new DefaultApplicationArguments(args));
}
CommandLineRunner
与ApplicationRunner
的区别在于它们接收的参数不同,前者接收的是String[]
类型的参数,也就是入口类的 main
方法接收的命令行参数的原始类型。后者对命令行参数进行了封装,接收ApplicationArguments
类型的命令行参数。
如果 runer 中的代码需要使用命令行参数,推荐使用ApplicationRunner
,因为ApplicationArguments
提供一些更方便处理命令行参数的 API。
假设命令行参数是:
String[] args = new String[]{
"--spring.profiles.active=dev",
"--spring.main.web-application-type=servlet",
"test"
};
ApplicationRunner:
public ApplicationRunner applicationRunner() {
return new ApplicationRunner() {
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner is called.");
Set<String> optionNames = args.getOptionNames();
for (String optionName : optionNames) {
List<String> optionValues = args.getOptionValues(optionName);
System.out.println("optionName: " + optionName + ", optionValues: " + optionValues);
}
List<String> nonOptionArgs = args.getNonOptionArgs();
System.out.println("nonOptionArgs: " + nonOptionArgs);
}
};
}
带双连字符--
的参数被称作可选参数(Option Argument),通过getOptionNames
方法可以获取命令行参数中的可选参数名称列表,getOptionValues
方法可以获取其参数值(可能有多个)。不带连字符的参数被称作非可选参数(Non Option Argument),这些参数只有名称,没有值。使用getNonOptionArgs
方法可以获取这些参数。
输出:
optionName: spring.main.web-application-type, optionValues: [servlet] optionName: spring.profiles.active, optionValues: [dev] nonOptionArgs: [test]
Banner
打印 Banner 到标准输出:
SpringApplicationBannerPrinter printer = new SpringApplicationBannerPrinter(
new DefaultResourceLoader(), new SpringBootBanner());
ApplicationEnvironment environment = new ApplicationEnvironment();
printer.print(environment, BannerTests.class, System.out);
可以通过环境属性spring.banner.location
,让 Spring 使用指定的文本文件 banner:
environment.getPropertySources().addLast(new MapPropertySource("custom", Map.of("spring.banner.location", "banner1.txt")));
printer.print(environment, BannerTests.class, System.out);
新版本 Spring Boot 已不再支持图片作为 Banner,详情请阅读。
默认 Banner 中有一个 Spring Boot 版本信息,可以通过以下方式获取:
String version = SpringBootVersion.getVersion();
总结
run 方法的 Spring 源码执行过程分析可以看这个。
本文的完整示例代码可以从获取。
文章评论