3.SpringBoot学习——第一个Web应用
Posted Soulballad
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3.SpringBoot学习——第一个Web应用相关的知识,希望对你有一定的参考价值。
1.简介
1.1 概述
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run". We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.
Spring Boot 可以轻松创建单独的,基于生产级的 Spring 应用程序,您需要做的可能“仅仅是去运行”。 我们提供了 Spring Platform 对 Spring 框架和第三方库进行处理,尽可能的降低使用的复杂度。大多数情况下 Spring Boot 应用只需要非常少的配置。
1.2 特点
- Create stand-alone Spring applications(创建独立的 spring 应用)
- Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files) (集成 tomcat,jetty,undertow 等内置容器,无需打包成 war 包)
- Provide opinionated ‘starter‘ dependencies to simplify your build configuration (提供众多 starter 扩展来简化依赖配置)
- Automatically configure Spring and 3rd party libraries whenever possible(无论何时都可以自动装配 spring 和第三方依赖)
- Provide production-ready features such as metrics, health checks, and externalized configuration(提供生产环境特性,如 指标信息、健康检查和外部化配置等)
- Absolutely no code generation and no requirement for XML configuration (无需代码生成和 xml 配置)
1.3 对比 Spring
-
Spring 是一种生态,它包含各种组件,针对开发中存在的问题提供了多种解决方案;
-
Spring Boot 为快速启动且最小化配置的 Spring 应用而设计,并且具备用于构建生产级应用的各种特性,提供了一些内置 starter;
-
Spring 应用需要复杂配置,一般在需要在 xml 中配置各种依赖;Spring Boot 简化了这些配置,默认使用注解进行扫描,最多只需要在 application.properties 中提供额外配置;
-
使用 maven 构建 Spring 应用需要提供各种 pom 依赖;而 Spring Boot 只需要提供了 starter 即可,starter 中已经对所需依赖进行了封装;
-
Spring 应用最终需要打成 war 包放到 Severlet 容器中进行运行;而 Spring Boot 可以打成 jar 包,使用 java -jar 命令直接运行;
-
...
2.环境
- JDK 1.8.0_201
- Spring Boot 2.2.0.RELEASE
- 构建工具(apache maven 3.6.3)
- 开发工具(IntelliJ IDEA )
3.代码
3.1 代码结构
3.2 maven 依赖
<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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.3 java代码
DemoController.java
@RestController
public class DemoController {
@GetMapping("/demo")
public String demo() {
return "demo";
}
}
3.4 git 地址
spring-boot/spring-boot-01-demo
4.结果
启动 SpringBootDemoApplication.main 方法,访问如下地址,页面显示 “demo” 表示服务运行正常
### GET /demo
GET http://localhost:8080/demo
5.源码分析
5.1 Spring Boot 启动流程?
从 main 方法开始
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
SpringApplication.run -> ConfigurableApplicationContext
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 设置 “java.awt.headless” 属性
configureHeadlessProperty();
// 使用 SpringFactoryLoader 获取 SpringApplicationRunListener 实例的 listeners
SpringApplicationRunListeners listeners = getRunListeners(args);
// 逐个启动 SpringApplicationRunListener,应用开始启动事件
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 获取 Environment,根据 webType 获取不同类型;并配置 propertySource 和 profiles
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
// 打印 banner
Banner printedBanner = printBanner(environment);
// 创建 spring 应用上下文,类型和 webType 有关
context = createApplicationContext();
// 使用 SpringFactoryLoader 获取 SpringBootExceptionReporter 实例的 exceptionReporters
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 上下文预处理,spring boot
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新上下文,spring context
refreshContext(context);
// 上下文后置处理,暂为空
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// listeners 启动完成事件
listeners.started(context);
// 触发 ApplicationRunner 和 CommandLineRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 引用运行事件,开始监听
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
5.2 Spring Boot 如何加载 Tomcat?
SpringApplication.run -> ConfigurableApplicationContext ,跟踪 tomcat 的创建过程,主要看 createApplicationContext() 和 refreshContext() 方法
public ConfigurableApplicationContext run(String... args) {
// ...
try {
// ...
context = createApplicationContext();
// ...
refreshContext(context);
// ...
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
// ...
return context;
}
createApplicationContext() 创建 spring 应用
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
最终创建的应用类型和 webApplicationType 有关,webApplicationType 在 SpringApplication 的构造函数中进行实例化
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
默认类型为 WebApplicationType.SERVLET
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
在 refreshContext 中调用了 refresh(context) 方法,这里的 applicationContext 为 AnnotationConfigServletWebServerApplicationContext
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}
然后调用 AbstractApplicationContext.refresh() 方法
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
// ...
try {
// ...
this.onRefresh();
// ...
} catch (BeansException var9) {
// ...
} finally {
this.resetCommonCaches();
}
}
}
onRefresh() 是一个钩子方法,根据上面分析,这里使用的是 servlet,所以会调用到 ServletWebServerApplicationContext.onRefresh()
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
在 ServletWebServerApplicationContext 中调用 createWebServer() 创建 web 服务
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
这里的 factory 通过 ServletWebServerFactory 来实例化,所以创建 ServletWebServer
最终在 TomcatServletWebServerFactory 中创建了 Tomcat、Connector、Engine、Host 等,这里可以结合 apache-tomcat 的配置文件 server.xml 来分析几者之间的层级关系
5.3 Spring Boot 默认容器为何是 tomcat?
先看下 spring-boot-starter-web 的依赖结构
spring-boot-starter-web 依赖了 spring-boot-starter-tomcat,又依赖了 tomcat-embed-core。但是只凭这个,并不能说明默认容器为 tomca 的原因。
要弄清这个问题,就要涉及到 Spring Boot 的自动装配,以及 WebServer 的装配。
@SpringBootApplication 是一个复合注解,它包含如下内容
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
@ConfigurationPropertiesScan
public @interface SpringBootApplication {
// ...
}
其中 @EnableAutoConfiguration 和自动装配相关,@EnableAutoConfiguration 声明如下
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// ...
}
@Import 导入一个 AutoConfigurationImportSelector,这个 Selector 中 selectImports() 实现如下
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
其中 getAutoConfigurationEntry 获取自动装配类型,它的实现如下
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
// 是否开启自动装配,默认开启,可通过 spring.boot.enableautoconfiguration 进行配置
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取元注解中的属性,它是一个 LinkedHashMap
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 = filter(configurations, autoConfigurationMetadata);
// 触发自动装配导入事件
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
在 getCandidateConfigurations 获取所有自动装配类,这个方法通过 SpringFactoriesLoader 加载 META-INF/spring.factories 中的内容,在 spring-boot-autoconfigure 的 spring.factories 中有如下内容
其中有一条配置为 EmbeddedWebServerFactoryCustomizerAutoConfiguration,这个即为内容 webServer 的自动装配类
它的实现如下
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
/**
* Nested configuration if Tomcat is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
/**
* Nested configuration if Jetty is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Server.class, Loader.class, WebAppContext.class })
public static class JettyWebServerFactoryCustomizerConfiguration {
@Bean
public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new JettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
/**
* Nested configuration if Undertow is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Undertow.class, SslClientAuthMode.class })
public static class UndertowWebServerFactoryCustomizerConfiguration {
@Bean
public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new UndertowWebServerFactoryCustomizer(environment, serverProperties);
}
}
/**
* Nested configuration if Netty is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpServer.class)
public static class NettyWebServerFactoryCustomizerConfiguration {
@Bean
public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new NettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
}
结合上面 spring-boot-starter-web 中引入了 tomcat-embed-core 依赖,可以发现,默认装配的类型即为
TomcatWebServerFactoryCustomizerConfiguration
以上是关于3.SpringBoot学习——第一个Web应用的主要内容,如果未能解决你的问题,请参考以下文章
学习参考《Flask Web开发:基于Python的Web应用开发实战(第2版)》中文PDF+源代码
可以在我自己的设备上发布应用程序(iPad 第 1 代 5.1.1)