读《maven实战》笔记

本文为阅读书籍《maven实战–》的读书笔记。

maven是声明式的:也就是所有功能由插件实现。

1.项目结构

约定大于配置:如果存在古老的项目不是这种结构,那么maven也有相应的配置来修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
my-app
|-- pom.xml
`-- src
|-- main
| `-- java
| `-- com
| `-- mycompany
| `-- app
| `-- App.java
`-- test
`-- java
`-- com
`-- mycompany
`-- app
`-- AppTest.java

2.maven坐标

  1. groupId:公司+项目,,eg:org.springframework.boot
  2. artifactId: 项目下的模块,eg:spring-boot-starter-web
  3. version
  4. packaging: jar, war等
  5. classfier: 附属构件,由附加的插件生成,比如spring-boot-starter-web-2.0.4.RELEASE-javadoc.jar

3.maven的组合和继承

组合

为了快速构建多个模块

1
2
3
4
<modules>
<module>A</module>
<module>B</module>
</modules>

继承

为了复用配置

1
2
3
4
5
6
<parent>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<relativePath>../xxx/pom.xml</relativePath>
</parent>

4.dependency

依赖构造的是有向无环图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<project>
...
<dependencies>
<dependency>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<type></type>
<scope></scope>
<optional></optional>
<exclusions>
<exclusion>
...
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
  • type: packaging, jar(默认)/war
  • scope: 见后面
  • optional: 是否可选,见后面

scope

  • compile: 编译、测试、运行都有效
  • test: 只对测试有效,eg: junit
  • provided: 编译和测试有效,运行时无效,eg:servlet-api.jar
  • runtime: 测试和运行有效,编译时无效: eg: JDBC驱动
  • system: 和provided一样,只是依赖时必须用systemPath显示指定依赖路径,而不是由maven解析,是与本机绑定:
    1
    2
    3
    4
    5
    6
    7
    <dependency>
    <groupId></groupId>
    <artifactId></artifactId>
    <version></version>
    <scope>system</scope>
    <systemPath>/xxx/xx.jar</systemPath>
    </dependency>
  • import: [import范围](### import范围)

传递性依赖

如果:A -> B -> C

那么:A–> C

传递性依赖范围影响

compile test provided runtime
compile compile - - runtime
test test - - test
provided provided - provided provided
runtime runtime - - runtime

依赖路径

  1. 最近优先:A->B->C->X(1.0)、A->D->X(2.0)
  2. 路径长度相同,先声明的优先(2.0.8之后):A->B->Y(1.0)、A->C->Y(2.0)

可选依赖(optional)

如果,A->B、B->X(可选)、B->Y(可选),那么A将不会依赖X和Y

但是不建议使用,因为OOP的单一职责原则。

dependencyManagement

依赖管理:Maven提供的dependencyManagement元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在dependencyManagement元素下的依赖声明不会引入实际的依赖,不过它能够约束dependencies下的依赖使用。

啥意思?看下面解释:

  1. 依赖管理是在继承中使用的

  2. 假如父pom声明了一些依赖:但是这并不会给父pom引入依赖

    1
    2
    3
    4
    5
    6
    7
    <dependencyManagement>
    <dependency>
    <groupId>A</groupId>
    <artifactId>a</artifactId>
    <version>xx</version>
    </dependency>
    </dependencyManagement>
  3. 子pom如果引入了a依赖,那么不需要声明版本,因为父pom里已经有了:

    1
    2
    3
    4
    <dependency>
    <groupId>A</groupId>
    <artifactId>a</artifactId>
    </dependency>

    如果子pom不声明a依赖,那么a就不会被引入

笔者强烈推荐这种写法,虽然也存在一定冗余,但是会降低冲突的概率。

import范围

import范围的依赖只在dependencyManagement元素下才有效果,使用该范围的依赖通常指向一个POM,作用是将目标POM中的dependencyManagement配置导入并合并到当前POM的dependencyManagement元素中。

比如,A模块(假如groupId为A,artifactId为a)的pom的内容如下:

1
2
3
4
5
<dependencyManagement>
<dependency>
...
</dependency>
</dependencyManagement>

B模块的pom想复用A的pom里的dependencyManagement的依赖,除了复制和继承,还可以使用import导入:

1
2
3
4
5
6
7
8
9
<dependencyManagement>
<dependency>
<groupId>A</groupId>
<artifactId>a</artifactId>
<version>xx</version>
<type>pom</type>
<scope>import></scope>
</dependency>
</dependencyManagement>

5.插件

maven的核心功能只有几兆,生命周期的功能都是由插件完成的,需要时会下载。

官方文档介绍了插件及其开源地址

插件目标

因为很多任务背后有很多可以复用的代码,因此,这些功能聚集在一个插件里,每个功能就是一个插件目标。

maven-dependency-plugin有十多个目标,每个目标对应了一个功能,常见的插件目标为dependency:analyzedependency:treedependency:list
这是一种通用的写法,冒号前面是插件前缀,冒号后面是该插件的目标。类似地,还可以写出compiler:compile(这是maven-compiler-plugincompile目标)和surefire:test(这是maven-surefire-plugintest目标)。

插件配置

命令行配置: -Dkey=value, 实际上是java自带的系统属性方式

1
$ mvn install -Dmaven.test.skip=true

POM文件全局配置

1
2
3
4
5
6
7
8
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

插件帮助描述

mvn help:describe -Dplugin=xxx [-Dgoal=xxx] [-Ddetail]

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
$ mvn help:describe -Dplugin=org.apache.maven.plugins:maven-compiler-plugin
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< com.jimo:jasypt-demo >------------------------
[INFO] Building jasypt-demo 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-help-plugin:3.1.1:describe (default-cli) @ jasypt-demo ---
[INFO] org.apache.maven.plugins:maven-compiler-plugin:3.8.1

Name: Apache Maven Compiler Plugin
Description: The Compiler Plugin is used to compile the sources of your
project.
Group Id: org.apache.maven.plugins
Artifact Id: maven-compiler-plugin
Version: 3.8.1
Goal Prefix: compiler

This plugin has 3 goals:

compiler:compile
Description: Compiles application sources

compiler:help
Description: Display help information on maven-compiler-plugin.
Call mvn compiler:help -Ddetail=true -Dgoal=<goal-name> to display
parameter details.

compiler:testCompile
Description: Compiles application test sources.

For more information, run 'mvn help:describe [...] -Ddetail'

6.常用maven命令

执行测试:mvn clean test

打包任务:jar:jar

安装任务:install:install

查看依赖

看3个地方:都是冒号分隔的

  1. 依赖名
  2. 中间的版本号
  3. 最后的依赖范围
1
$ mvn dependency:list | grep springframework

查看依赖树,层级关系(第一依赖、第二依赖…)

1
$ mvn dependency:tree

分析依赖

1
$ mvn dependency:analyze

接下来看其输出,着重关注警告。

Used undeclared dependencies found
指项目中使用到的,但是没有显式声明的依赖,这里是spring-context。这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的Java import声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。因此,显式声明任何项目中直接用到的依赖。

1
2
3
4
5
6
7
[WARNING] Used undeclared dependencies found:
[WARNING] org.springframework.boot:spring-boot:jar:2.0.4.RELEASE:compile
[WARNING] org.springframework.cloud:spring-cloud-netflix-eureka-server:jar:2.0.1.RELEASE:compile
[WARNING] org.springframework:spring-test:jar:5.0.8.RELEASE:test
[WARNING] org.springframework.boot:spring-boot-test:jar:2.0.4.RELEASE:test
[WARNING] junit:junit:jar:4.12:test
[WARNING] org.springframework.boot:spring-boot-autoconfigure:jar:2.0.4.RELEASE:compile

Unused declared dependencies: 意指项目中未使用的,但显式声明的依赖。需要注意的是,对于这样一类依赖,我们不应该简单地直接删除其声明,而是应该仔细分析。由于dependency:analyze只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。当然,有时候确实能通过该信息找到一些没用的依赖,但一定要小心测试。

1
2
3
4
5
6
7
8
[WARNING] Unused declared dependencies found:
[WARNING] org.springframework.cloud:spring-cloud-starter-netflix-eureka-server:jar:2.0.1.RELEASE:compile
[WARNING] org.springframework.boot:spring-boot-starter-undertow:jar:2.0.4.RELEASE:compile
[WARNING] org.springframework.boot:spring-boot-starter-web:jar:2.0.4.RELEASE:compile
[WARNING] org.springframework.boot:spring-boot-starter-test:jar:2.0.4.RELEASE:test
[WARNING] de.codecentric:spring-boot-admin-starter-client:jar:2.0.4:compile
[WARNING] org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:jar:2.0.1.RELEASE:compile
[WARNING] com.github.ulisesbocchio:jasypt-spring-boot-starter:jar:2.1.1:compile

pluginManagement

和dependencyManagement类似。

仓库

为了满足一些复杂的需求,Maven还支持更高级的镜像配置:

  • <mirrorOf>*</mirrorOf>:匹配所有远程仓库。
  • <mirrorOf>external:*</mirrorOf>:匹配所有远程仓库,使用localhost的除外,使用file://协议的除外。也就是说,匹配所有不在本机上的远程仓库。
  • <mirrorOf>repo1,repo2</mirrorOf>:匹配仓库repo1和repo2,使用逗号分隔多个远程仓库。
  • <mirrorOf>*,!repo1</mirrorOf>:匹配所有远程仓库,repo1除外,使用感叹号将仓库从匹配中排除。
1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>internal-repo</id>
<name>repo jimo</name>
<url>http://IP/maven3</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>

maven生命周期

Maven拥有三套相互独立的生命周期,它们分别为clean、default和site。

  • clean生命周期的目的是清理项目
  • default生命周期的目的是构建项目
  • site生命周期的目的是建立项目站点。

clean

  1. pre-clean执行一些清理前需要完成的工作。
  2. clean清理上一次构建生成的文件。
  3. post-clean执行一些清理后需要完成的工作。

default

官方文档

  • validate
  • initialize
  • generate-sources
  • process-sources处理项目主资源文件。一般来说,是对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的主classpath目录中。
  • generate-resources
  • process-resources
  • compile编译项目的主源码。一般来说,是编译src/main/java目录下的Java文件至项目输出的主classpath目录中。
  • process-classes
  • generate-test-sources
  • process-test-sources处理项目测试资源文件。一般来说,是对src/test/resources目录的内容进行变量替换等工作后,复制到项目输出的测试classpath目录中。
  • generate-test-resources
  • process-test-resources
  • test-compile编译项目的测试代码。一般来说,是编译src/test/java目录下的Java文件至项目输出的测试classpath目录中。
  • process-test-classes
  • test使用单元测试框架运行测试,测试代码不会被打包或部署。
  • prepare-package
  • package接受编译好的代码,打包成可发布的格式,如JAR。
  • pre-integration-test
  • integration-test
  • post-integration-test
  • verify
  • install将包安装到Maven本地仓库,供本地其他Maven项目使用。
  • deploy将最终的包复制到远程仓库,供其他开发人员和Maven项目使用。

site

  • pre-site执行一些在生成项目站点之前需要完成的工作。
  • site生成项目站点文档。
  • post-site执行一些在生成项目站点之后需要完成的工作。
  • site-deploy将生成的项目站点发布到服务器上。

生命周期命令

就是各个生命周期的命令组合使用:

1
2
3
4
$ mvn clean
$ mvn test
$ mvn clean install
$ mvn clean deploy site-deploy

实践操作

写插件实践

自己写maven插件

测试插件:maven-invoker-plugin

打包时指定主类

当直接打包运行时会出现找不到主类问题:

1
2
$ java -jar target/test-maven-main-1.0-SNAPSHOT.jar 
target/test-maven-main-1.0-SNAPSHOT.jar中没有主清单属性

原因是META-INF里没有主类信息;

这时候增加插件解决:

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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.jimo.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

结果:

1
2
3
4
$ mvn clean package

$ java -jar target/test-maven-main-1.0-SNAPSHOT.jar
hello world