Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

工作中经常需要为新开展的业务创建新工程,如果每次都重新搭建、或者拷贝老项目,这些重复工作会影响开发效率,也不利于维护(例如为新工程统一引入新组件、或升级配置文件等)。Maven 提供了 archetype 骨架插件,用于抽取这些重复的配置和代码,以模板的方式创建新项目。

Maven Archetype Plugin(骨架插件)能够让用户从现有的模板(即骨架)中创建 Maven 项目,也能够从现有的项目中创建骨架。其流程如下:

Maven Archetype Plugin

从上图可见,该插件提供了如下目标(即命令):

  • archetype:generate creates a Maven project from an archetype: asks the user to choose an archetype from the archetype catalog, and retrieves it from the remote repository. Once retrieved, it is processed to create a working Maven project.
  • archetype:create-from-project creates an archetype from an existing project.(注意如果需要包含 yml 配置文件,需要加上参数 -Darchetype.filteredExtentions=yml
  • archetype:crawl search a repository for archetypes and updates a catalog.

下面具体演示如何使用。

创建 archetype 工程样例

方式一

利用 Maven 内置的 maven-archetype-archetype 构件创建一个骨架工程样例:

1
2
3
4
mvn archetype:generate
-DgroupId=[your project's group id]
-DartifactId=[your project's artifact id]
-DarchetypeArtifactId=maven-archetype-archetype

创建成功后,其目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
archetype
|-- pom.xml // archetype pom
`-- src
`-- main
`-- resources
|-- META-INF
| `-- maven
| `--archetype.xml // archetype descriptor
`-- archetype-resources // prototype files
|-- pom.xml // prototype pom
`-- src
|-- main
| `-- java
| `-- App.java
`-- test
`-- java
`-- AppTest.java

骨架由以下四个部分组成,各文件作用如下:

组成部分 组成部分 路径 描述
archetype pom 骨架的 POM 根目录下的 pom.xml
archetype descriptor 骨架描述符文件 src/main/resources/META-INF/maven/archetype.xml 这个文件列出了包含在 archetype 中的所有文件并将这些文件分类,因此 archetype 生成机制才能正确的处理。
prototype pom 新工程的原型 POM src/main/resources/archetype-resources/pom.xml archetype 插件会直接复制这个 pom.xml,然后替换其中的占位符 ${artifactId}${groupId}${version}
prototype files 新工程的原型文件 src/main/resources/archetype-resources/ archetype 插件会直接复制这些文件

方式二

利用公司现有模板项目创建骨架:

1
mvn archetype:create-from-project -Darchetype.filteredExtentions=yml,xml,java,jsp

创建成功后,其目录结构如下:

1
2
3
4
5
6
7
8
9
youproject
|-- pom.xml // 项目源文件
`-- src // 项目源文件
`-- main
`-- test
`-- target // 创建结果
`-- generated-sources
`-- archetype
// 目录结构同方式一。后续安装 archetype 到本地仓库时,需要 cd 到本目录,执行 mvn install;如果是发布到远程仓库,则 mvn deploy

-Darchetype.filteredExtentions 用于指定要过滤的文件后缀名,被过滤的文件将会替换文件里面用到的占位符。在生成的 archetype.xml 文件时,命令将会扫描模板项目中所有的文件类型,为上述指定的文件类型添加 filtered="true" 属性:

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
<fileSet filtered="true" packaged="true" encoding="UTF-8">
<directory>src/main/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</fileSet>
<fileSet filtered="true" encoding="UTF-8">
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
</fileSet>
<fileSet filtered="true" encoding="UTF-8">
<directory>src/main/webapp</directory>
<includes>
<include>**/*.jsp</include>
<include>**/*.xml</include>
</includes>
</fileSet>
<fileSet filtered="true" packaged="true" encoding="UTF-8">
<directory>src/test/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</fileSet>

配置骨架

配置描述符 archetype.xml

然后,配置 archetype.xml,详见:archetype descriptor

配置新工程的 pom.xml

使用占位符 ${artifactId}${groupId}${version},这些变量都将在 archetype:generate 命令运行时被初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<version>${version}</version>
<packaging>jar</packaging>

<name>A custom project</name>
<url>http://www.myorganization.org</url>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

配置新工程的相关文件

将新工程所需文件,全部拷贝到 src/main/resources/archetype-resources/ 目录下。

安装本地仓库

创建骨架并配置完毕,首先安装到本地仓库:

  • 如果是使用 Maven 内置的 maven-archetype-archetype 构件创建的骨架工程样例,直接在该目录下执行安装命令即可。
  • 如果是使用命令 mvn archetype:create-from-project 从现有的项目中创建骨架,需要先 cd 进入到 target/generated-sources/archetype/ 目录,再运行 mvn install
1
mvn install

执行如下插件 goal:

1
2
3
4
5
6
maven-resources-plugin:resources  // 拷贝资源文件
maven-resources-plugin:testResources // 拷贝测试资源文件
maven-archetype-plugin:jar // 在 target 目录下构建出 archetype jar
maven-archetype-plugin:integration-test
maven-install-plugin:install // 将构建出来的 jar 和 pom 安装到本地仓库
maven-archetype-plugin:update-local-catalog // 更新本地仓库根目录下的 archetype-catalog.xml

安装完毕,构建出来的 archetype jar artifactId-archetype-version.jar 将会安装到本地仓库。此时需要更新本地仓库根目录下的 archetype-catalog.xml ,插入一段骨架配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<archetype-catalog xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0 http://maven.apache.org/xsd/archetype-catalog-1.0.0.xsd"
xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<archetypes>
<!-- 新插入的骨架 -->
<archetype>
<groupId>[your project's group id]</groupId>
<artifactId>[your project's artifact id]</artifactId>
<version>xxx</version>
<description>xxx</description>
</archetype>
</archetypes>
</archetype-catalog>

可以使用命令:mvn archetype:crawl,将自动搜索仓库中的骨架并更新骨架配置。

发布到远程仓库

骨架生成成功,并且一切符合预期之后,可以发布到远程仓库供他人使用:

1
mvn deploy

创建新项目

命令行方式

尝试创建项目,选择想要使用的骨架,并为新工程指定 groupIdartifactId,以及包名 package

1
2
3
4
5
6
7
8
mvn archetype:generate                                  \
-DarchetypeGroupId=<archetype-groupId> \
-DarchetypeArtifactId=<archetype-artifactId> \
-DarchetypeVersion=<archetype-version> \
-DgroupId=<my.groupid> \
-DartifactId=<my-artifactId> \
-Dversion=<my.version> \
-Dpackage=my.package

输出日志:

1
2
3
4
5
6
7
8
9
10
[INFO] Using property: groupId = <my.groupid>
[INFO] Using property: artifactId = <my-artifactId>
[INFO] Using property: version = <my.version>
[INFO] Using property: package = my.package
Confirm properties configuration:
groupId: <my.groupid>
artifactId: <my-artifactId>
version: <my.version>
package: my.package
Y:

回复 Y 确认即可。

IDEA

File > New Module > Maven,勾选 Create from archetype,点击 Add Archetype,配置如下:

IDEA 中添加 archetype

输入创建 archetype 工程时,定义的 GroupId、ArtifactId、Version,并选择你远程仓库的地址即可,例如:http://xxx/nexus/content/repositories/snapshots。

配置完毕,创建新工程时,将会执行命令:

1
2
3
4
5
6
7
8
9
-DinteractiveMode=false
-DarchetypeGroupId=
-DarchetypeArtifactId=
-DarchetypeVersion=
-DarchetypeRepository=http://xxx/nexus/content/repositories/snapshots
-DgroupId=
-DartifactId=
-Dversion=
org.apache.maven.plugins:maven-archetype-plugin:RELEASE:generate

goal generate 执行过程中会下载指定的 archetype jar,并根据指定参数创建新工程。

注意,由于这种方式只会下载指定的 archetype jar 到本地仓库,但不会将骨架添加到本地仓库根目录下的骨架目录文件 archetype-catalog.xml 之中。这将会导致在 IDEA 之外以命令行方式执行 mvn archetype:generate 生成新工程时,由于在骨架目录文件中找不到指定 archetype 而报错,因此需要将该 archetype 添加到骨架目录文件下。解决方法是执行命令:mvn archetype:crawl 遍历本地仓库搜索骨架并更新目录文件。

参考

https://maven.apache.org/guides/introduction/introduction-to-archetypes.html

https://maven.apache.org/guides/mini/guide-creating-archetypes.html

http://maven.apache.org/archetype/maven-archetype-plugin/

Maven 本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由 maven-compiler-plugin 完成的。每个插件会有一个或者多个目标(goal),例如 maven-compiler-plugin 插件的 compile 目标用来编译位于 src/main/java/ 目录下的主源码,testCompile 目标用来编译位于 src/test/java/ 目录下的测试源码。

用户可以通过两种方式调用 Maven 插件目标:

  1. 将插件目标与生命周期阶段(lifecycle phase)绑定,这样用户在命令行只是输入生命周期阶段而已,例如 Maven 默认将 maven-compiler-plugincompile 目标与 compile 生命周期阶段绑定,因此命令 mvn compile 实际上是先定位到 compile 这一生命周期阶段,然后再根据绑定关系调用 maven-compiler-plugincompile 目标。
  2. 直接在命令行指定要执行的插件目标(goal),例如 mvn archetype:generate 就表示调用 maven-archetype-plugingenerate 目标,这种带冒号的调用方式与生命周期无关

常用插件整理如下:

Maven 常用插件

核心插件

maven-clean-plugin

maven-resources-plugin

maven-compiler-plugin

https://maven.apache.org/plugins/maven-compiler-plugin/index.html

https://docs.oracle.com/en/java/javase/11/tools/javac.html

配置 javac 编译使用指定的 JDK

https://maven.apache.org/plugins/maven-compiler-plugin/examples/compile-using-different-jdk.html

配置 javac 编译版本

1
2
3
# -source  Specifies the version of source code accepted.
# -target Generates class files for a specific VM version.
$ javac -source 1.8 -target 1.8

https://maven.apache.org/plugins/maven-compiler-plugin/examples/set-compiler-source-and-target.html

方式一

1
2
3
4
5
6
<project>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
</project>

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

配置 javac 编译参数

https://maven.apache.org/plugins/maven-compiler-plugin/examples/pass-compiler-arguments.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

maven-surefire-plugin

配置默认 Skip Tests

https://maven.apache.org/surefire/maven-surefire-plugin/examples/skipping-tests.html

方式一:

1
2
3
4
5
<project>
<properties>
<skipTests>true</skipTests>
</properties>
</project>

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

maven-install-plugin

maven-deploy-plugin

打包工具

maven-jar-plugin

maven-war-plugin

其它工具

maven-archetype-plugin

用于生成骨架,详见:Maven 骨架快速搭建项目

maven-assembly-plugin

用于将项目输出及其依赖项、模块、站点文档和其它文件聚合构建成一个可执行的分发包。

项目简单配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.github.testproject.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>
jar-with-dependencies
</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>

执行 Goal assembly:single,将在 target 目录中生成一个 artifactId-version-jar-with-dependencies.jar 文件,内含所需的所有依赖,执行 java -jar 即可运行。

maven-dependency-plugin

用于分析项目依赖,例如通过 mvn dependency:tree 命令分析 Dubbo 默认依赖的第三方库:

1
2
3
4
[INFO] +- com.alibaba:dubbo:jar:2.5.9-SNAPSHOT:compile
[INFO] | +- org.springframework:spring-context:jar:4.3.10.RELEASE:compile
[INFO] | +- org.javassist:javassist:jar:3.21.0-GA:compile
[INFO] | \- org.jboss.netty:netty:jar:3.2.5.Final:compile

Spring Boot 插件

Spring Boot 提供了 spring-boot-maven-plugin 插件,可用于本地快速编译并运行、及项目打包。参考:Maven 插件

IDEA Maven 插件

最后来看下 IDEA Maven 插件提供的 Maven Projects tool window 功能:

IDEA Maven Projects

参考

http://maven.apache.org/plugins/index.html

Nginx 中配置 HTTPS/SSL 加密是非常简单的,只需要将可选 HTTP 模块中的 ngx_http_ssl_module 编译进去即可。然后有两种方式开启 SSL 模式:

  • ssl on
  • listen 443 ssl 此端口上接收的所有连接都工作在 SSL 模式。

建议使用 listen 指令的 ssl 参数替代 ssl on 指令,这样可以为同时处理 HTTP 和 HTTPS 请求的服务器提供更加紧凑的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# http 和 https(ssl) 并存配置:

http {
server {
server_name example.com;
listen 80;
listen 443 ssl;

ssl_certificate example.com.crt;
ssl_certificate_key example.com.key;

location / {
proxy_pass http://127.0.0.1:81/;
}
}
}

注意,如果两个配置同时启用,HTTP 访问可能会报错:

1
2
400 Bad Request
The plain HTTP request was sent to HTTPS port

集群

Nginx 标准 HTTP 模块 ngx_http_upstream_module 内置了集群和负载均衡功能,使用其中的 upstream 配合 proxy_pass 指令即可快速实现一个集群:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
upstream backend {
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;

server backup1.example.com:8080 backup;
server backup2.example.com:8080 down;
}

server {
location / {
proxy_pass http://backend;
}
}
}

其中 server 指令的常用参数描述如下:

参数 描述
weight=number 设置服务器的轮询权重,默认为 1。用于后端服务器性能不均的情况。
max_conns=number 设置被代理服务器的最大可用并发连接数限制,默认为 0,表示没有限制。
max_fails=number 设置最大失败重试次数,默认为 1。设置为 0 表示禁用重试。
fail_timeout=time 设置失败时间,默认 10 秒。
backup 将服务器标记为备份服务器。当主服务器不可用时,它将被传递请求。
down 将服务器标记为永久不可用。

负载均衡

负载均衡(Load Balance),其意思就是将运算或存储负载按一定的算法分摊到多个运算或存储单元上,下面介绍 Nginx 几种常见的负载均衡方法:

  • 默认策略:加权轮询策略(weighted round-robin)。
  • random,加权随机策略。
  • ip_hash,基于客户端 IP 计算出哈希值,再根据服务器数量取模选取服务器(ip_hash % server_size = server_no)。
  • hash key [consistent],基于指定 key 计算出哈希值,再根据服务器数量取模选取服务器。可选一致性哈希算法缓解重映射问题。
  • least_conn,基于最小活跃连接数(加权)。如果有多个服务器符合条件,则使用加权轮询策略依次响应。
  • least_time,基于最小平均响应时间和最小活跃连接数(加权)。如果有多个服务器符合条件,则使用加权轮询策略依次响应。

ip hash

使用 Nginx ip_hash 指令,配置如下:

1
2
3
4
5
6
7
8
upstream backend {
ip_hash;

server backend1.example.com;
server backend2.example.com;
server backend3.example.com down;
server backend4.example.com;
}

ip_hash 指令指定集群使用基于客户端 IP 地址的负载均衡方法。 客户端 IPv4 地址的前三个八位字节或整个 IPv6 地址用作哈希键。 该方法确保来自同一客户端的请求将始终传递到同一台服务器,除非此服务器不可用,客户端请求则将被转发到另一台服务器(多数情况下,始终是同一台服务器)。
如果其中一台服务器需要临时删除,则应使用 down 参数标记,以便保留当前客户端 IP 地址的哈希值。

一致性 hash

使用 Nginx hash 指令,常用的例如基于来源 IP 进行哈希,配置如下:

1
2
3
4
5
6
upstream backend {
hash $remote_addr consistent;

server backend1.example.com;
server backend2.example.com;
}

hash 指令指定集群使用基于指定 hash 散列键的负载均衡方法。散列键可以包含文本,变量及其组合。请注意,从集群中添加或删除服务器可能会导致大量键被重新映射到不同的服务器。

解决办法是使用 consistent 参数启用 ketama 一致性 hash 算法。 该算法将每个 server 虚拟成 n 个节点,均匀分布到 hash 环上。每次请求,根据配置的参数计算出一个 hash 值,在 hash 环上查找离这个 hash 最近的虚拟节点,对应的 server 作为该次请求的后端服务器。该算法确保在添加或删除服务器时,只会有少量键被重新映射到不同的服务器。这有助于为缓存服务器实现更高的缓存命中率。

会话保持

为了确保与某个客户端相关的所有请求都能够由同一台服务器进行处理,我们需要在负载均衡上启用会话保持功能,以确保负载均衡的部署不会影响到正常的业务处理,避免会话丢失。

Nginx 会话保持一般有两种方案:

  • 基于源 IP 地址的 ip_hash
  • sticky cookie 粘滞会话(也称会话保持\会话绑定)

ip_hash 实现会话保持的问题在于,当多个客户是通过正向代理(如翻墙)或 NAT 地址转换的方式来访问服务器时,由于都分配到同一台服务器上,会导致服务器之间的负载失衡。

sticky cookie 实现会话保持,在一定条件下可以保证同一个客户端访问的都是同一个后端服务器。通过 Nginx sticky 指令,配置如下:

1
2
3
4
5
6
upstream backend {
server backend1.example.com;
server backend2.example.com;

sticky cookie srv_id expires=1h domain=.example.com path=/;
}

下面这份配置在同一域名下有两个 location,分别对应了两组集群服务。为了分别实现会话保持,将 cookie 写入了对应的 path 下,避免 cookie 互相干扰,也减少了数据传输量:

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
http {
upstream backend1 {
server backup1.example.com:8080;
server backup1.example.com:8081;
sticky cookie srv_backend1 path=/backend1;
}

upstream backend2 {
server backup2.example.com:8080;
server backup2.example.com:8081;
sticky cookie srv_backend2 path=/backend2;
}

server {
server_name example.com;
listen 80;

location /backend1/ {
proxy_pass http://backend1;
}

location /backend2/ {
proxy_pass http://backend2;
}
}
}

健康检查

健康检查(Health Check)是保障集群可用性的重要手段,有三种常见的健康检查方法:

主动式健康检查

nginx_upstream_check_module 第三方模块为例,演示配置如下:

1
2
3
4
5
6
upstream backend1 {
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
check_keepalive_requests 100;
check_http_send "HEAD /m/monitor.html HTTP/1.1\r\nConnection: keep-alive\r\nHost: check.com\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}

这段配置表示:

  1. check 指令配置:每隔 interval 毫秒主动发送一个 http 健康检查包给后端服务器。请求超时时间为 timeout 毫秒。如果连续失败次数达到 fall_count,服务器就被认为是 down;如果连续成功次数达到 rise_count,服务器就被认为是 up。
  2. check_keepalive_requests 指令配置:一个连接发送的请求数。
  3. check_http_send 指令配置:请求包的内容(注意,这里必须配置 Host 请求头否则可能报错)。
  4. check_http_expect_alive 指令配置:响应状态码为 2XX3XX 表示请求成功、服务健康。

查看 Tomcat access.log 如下:

1
2
3
4
5
6
127.0.0.1 - - [06/Jun/2017:21:03:30 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:33 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:36 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:39 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:42 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:45 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -

此时关闭某台后端服务器,一段时间后再访问,请求会被路由到其它服务器;重启后,该服务器自动加入集群。通过健康状态页面 /status 可见:

1
2
3
4
5
6
7
Nginx http upstream check status

Check upstream server number: 2, generation: 2

Index Upstream Name Status Rise counts Fall counts Check type Check port
0 backend1 127.0.0.1:8080 up 4741 0 http 0
1 backend1 127.0.0.1:8081 down 0 2340 http 0

常用变量

$upstream_addr

该模块中很常用的一个变量,用于标识集群中服务器的 IP 和端口。一般会加入到 Nginx 日志、同时脱敏后加入到响应头中,用于排查问题来源:

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
http {

...

log_format main '"$http_x_forwarded_for" - "$upstream_addr" - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $remote_addr $request_time_msec'
access_log logs/access.log main;

map $upstream_addr $short_address {
~^\d+\.\d+\.\d+\.(.*) '';
}

server {
server_name example.com;
listen 80;

upstream backend {
server 127.0.0.1:81;
server 127.0.0.1:82;
}

location / {
add_header X-From $short_address$1;
proxy_pass http://backend/;
}
}
}

什么是代理?

一张图了解两种代理模式的区别:

两种代理模式

常见应用场景:

  • 正向代理:VPN 翻墙
  • 反向代理:Web 站点服务

Nginx 反向代理

Nginx 代理功能由标准 HTTP 模块中内置的 ngx_http_proxy_module 提供,因此无需额外编译,常见配置如下:

1
2
3
4
5
6
7
8
9
10
http {
server {
server_name example.com;
listen 80;

location / {
proxy_pass http://127.0.0.1:81/;
}
}
}

proxy_pass 指令用于配置被代理服务器的协议和地址。除了配置单机,还可以配置集群,详见 Nginx 负载均衡

常见问题

To slash or not to slash

Here is a handy table that shows you how the request will be received by your WebApp, depending on how you write the location and proxy_pass declarations. Assume all requests go to http://localhost:8080:

location proxy_pass Request Received by upstream
/webapp/ http://localhost:8080/api/ /webapp/foo?bar=baz /api/foo?bar=baz ✅
/webapp/ http://localhost:8080/api /webapp/foo?bar=baz /apifoo?bar=baz
/webapp http://localhost:8080/api/ /webapp/foo?bar=baz /api//foo?bar=baz
/webapp http://localhost:8080/api /webapp/foo?bar=baz /api/foo?bar=baz
/webapp http://localhost:8080/api /webappfoo?bar=baz /apifoo?bar=baz

In other words: You usually always want a trailing slash, never want to mix with and without trailing slash, and only want without trailing slash when you want to concatenate a certain path component together (which I guess is quite rarely the case).

上游无法获取真实的访问来源信息

使用反向代理之后,上游服务器(如 Tomcat)无法获取真实的访问来源信息(如协议、域名、访问 IP),例如下面代码:

1
2
3
4
5
6
request.getScheme() // 总是 http,而不是实际的 http 或 https
request.isSecure() // 总是 false
request.getRemoteAddr() // Nginx IP
request.getServerName() // 127.0.0.1
request.getRequestURL() // http://127.0.0.1:81/index
response.sendRedirect(...) // 总是重定向到 http

这个问题需要在 Nginx 和 Tomcat 中做一些配置以解决问题。

Nginx 配置:

使用 proxy_set_header 指令为上游服务器添加请求头:

1
2
3
4
5
6
7
8
9
10
http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

server {
...
}
}

Tomcat 配置:

在 Tomcat 的 server.xml 中配置 RemoteIpValve 让代码能够获取真实 IP 和协议:

1
2
3
<Valve className="org.apache.catalina.valves.RemoteIpValve" 
remoteIpHeader="X-Forwarded-For"
protocolHeader="X-Forwarded-Proto" />

解决结果:

1
2
3
4
5
6
7
request.getScheme() // 实际的 http 或 https
request.isSecure() // 对应的 false 或 true
request.getRemoteAddr() // 用户 IP
request.getHeader("X-Real-IP") // 用户 IP
request.getServerName() // example.com
request.getRequestURL() // 对应的 http://example.com/index 或 https://example.com/index
response.sendRedirect(...) // 实际的 http 或 https

全局 proxy_set_header 失效

先来看下 proxy_set_header 的语法:

1
2
3
4
5
6
7
8
9
语法:	proxy_set_header field value;
默认值: proxy_set_header Host $proxy_host;
proxy_set_header Connection close;
上下文: http, server, location

允许重新定义或者添加发往后端服务器的请求头。value 可以包含文本、变量或者它们的组合。当且仅当当前配置级别中没有定义 proxy_set_header 指令时,会从上面的级别继承配置。 默认情况下,只有两个请求头会被重新定义:

proxy_set_header Host $proxy_host;
proxy_set_header Connection close;

这里隐含一个坑:如果当前配置级别中定义了 proxy_set_header 指令,哪怕只配置了一个,都会导致无法从上面的级别继承配置,即导致全局级别的 proxy_set_header 配置失效。例如下述 HTTP 长连接配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

upstream backend {
server 127.0.0.1:8080;
keepalive 16;
}

server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
...
}
}
}

导致全局级别的 proxy_set_header 配置失效:

proxy_set_header 失效

解决办法是在 location 中重新配置这四个请求头。

参考

How nginx processes a request ?

万字多图,搞懂 Nginx 高性能网络工作原理!

本文用于理顺 Maven 构建的生命周期(Build Lifecycle)概念,掌握执行 Maven 命令时其背后的原理。

构建

生命周期(Lifecycle)

生命周期(Lifecycle)是一个抽象的概念,意味着它并不做任何实质性的事情,也就是说它像接口,只定义规范,具体细节不管。具体的实现细节则交给了 Maven 各个插件(Plugin)。

生命周期(Lifecycle)是一系列有序的阶段(Phase),而插件的目标(Goal)是跟某个生命周期的阶段绑定在一起的,如果一个阶段没有绑定任何目标,则运行该阶段没有任何实质意义。

摘录一段官方的描述:

A Build lifecycle is Made Up of build Phases.

A build phase represents a stage in the lifecycle.

A Build Phase is Made Up of Goals.

Maven 内置三种生命周期:

内置 Lifecycle 描述 内置 Phase 数量
clean Project cleaning 3 个
default Project deployment 23 个
site Creation of project’s site documentation 4 个

阶段(Phase)

Phase 虽然很多,但其中带连字符 (pre-*, post-*, or process-*) 的 Phase 通常不会在命令行中直接使用。

那么有哪些常用的 Phase?

Phase 描述
clean remove all files generated by the previous build.
complie compile the source code of the project
test test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed.
package take the compiled code and package it in its distributable format, such as a JAR.
install install the package into the local repository, for use as a dependency in other projects locally
deploy done in the build environment, copies the final package to the remote repository for sharing with other developers and projects.

如何运行一个 Phase?使用命令 mvn phase。运行时,首先由该 phase 确定对应的生命周期。然后从生命周期的第一个 Phase 开始,按顺序运行到该 phase。

如果要跳过测试,参数如下:mvn package -DskipTests

目标(Goal)

Phase 是一个逻辑概念,本身并不包含任何构建信息,运行 Phase 时,只是运行绑定到该 Phase 的 Goal。

Plugin 是一个物理概念,安装了 Plugin 才会有相应的一些 Goal。而每个 Goal 代表了该 Plugin 的一种能力。如果要直接运行一个 Goal,可以使用 mvn <plugin-prefix>:<goal>。其中 plugin-prefix 的约定格式如下:

约定格式 描述
maven-${prefix}-plugin Apache Maven 团队维护的官方插件。例如 maven-compiler-plugin,命令:mvn dependency:list
${prefix}-maven-plugin 其它插件。例如 spring-boot-maven-plugin,命令:mvn spring-boot:run

参考

http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html

BOM

What Is Maven BOM❓

BOM stands for Bill Of Materials. A BOM is a special kind of POM that is used to control the versions of a project’s dependencies and provide a central place to define and update those versions.

BOM provides the flexibility to add a dependency to our module without worrying about the version that we should depend on.

在大型项目中,BOM 用于将一组相关的、可以良好协作的构建(Maven Artifact)组合在一起,提供版本管理,避免构件间潜在的版本不兼容风险。

BOM 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
<modelVersion>4.0.0</modelVersion>

<groupId>test</groupId>
<artifactId>project-n-bom</artifactId>
<version>1.0</version>
<packaging>pom</packaging> <!-- 必须是 pom -->

<!-- 属性配置 -->
<properties...>
<!-- 依赖管理配置,声明该 BOM 管理的依赖 -->
<dependencyManagement...>
</project>

BOM 引用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 每个公司可能都拥有自己的标准父项目,为了解决 Maven 单继承问题,可以使用 `<dependencyManagement>` 通过组合方式来享受其提供的依赖版本统一管理的好处 -->
<dependencyManagement>
<dependencies>
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-bom -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-bom</artifactId>
<version>${openfeign.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

常用的 BOM 如下:https://mvnrepository.com/tags/bom

Project Maven URL
JUnit 5 https://mvnrepository.com/artifact/org.junit/junit-bom
Log4j 2 https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-bom
Jackson https://mvnrepository.com/artifact/com.fasterxml.jackson/jackson-bom
OpenFeign https://mvnrepository.com/artifact/io.github.openfeign/feign-bom
Reactor https://mvnrepository.com/artifact/io.projectreactor/reactor-bom
Netty https://mvnrepository.com/artifact/io.netty/netty-bom
Jetty https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-bom
Jersey https://mvnrepository.com/artifact/org.glassfish.jersey/jersey-bom
Micrometer https://mvnrepository.com/artifact/io.micrometer/micrometer-bom
Spring Framework https://mvnrepository.com/artifact/org.springframework/spring-framework-bom
Spring Session https://mvnrepository.com/artifact/org.springframework.session/spring-session-bom
Spring Security https://mvnrepository.com/artifact/org.springframework.security/spring-security-bom
Spring Integration https://mvnrepository.com/artifact/org.springframework.integration/spring-integration-bom
Spring Boot https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies
Spring Cloud https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies

实践例子

<dependencyManagement> 用于在具有层级关系的项目间统一管理依赖的版本,一般用在父项目中,通过它来管理 jar 包的版本,让子项目中引用一个依赖而不用显式的指定版本号,以达到依赖版本统一管理的目的。

这种方法的好处是显而易见的。依赖的细节(如版本号)可以在一个中心位置集中设置,并传播到所有继承的 POM。

但由于现实中有可能是 N 个项目都继承同一个父项目,如果把它们的依赖全部放到父项目的 <DependencyManagement> 中管理,势必会导致父项目的依赖配置急剧膨胀,形成一个巨型 POM。更为关键的是,后续各项目组都有升级各自提供的依赖版本的需要,如果大家都去改这个公共的父项目,版本管理会变得很混乱。

解决办法是按项目组粒度对依赖进行分组管理,分而治之。将每组依赖抽取成像 spring-boot-dependencies 一样的 BOM,并在父项目中 <dependencyManagement> 使用 scope=import 方式将这些 BOM 组合起来,这样父项目的 POM 就会十分干净且稳定,各组依赖的版本管理也能转由各自项目组的 BOM 专门负责,类似一个金字塔模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                   +------------+
| parent-pom |
+------+-----+
|
+-----------------|-----------------+
| | |
+-------v-------+ +-------+-------+ +-------v-------+
| Project-A-BOM | | Project-B-BOM | | Project-N-BOM |
+---------------+ +-------+-------+ +---------------+
|
+--------------|---------------+
| | |
+-----v------+ +-----v------+ +------v-----+
| Artifact-A | | Artifact-B | | Artifact-N |
+------------+ +------------+ +------------+

例如某个公司的标准父项目 POM 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<project>
<modelVersion>4.0.0</modelVersion>

<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging> <!-- 必须是 pom -->

<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot 1 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- 此处省略 N 组依赖 -->
......
</dependencies>
</dependencyManagement>
</project>

开发阶段为了测试 Spring Boot 2 的兼容性,该公司 child 项目新开 git 分支,修改 POM 如下:

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
<project>
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>child</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot 2 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

上例 child 项目覆盖了父项目 <dependencyManagement> 的 Spring Boot 版本,当兼容性测试通过后,子项目 <dependencyManagement> 即可去掉,转而升级父项目 Spring Boot 的版本号即可,这样所有子项目都会统一升级版本。

参考

https://en.wikipedia.org/wiki/Bill_of_materials

https://www.baeldung.com/spring-maven-bom

https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-build-systems.html

https://projectreactor.io/docs/core/release/reference/#getting-started-understanding-bom

除了依赖关系,继承和聚合关系也是 Maven 项目中很常用的两种关系。

继承关系

  • 在子项目中,使用 <parent> 指定父项目;
  • 项目能继承父项目的一些配置(属性,依赖等);
  • 一般用于多项目共享父配置。
  • 特别注意 relativePath 元素。虽非必填,但可以用作 Maven 的一个指示符,以在搜索本地和远程仓库之前首先搜索此路径,以达到优先使用本地父项目的目的(本地开发过程中有时我们会修改本地父项目的配置,例如修改依赖版本号)。

父项目配置如下:

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
<project>
<modelVersion>4.0.0</modelVersion>

<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging> <!-- 必须是 pom -->

<!-- 以下配置都可以被子项目继承 -->

<!-- 属性配置 -->
<properties...>
<!-- 依赖管理配置,如 spring-cloud、spring-boot、reactor -->
<dependencyManagement...>
<!-- 依赖配置 -->
<dependencies...>
<!-- 构建配置 -->
<build...>
<!-- Nexus 分发包地址 -->
<distributionManagement...>
<!-- Nexus 仓库地址 -->
<repositories...>
<!-- Nexus 插件地址 -->
<pluginRepositories...>
</project>

子项目配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<relativePath>../parent</relativePath>
</parent>

<groupId>test</groupId> <!-- 可继承自父项目 -->
<artifactId>child-a</artifactId>
<version>1.0</version> <!-- 可继承自父项目 -->
</project>

聚合关系

  • 在父项目中,使用 <modules> 指定子项目;
  • 在父项目中进行构建,会同时构建全部子项目;
  • 一般用于批量管理一个项目的多个模块;

父项目配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
<modelVersion>4.0.0</modelVersion>

<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging> <!-- 必须是 pom -->

<modules>
<!-- 子模块相对于父项目的路径,一般为 artifactId -->
<module>child-a</module>
<module>child-a</module>
</modules>
</project>

依赖关系

依赖关系是 Maven 项目之间最常见的关系。Maven 会自动解析所有项目的直接依赖传递性依赖,并且根据规则正确判断每个依赖范围(Dependency Scope)。对于一些依赖冲突,自动进行依赖调解(Dependency Mediation),或开发者手动排除依赖(exclusions),以确保任何一个构件只有唯一的版本在依赖中存在。经过这些工作之后,得到的那些依赖被称为已解析依赖(Resolved Dependency),从而产生一个 Effective POM。

依赖配置

下面是一段依赖配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<project>

<dependencies>
<dependency>
<groupId></groupId> <!-- 依赖的构件属组 -->
<artifactId></artifactId> <!-- 依赖的构件ID -->
<version></version> <!-- 依赖的构件版本 -->
<type></type> <!-- 依赖类型,默认值为 jar -->
<scope></scope> <!-- 依赖范围 -->
<optional></optional> <!-- 可选依赖,默认情况下不会被继承,只有声明了该依赖才会继承 -->
<exclusions> <!-- 排除(传递性)依赖 -->
<exclusion></exclusion>

</exclusions>
</dependency>

</dependencies>

</project>

依赖传递

Maven 会解析各个直接依赖的 POM,将那些必要的间接依赖,以传递性依赖的方式引入到当前项目中,而开发者不用再考虑该直接依赖还依赖了什么,或手动引入导致多余依赖:

传递性依赖

依赖调解

Maven 调解依赖有两个基本原则:

  1. 第一原则:路径最短者优先
    假设 X 是 A 的传递性依赖:A->B->C->X(1.0) 依赖路径长度为 3,A->D->X(2.0) 长度为 2,则 X(2.0) 会先被解析使用。
  2. 第二原则:第一声明者优先
    在依赖路径长度相等的前提下,在 POM 中,依赖声明的顺序决定了谁会先被解析使用。

依赖范围 scope

依赖范围 scope 用于控制依赖与这三种 classpath(编译、测试、运行)的关系,即是否有效:

依赖范围(Scope) 编译classpath 测试classpath 运行classpath 例子
compile spring-core
test × × spring-test、junit、mockito
provided × servlet-api(运行时,由于容器如tomcat已经提供,无须重复引入)
runtime × JDBC 驱动实现(编译时,只需 JDK 提供的 JDBC 接口;运行时,才需要接口的具体实现)
system × 本地的,Maven 仓库之外的类库文件
import / / / Maven 2.0.9 版本后出的属性,import 只能在 dependencyManagement 的中使用,能解决 Maven 单继承问题,import 依赖关系实际上并不参与限制依赖关系的传递性。

可选依赖 optional

可选依赖 optionaltrue 时,该依赖不会被传递进来。

依赖排除 exclusions

当传递性依赖的结果不是自己预期的构件版本,可以排除依赖重新自定义。下例展示了使用 <exclusions> 排除 A 的传递性依赖 C,并显式声明依赖 C(1.10):

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
<project>

<dependencies>
<dependency>
<groupId></groupId>
<artifactId>A</artifactId>
<version>1.0</version>
<exclusions> <!-- 排除 A 的传递性依赖 C -->
<exclusion>
<groupId></groupId>
<artifactId>C</artifactId>
</exclusion>

</exclusions>
</dependency>

<!-- 显式声明依赖 C(1.10) -->
<dependency>
<groupId></groupId>
<artifactId>C</artifactId>
<version>1.10</version>
</dependency>

</dependencies>

</project>

结果如图:

依赖排除

依赖归类

当多个构件使用同一版本号时,为了便于后期统一升级维护,可以使用 <properties> 属性统一定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project>

<properties>
<spring.version>5.0.0.RELEASE</spring.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
...
</dependencies>

</project>

常见问题

解决依赖冲突

例如在使用 Spring Boot 时,其依赖引入了一个有缺陷的 Jackson 框架版本 1.9.X,此时需要覆盖起步依赖引入的传递依赖:

方式一,如果项目未使用 Jackson 相关功能,可以使用 <exclusions> 排除依赖:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
</exclusion>
</exclusions>
</dependency>

方式二,如果项目使用了,可以利用“就近原则”,覆盖传递依赖的版本号,修复缺陷:

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.4.3</version>
</dependency>

依赖下载问题

有时候某个依赖下载有问题时,会导致本地环境找不到相关类。此时可以找到本地仓库下的相关 jar 包目录,排查下是否只有 *.lastupdated 文件而没有相应 jar 包。如果是,则删掉该目录重新下载依赖即可。

参考

http://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html

Maven optional关键字透彻图解

Maven 坐标

Maven 定义了这样一组规则:世界上任何一个构件都可以使用 Maven 坐标唯一标识,Maven 坐标的元素包括:

坐标元素 描述
groupId 必选。定义当前 Maven 项目隶属的实际项目。
artifactId 必选。定义实际项目中的一个 Maven 项目(模块),推荐使用实际项目名称作为前缀。
version 必选。定义当前 Maven 项目所处版本。
packaging 可选。定义 Maven 项目的打包方式,默认为 jar
classifier 不能直接定义。用来帮助定义构建输出的一些附属构件。如 javadoc、sources。

POM 关系类型

Maven 一个强大的地方在于处理项目之间的关系。其中包括依赖关系(包括传递性依赖关系)、继承关系聚合关系(多模块项目)。

详见:《Maven 依赖关系》、《Maven 继承关系与聚合关系

属性

Maven 属性是类似于 Ant 属性一样的值占位符。可以通过符号 ${X}(其中 X 是属性)在 POM 中的任意位置访问它们的值。它们也可以被插件用作默认值使用,例如:

1
2
3
4
5
6
7
8
9
10
<project>
...
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
...
</project>

共有五种风格的占位符:

${env.X} 返回 shell 的环境变量。例如 ${env.PATH} 返回 PATH 环境变量。为了可靠性,Maven 2.1.0 开始将环境变量的名称归一化为全部大写。
${project.x} 返回 pom.xml 文件中对应元素的值。 例如可通过 ${project.version} 访问 <project><version>1.0</version></project>
${settings.x} 返回 settings.xml 文件中对应元素的值。例如 <settings><offline>false</offline></settings> 可通过 ${settings.offline} 访问。
${java.x} 返回 Java 系统属性。 通过 java.lang.System.getProperties() 可访问的所有属性都可用作 POM 属性,例如${java.home}
${x} 返回 pom.xml 文件中 <properties/> 元素内的值。例如 <spring.version>1.0.1-SNAPSHOT</spring.version> 可通过 ${spring.version} 访问。

参考

http://maven.apache.org/pom.html