最近在项目中使用 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 项目的时候要注意一点,入口类必须放在某个包下面,而不能放在默认包(也就是说不能直接放在 src/main/java 目录下),否则会导致项目启动失败(其实原因很简单,因为 Spring Boot 通过 @ComponentScan 来扫描 Bean,如果入口类放在默认包下,也就意味着 Spring Boot 要扫描所有 jar 包中的所有的类):
** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package.
至此,我们就写好了一个 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>
第二个文件是控制器类,粗看上去就是一个普通的类,外加上一个方法,只不过加上了两个注解 @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 的 全称叫做 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-INF/MANIFEST.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-INF/MANIFEST.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 项目只有三个非常简单的文件,但是想彻底弄懂每个文件,绝对不是那么容易。