最简单的一个 Spring Boot 项目
最近在项目中使用 Spring Boot,对它的简单易用印象很深刻。Spring Boot 最大的特点是它大大简化了传统 Spring 项目的配置,使用 Spring Boot 开发 Web 项目,几乎没有任何的 xml 配置。而且它最方便的地方在于它内嵌了 Servlet 容器(可以自己选择 Tomcat、Jetty 或者 Undertow),这样我们就不需要以 war 包来部署项目,直接使用 java -jar hello.jar
就可以运行一个 Web 项目。
我们以 Maven 项目为例,Spring Boot 除了支持 Maven,还支持 Gradle 项目。一个最简单的 Spring Boot Web 项目只有 3 个文件(其实如果想要更简单一点,入口和控制器类甚至可以写在同一个文件中)。首先是一个入口文件:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后再写一个控制器类:
@RestController
public class HelloController {
@RequestMapping("/")
public String index() {
return "Hello World!";
}
}
最后是这个项目的 POM(Project Object Model,项目对象模型) 文件:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.stonie</groupId>
<artifactId>spring-boot-sample</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在新建 Spring Boot 项目的时候要注意一点,入口类必须放在某个包下面,而不能放在默认包(也就是说不能直接放在 srcmainjava 目录下),否则会导致项目启动失败(其实原因很简单,因为 Spring Boot 通过 @ComponentScan 来扫描 Bean,如果入口类放在默认包下,也就意味着 Spring Boot 要扫描所有 jar 包中的所有的类):
** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package.
至此,我们就写好了一个 Spring Boot 项目,完整的源码可以参考 这里。从代码上看项目非常简单,但是这里有很多值得我们学习的地方。
一、从 SpringBootApplication 注解看 Spring Boot 自动配置原理
Spring Boot 项目通常都有一个入口类,入口类中的 main
方法和标准的 Java 应用入口方法是一样的,在上面的例子中,这个 main
方法中只有一行代码:SpringApplication.run()
,这是一个静态方法,用于启动整个 Spring Boot 项目。和其他 Java 程序不一样的是,入口类上多了一个 @SpringBootApplication
注解,这是非常重要的一个注解,它由多个注解组合而成,包括了:@SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
和其他一些注解。Spring Boot 是如何做到不需要任何配置文件的,看名字也可以猜出来,其秘密就在于 @EnableAutoConfiguration
这个注解实现了自动配置。这个注解的实现如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
其中最为重要的一行代码为:@Import({AutoConfigurationImportSelector.class})
,其中 @Import
是 Spring 提供的一个注解,可以导入配置类或者 Bean 到当前类中。AutoConfigurationImportSelector
类的实现比较复杂,简单来说就是扫描所有 jar 包中的 META-INF/spring-factories
文件,这个文件中声明了有哪些自动配置。我们可以打开 spring-boot-autoconfigure.jar 文件,这里就有这个文件,其中定义了一个属性 org.springframework.boot.autoconfigure.EnableAutoConfiguration
如下所示(有删减):
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
...
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
...
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration
可以看到 Spring Boot 已经内置了大量的自动配置,我们查看我们这个项目的依赖关系,如下图:
我们这个项目中使用了 spring-boot-starter-web
,可以看出它依赖于 spring-boot-starter-tomcat 和 spring-webmvc,所以这里会自动对 Tomcat 和 Spring MVC 进行配置。但是这里有一个问题,这里列出来的自动配置有那么多,难道 Spring Boot 都要一个个的去加载配置吗?当然不是,Spring Boot 也没那么傻,所以这里就要重点介绍一下从 Spring 4.x 开始引入的一个新特性:@Conditional
(也叫 条件注解)。
@Conditional
可以根据条件来创建 Bean,譬如随便拿上面一个自动配置类 RedisAutoConfiguration
来看,其中用到的条件注解为 @ConditionalOnClass({RedisOperations.class})
说明只有在 RedisOperations
类存在时才会自动配置,而我们这个项目并没有引入 redis,所以并不会加载 redis 的配置。
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
}
那么我们的程序在启动的时候都自动加载了哪些配置呢?我们可以通过命令行参数 --debug
来启动 Spring Boot 应用:
$ java -jar hello.jar --debug
启动时控制台会打印出详情的信息,类似于下面这样(实际打印的日志会非常多,有兴趣的同学可以自行挖掘):
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
EmbeddedWebServerFactoryCustomizerAutoConfiguration.TomcatWebServerFactoryCustomizerConfiguration matched:
- @ConditionalOnClass found required classes 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
ServletWebServerFactoryAutoConfiguration matched:
- @ConditionalOnClass found required class 'javax.servlet.ServletRequest'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- found ConfigurableWebEnvironment (OnWebApplicationCondition)
ServletWebServerFactoryAutoConfiguration#tomcatServletWebServerFactoryCustomizer matched:
- @ConditionalOnClass found required class 'org.apache.catalina.startup.Tomcat'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
ServletWebServerFactoryConfiguration.EmbeddedTomcat matched:
- @ConditionalOnClass found required classes 'javax.servlet.Servlet', 'org.apache.catalina.startup.Tomcat', 'org.apache.coyote.UpgradeProtocol'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.server.ServletWebServerFactory; SearchStrategy: current) did not find any beans (OnBeanCondition)
WebMvcAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet', 'org.springframework.web.servlet.config.annotation.WebMvcConfigurer'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- found ConfigurableWebEnvironment (OnWebApplicationCondition)
- @ConditionalOnMissingBean (types: org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; SearchStrategy: all) did not find any beans (OnBeanCondition)
Negative matches:
-----------------
ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)
我从日志中挑选中我们这里比较感兴趣的 EmbeddedWebServerFactoryCustomizerAutoConfiguration
,我们看看它的实现:
从这里就可以看出 Spring Boot 支持三种嵌入的 Web Server:Undertow、Jetty 和 Tomcat。根据上面的依赖关系 spring-boot-starter-web
默认是加载 spring-boot-starter-tomcat
的,所以这里会自动加载 Tomcat 的配置。
如果我们想改变默认的 Web Server,譬如改成轻量级的 Undertow,可以在 POM 文件中使用 exclusion 移除对 spring-boot-starter-tomcat
的引用,并加上对 spring-boot-starter-undertow
的引用,如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
</dependencies>
二、探究 Spring MVC 如何映射请求?
第二个文件是控制器类,粗看上去就是一个普通的类,外加上一个方法,只不过加上了两个注解 @RestController
和 @RequestMapping("/")
这个方法竟然就可以处理 Web 请求了。是不是觉得这有点神奇?为什么在浏览器里访问 http://localhost:8080
时页面会显示出这里返回的 Hello World!
?
其实,这一切都是 Spring MVC 的功劳。只不过在 Spring Boot 项目里,Spring MVC 的配置被简化了。我们先回忆一下在传统的 Spring MVC 里如何实现一个控制器类,首先,我们要先在 web.xml 里定义 DispatcherServlet
,并为这个 Servlet 配置相应的 servlet-mapping
,类似于下面这样:
<web-app>
<display-name>appName</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:/applicationContext.xml
</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
可以说 DispatcherServlet
是 Spring MVC 的核心,通过上面这个配置,它就可以截获 Web 应用的所有请求并将其分派给相应的处理器进行处理。在 Servlet 3.0 之后,还可以通过编程的方式来配置 Servlet 容器,Spring MVC 提供了一个接口 WebApplicationInitializer
,通过实现这个接口也可以达到 web.xml 配置文件的目的,如下所示:
public class AppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(appContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
那么 DispatcherServlet
是如何把 HTTP 请求映射到控制器的某个方法的呢?感兴趣的可以看看 DispatcherServlet
的源码,其实在 DispatcherServlet
初始化的时候,会扫描当前容器所有的 Bean,将包含 @Controller
和 @RequestMapping
注解的类和方法,映射到 HandleMappering
,为了实现这一点,Spring MVC 一般都有一个 dispatch-servlet.xml 配置文件:
<beans>
<context:component-scan base-package="com.stonie.hello" />
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />
</beans>
其中,component-scan
用于开启注解扫描,RequestMappingHandlerMapping
叫做 处理器映射,RequestMappingHandlerAdapter
叫做 处理器适配器(在老版本的 Spring MVC 中你可能看到的是 DefaultAnnotationHandlerMapping
和 AnnotationMethodHandlerAdapter
)。这两个类负责将 HTTP 方法,HTTP 路径,HTTP 参数匹配到具体的 @RequestMapping
注解的类和方法上。
@RequestMapping
注解的方法所支持的参数类型和返回类型非常丰富和灵活,从这里也可以看出 Spring MVC 的强大之处。这样虽然让开发人员可以根据需要任意选择,但是也会给开发人员带来困惑,可以参考这篇博客的总结:Spring MVC @RequestMapping 方法所支持的参数类型和返回类型详解。
上面就是 Spring MVC 实现请求映射的原理,在传统的 Spring MVC 项目中,这样的配置文件是很常见的,但是在 Spring Boot 项目中,这些配置都自动实现了,可以再深入研究下 DispatcherServletAutoConfiguration
和 WebMvcAutoConfiguration
这两个类。
三、解读 POM 文件
POM 的 全称叫做 Project Object Model,翻译过来就是项目对象模型,它用来定义项目的基本信息,构建步骤,依赖信息等等。pom.xml 文件作为 Maven 项目的核心,和 Make 的 Makefile、Ant 的 build.xml 文件一样。
在这篇博客的最后,让我们来看看这个项目的 pom.xml 文件。首先我们定义了三个元素:groupId
、artifactId
和 version
,这被称为 Maven 坐标,Maven 坐标保证了每个项目都有一个唯一的坐标值,当我们需要在其他项目中引用这个项目时,通过坐标就可以很方便的定位到该项目。
然后下面定义了一个依赖 spring-boot-starter-web
,并声明这个 POM 继承自 spring-boot-starter-parent
,别小看这一句继承,里面可是另有乾坤。你可以打开 spring-boot-starter-parent
的 POM 文件,可以发现它又继承自 spring-boot-dependencies
。在 spring-boot-starter-parent
中定义了一堆的插件,这些插件让 Maven 也能构建 Spring Boot 项目,其中最重要的一个插件是 spring-boot-maven-plugin
,这就是我们项目后面要用到的插件。另外,在 spring-boot-dependencies
中定义了一堆的依赖,足足有 3000+ 行,我们前面介绍 Spring Boot 的自动配置原理时就说过,它定义了很多自动配置类,几乎能用到的依赖它都依赖了。
在 pom.xml 文件的 <build>
元素中定义了 spring-boot-maven-plugin
插件之后,就可以运行下面的命令和平常的 jar 包一样进行打包了:
$ mvn clean package
而如果在这里没有定义 <build>
元素,也可以通过下面的命令来打包:
$ mvn clean package spring-boot:repackage
如果不用这个命令,打出来的包里只有我们写的两个类文件,所有依赖的 jar 包都没有包含进去,这样的 jar 包是无法运行的。而 spring-boot:repackage
插件会在执行完 mvn package 之后再次进行打包为可执行的软件包,并且将 mvn package 打的原始的包命名为 *.jar.original。
我们可以打开 *.jar.original 里的 META-INFMANIFEST.MF 文件:
Manifest-Version: 1.0
Implementation-Title: spring-boot-sample
Implementation-Version: 1.0-SNAPSHOT
Built-By: aneasystone
Implementation-Vendor-Id: com.stonie
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_111
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/spring-boot-sample
然后再打开 *.jar 里的 META-INFMANIFEST.MF 文件:
Manifest-Version: 1.0
Implementation-Title: spring-boot-sample
Implementation-Version: 1.0-SNAPSHOT
Built-By: aneasystone
Implementation-Vendor-Id: com.stonie
Spring-Boot-Version: 2.0.2.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.stonie.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_111
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/spring-boot-sample
可以发现新打的包里多了五行代码:
Spring-Boot-Version: 2.0.2.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.stonie.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
并且我们可以在 BOOT-INF/lib/ 目录下找到项目依赖的所有 jar 包,说明 spring-boot-maven-plugin
插件已经自动帮我们把 jar 包转换成了一个可运行的 Spring Boot 应用。
要理解 Spring Boot 是如何通过 Maven 打包的,这里有两个非常重要的概念:生命周期 和 插件目标。Maven 定义了三套生命周期:clean、default 和 site,其中 clean 用于清理项目,default 用于执行构建项目需要的具体步骤,site 用于发布项目站点。其中 clean 和 default 是最常使用的。譬如我们平常执行 mvn clean compile
来清理并编译项目时就用到了 clean 和 default 生命周期,其中,mvn clean
调用的是 clean 生命周期的 clean 阶段,mvn compile
调用的是 default 生命周期的 compile 阶段。
通过 mvn 命令不仅可以直接调用生命周期的某个阶段,还可以调用某个插件目标,譬如上面的 mvn spring-boot:repackage
就是调用 spring-boot 插件的 repackage 目标。实际上,Maven 的核心就是插件,它是一款基于插件的框架,所有的工作其实都是交给插件完成的,包括上面说的 clean 和 compile 实际上就是通过 clean:clean 和 compiler:compile 这两个插件完成的。
不过上面的命令中还有一个问题,执行 mvn spring-boot:repackage
时,Maven 为什么可以根据 spring-boot
这个名字定位到 spring-boot-maven-plugin
这个插件的?这是因为 spring-boot
就是 spring-boot-maven-plugin
插件,这被称为 插件前缀,为了方便书写 mvn 命令,可以给每个插件都定义一个插件前缀,这样就不用在命令行中写那么长的插件名称了。
总结
越是看似简单的东西,背后越是蕴含着无限玄机,从平时的开发工作中,要善于从细节中发现问题。虽然这个 Spring Boot 项目只有三个非常简单的文件,但是想彻底弄懂每个文件,绝对不是那么容易。
参考
- Building an Application with Spring Boot
- Spring application does not start outside of a package
- Spring Boot 的自动配置
- Spring Boot 学习笔记 02 -- 深入了解自动配置
- Difference between spring @Controller and @RestController annotation
- Spring6:基于注解的 Spring MVC(上篇)
- Spring MVC @RequestMapping 方法所支持的参数类型和返回类型详解
- Spring Boot 的 Maven 插件 Spring Boot Maven plugin 详解
- Spring Boot Maven Plugin