搭建第一个 Spring Boot web应用

作为一个老java, 说来惭愧,spring boot出来这么多年了,才第一次使用。 不知从何下手,先看看官方文档 说了些什么吧。

后面内容就是照搬, 没动脑,只是记录,免得以后还得面对abc,实在是累

环境

首先确认开发环境

1
2
3
4
5
6
7
8
9
10
11
$ java -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

$ mvn -v
Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T02:41:47+08:00)
Maven home: /Users/huazhizui/application/apache-maven-3.6.0
Java version: 1.8.0_201, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "mac os x", version: "10.15.4", arch: "x86_64", family: "mac"

创建一个目录,作为本示例的工作目录, 后面都操作都在这个目录中进行。

1
2
$ mkdir spring-boot-demo2
$ cd spring-boot-demo2

创建pom文件

作为一个maven项目, 那必须得有一个pom.xml文件

1
$ touch pom.xml

pom.xml 文件中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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>

<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>

<description/>
<developers>
<developer/>
</developers>
<licenses>
<license/>
</licenses>
<scm>
<url/>
</scm>
<url/>

<!-- Additional lines to be added here... -->

</project>

这个pom里面我们只设置了一个parent节点, spring-boot-starter-parent 是一个特殊的starter,它主要提供一些maven默认配置, 还有依赖管理器, 这样我们在引入依赖的时候, 就可以省略version标签了, 他们使用spring-boot-starter-parent dependency-management 里定义的版本。 但是它本身不会添加任何实际的依赖。我们查看一下maven依赖树,可以看到依赖是空的:

1
$ mvn dependency:tree

添加web starter

既然要做web应用, 那添加的第一个依赖必须是spring-boot-starter-web

1
2
3
4
5
6
7
8
9
10
11
<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">
<!-- 省略了其它配置 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- 省略了其它配置 -->
</project>

重新检查依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ mvn dependency:tree
[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT
[INFO] \- org.springframework.boot:spring-boot-starter-web:jar:2.3.0.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot-starter:jar:2.3.0.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot:jar:2.3.0.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.3.0.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-logging:jar:2.3.0.RELEASE:compile
[INFO] | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | +- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | | \- org.slf4j:slf4j-api:jar:1.7.30:compile
[INFO] | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.13.2:compile
[INFO] | | | \- org.apache.logging.log4j:log4j-api:jar:2.13.2:compile
[INFO] | | \- org.slf4j:jul-to-slf4j:jar:1.7.30:compile
[INFO] | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] | +- org.springframework:spring-core:jar:5.2.6.RELEASE:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.2.6.RELEASE:compile
[INFO] | \- org.yaml:snakeyaml:jar:1.26:compile
[INFO] +- org.springframework.boot:spring-boot-starter-json:jar:2.3.0.RELEASE:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.11.0:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.0:compile
[INFO] | | \- com.fasterxml.jackson.core:jackson-core:jar:2.11.0:compile
[INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.11.0:compile
[INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.11.0:compile
[INFO] | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.11.0:compile
[INFO] +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.3.0.RELEASE:compile
[INFO] | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.35:compile
[INFO] | +- org.glassfish:jakarta.el:jar:3.0.3:compile
[INFO] | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.35:compile
[INFO] +- org.springframework:spring-web:jar:5.2.6.RELEASE:compile
[INFO] | \- org.springframework:spring-beans:jar:5.2.6.RELEASE:compile
[INFO] \- org.springframework:spring-webmvc:jar:5.2.6.RELEASE:compile
[INFO] +- org.springframework:spring-aop:jar:5.2.6.RELEASE:compile
[INFO] +- org.springframework:spring-context:jar:5.2.6.RELEASE:compile
[INFO] \- org.springframework:spring-expression:jar:5.2.6.RELEASE:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 03:08 min
[INFO] Finished at: 2020-05-25T16:29:09+08:00
[INFO] ------------------------------------------------------------------------

可以看到spring-web spring-beans spring-webmvc 等spring web应用必须的jar包都添加进来了, 还有一个特殊的spring-boot-starter-tomcat, 后面会知道它有什么用。

至此, spring boot web 开发环境就搭建好了, 简直不能更方便了。 开始写业务代码吧

开始编写业务代码了

根据maven的尿性,代码要放在src/main/java 目录,我们建一个:

1
$ mkdir -p src/main/java

然后在此目录下建我们java文件src/main/java/Example.java

1
$ touch src/main/java/Example.java

现在项目目录结构如下:

1
2
3
4
5
6
7
$ tree .
.
├── pom.xml
└── src
└── main
└── java
└── Example.java

用最喜欢的编辑器编辑Example.java, 写入如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.web.bind.annotation.*;

@RestController
@EnableAutoConfiguration
public class Example {

@RequestMapping("/")
String home() {
return "Hello World!";
}

public static void main(String[] args) {
SpringApplication.run(Example.class, args);
}

}

运行项目

注意到有public static void main(String[] args)方法, 这是java程序执行入口,如果在IDE里面, 直接运行这个类就可以了。

由于我们使用了spring-boot-starter-parent, 所以可以直接在命令行运行我们的程序:

1
$ mvn spring-boot:run

输出内容大致是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[INFO] Attaching agents: []

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.0.RELEASE)

2020-05-25 17:21:42.595 INFO 4957 --- [ main] Example : Starting Example on huazhizuideMacBook-Pro.local with PID 4957 (/Users/huazhizui/code/java/spring-boot-demo2/target/classes started by huazhizui in /Users/huazhizui/code/java/spring-boot-demo2)
2020-05-25 17:21:42.596 INFO 4957 --- [ main] Example : No active profile set, falling back to default profiles: default
2020-05-25 17:21:43.191 INFO 4957 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-05-25 17:21:43.232 INFO 4957 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-05-25 17:21:43.232 INFO 4957 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.35]
2020-05-25 17:21:43.292 INFO 4957 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-05-25 17:21:43.292 INFO 4957 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 666 ms
2020-05-25 17:21:43.408 INFO 4957 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-05-25 17:21:43.529 INFO 4957 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-05-25 17:21:43.535 INFO 4957 --- [ main] Example : Started Example in 1.354 seconds (JVM running for 1.72)

可以看到启动了一个tomcat, 服务端口是8080 , 非常方便, 不用自己安装tomcat, 配置一大堆东西, just one command, 这就是spring-boot-starter-tomcat的作用。

浏览器访问一下http://localhost:8080/, 返回

1
Hello World!

分分钟, 一个简单的web项目就完成。

项目打包成可执行jar

项目依赖较多的情况下, 使用maven标准方法把项目打包成可执行jar, 问题一大堆, 被坑过的都懂。 以前使用maven UberJar插件, 经常一通配置可以解决。但spring boot 提供了更好的方案: spring-boot-maven-plugin。 pom文件添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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">
<!-- 省略了其它配置 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- 省略了其它配置 -->
</project>

然后运行mvn package , 整个项目和依赖就全部打到一个jar包里了

1
2
3
4
5
6
7
8
9
$ mvn package
...
[INFO] --- spring-boot-maven-plugin:2.3.0.RELEASE:repackage (repackage) @ myproject ---
[INFO] Replacing main artifact with repackaged archive
...
$ ll target
...
-rw-r--r-- 1 huazhizui staff 16M 5 25 18:23 myproject-0.0.1-SNAPSHOT.jar
-rw-r--r-- 1 huazhizui staff 2.2K 5 25 18:23 myproject-0.0.1-SNAPSHOT.jar.original

查看target目录,这个myproject-0.0.1-SNAPSHOT.jar文件大概16M, 就是我们要的可执行jar包了, myproject-0.0.1-SNAPSHOT.jar.original是我们原来单纯的jar包,只有2.2K, 只包含我们自己写的代码。

如果好奇这个巨大的16M jar包里空间都有些撒, 请使用:

1
$ jar tvf target/myproject-0.0.1-SNAPSHOT.jar

这个包含一切的jar包, 执行姿势如下:

1
$ java -jar target/myproject-0.0.1-SNAPSHOT.jar

特别提示: java -jar 模式下,程序运行的classpath只有这个jar包,就算指定了-classpath 选项,也没选用, 会直接被忽略。

运行效果和mvn spring-boot:run 一致。

总结

从搭建开始到最终页面展示“Hello World!” , 我们只写了10来行代码, 一个配置文件都没写过, 甚至连web.xml文件都没管过。这究竟是如何发生的?

看一下Example.java, 注意到三个注解@RestController@EnableAutoConfiguration@RequestMapping, 还有一个main方法, 这在普通的web项目里可以没有的。

@RestController、@RequestMapping注解

这两注解, 是SpringMvc里的注解,@RestController 表明这个类是一个Controller可以用来处理http请求, @RequestMapping表明了这个为和方法能处理哪些请求地址。

@EnableAutoConfiguration注解和自动配置

这个注解就厉害了, 这就是0配置的核心, 有个这个注解, spring boot 会根据我们依赖的jar包来推测出我们需要对spring做的配置。这样我们就不需要手动去写一大堆spring配置文件了。

我们这个项目中,由于我们依赖了spring-boot-starter-web,这个starter 添加了springMvctomcat依赖, 所以spring 自动配置功能就会假设这是个web项目, 并做出相应的配置, 包括tomcat的配置。

当然, 自动配置功能 并不是只能跟starter一起使用, 就算不用starter, 通过自己手动添加依赖, 自动配置也能正常工作。

main 方法

可以看出这是java标准的main方法, 是java程序的入口。但是我们都知道, 通常web项目是在web容器中运行, 所以并不需要main方法, 那这个main方法是干什么用的?

回到pom.xml文件, 发现里面并没有正常web项目都会有的<packaging>war</packaging> 标签, 这是标签是告诉maven我们的项目要打成war包。 既然这个项目没有, 那说明, 压根就不是在容器里运行的, 而是做为普通的java程序运行的。 我们程序运行起来也只干一件事, 调用SpringApplication.run, 告诉SpringApplication该干活了。

为了验证这个猜想, 我们可以看看通过mvn spring-boot:run启动项目的实际命令, 先看一看有哪些java 进程:

1
2
3
$ jps -mlv | grep -v "jps"
1111 org.codehaus.plexus.classworlds.launcher.Launcher spring-boot:run -Dclassworlds.conf=/Users/huazhizui/application/apache-maven-3.6.0/bin/m2.conf -Dmaven.home=/Users/huazhizui/application/apache-maven-3.6.0 -Dlibrary.jansi.path=/Users/huazhizui/application/apache-maven-3.6.0/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/Users/huazhizui/code/java/spring-boot-demo2
1149 Example -Xverify:none -XX:TieredStopAtLevel=1

可以看到两个进程11111149, 可以看到1111是maven进程, 它的参数是spring-boot:run1149 就是运行我们项目的进程,查看它的完整命令:

1
2
$ ps -ef | grep "1149" | grep -v "grep"
501 1149 1111 0 4:53下午 ttys001 0:03.06 /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/bin/java -Xverify:none -XX:TieredStopAtLevel=1 -cp /Users/huazhizui/code/java/spring-boot-demo2/target/classes:/Users/huazhizui/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.3.0.RELEASE/spring-boot-starter-web-2.3.0.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/boot/spring-boot-starter/2.3.0.RELEASE/spring-boot-starter-2.3.0.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/boot/spring-boot/2.3.0.RELEASE/spring-boot-2.3.0.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.3.0.RELEASE/spring-boot-autoconfigure-2.3.0.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.3.0.RELEASE/spring-boot-starter-logging-2.3.0.RELEASE.jar:/Users/huazhizui/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:/Users/huazhizui/.m2/repository/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:/Users/huazhizui/.m2/repository/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar:/Users/huazhizui/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.13.2/log4j-to-slf4j-2.13.2.jar:/Users/huazhizui/.m2/repository/org/apache/logging/log4j/log4j-api/2.13.2/log4j-api-2.13.2.jar:/Users/huazhizui/.m2/repository/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar:/Users/huazhizui/.m2/repository/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-core/5.2.6.RELEASE/spring-core-5.2.6.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-jcl/5.2.6.RELEASE/spring-jcl-5.2.6.RELEASE.jar:/Users/huazhizui/.m2/repository/org/yaml/snakeyaml/1.26/snakeyaml-1.26.jar:/Users/huazhizui/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.3.0.RELEASE/spring-boot-starter-json-2.3.0.RELEASE.jar:/Users/huazhizui/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.11.0/jackson-databind-2.11.0.jar:/Users/huazhizui/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.11.0/jackson-annotations-2.11.0.jar:/Users/huazhizui/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.11.0/jackson-core-2.11.0.jar:/Users/huazhizui/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.11.0/jackson-datatype-jdk8-2.11.0.jar:/Users/huazhizui/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.11.0/jackson-datatype-jsr310-2.11.0.jar:/Users/huazhizui/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.11.0/jackson-module-parameter-names-2.11.0.jar:/Users/huazhizui/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/2.3.0.RELEASE/spring-boot-starter-tomcat-2.3.0.RELEASE.jar:/Users/huazhizui/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.35/tomcat-embed-core-9.0.35.jar:/Users/huazhizui/.m2/repository/org/glassfish/jakarta.el/3.0.3/jakarta.el-3.0.3.jar:/Users/huazhizui/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.35/tomcat-embed-websocket-9.0.35.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-web/5.2.6.RELEASE/spring-web-5.2.6.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-beans/5.2.6.RELEASE/spring-beans-5.2.6.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-webmvc/5.2.6.RELEASE/spring-webmvc-5.2.6.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-aop/5.2.6.RELEASE/spring-aop-5.2.6.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-context/5.2.6.RELEASE/spring-context-5.2.6.RELEASE.jar:/Users/huazhizui/.m2/repository/org/springframework/spring-expression/5.2.6.RELEASE/spring-expression-5.2.6.RELEASE.jar Example

可以看到11491111 的子进程, 说明我们这个项目是通过maven启动的。整个命令非常长, 但大部分是设置classpath, 简化看看,就是这样一条命令

1
java -Xverify:none -XX:TieredStopAtLevel=1 -cp ... Example

得证,springboot 的运行方式,就是一个普通java 程序运行的方式

当然这种通过maven插件启动的方式, 只能在开发时用, 上线的话应该打成可运行jar包来运行

仔细观察下 -cp, 其实就是我们项目的/target/classes 目录和所有依赖jar 的组合, 很正常,很标准

既然我们项目只是个普通java程序, 那为什么能提供web服务? 这就是SpringApplication.run 干的事情了,我们main方法调用SpringApplication.run, 它先初始化好spring环境,根据依赖知道是个web项目而且引用了tomcat, 所以配置好tomcat, 然后启动tomcat,提供web服务功能。

至此,算是初识了spring boot