Maven实战 1
Maven实战1
一、maven的安装和配置
1. Windows
官网下载-->配置环境变量-->mvn --version测试可用
2. Linux
自行apt安装...
3. 一条有用的maven命令
# Displays a list of the platform details like system properties and environment variables.
# Maven会自动下载maven-help-plugin,包括pom和jar等依赖
mvn help:system
4. 设置HTTP代理
如果公司禁止访问外网,这也就限制了Maven中央仓库的访问。设置代理,通过代理访问
setting.xml(apache-maven-3.6.1\conf\下)新增结点
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>
二、maven的使用入门
1. pom文件的编写
pom文件类似于Makefile或build.gradle等配置文件,即使没有代码,空的文件夹也是可以进行maven管理。一个最简单的pom.xml:
<?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">
<!-- 指定pom模型的版本,对于maven2及3只能是4.0.0 -->
<modelVersion>4.0.0</modelVersion>
<!-- 公司名+项目名 -->
<groupId>com.hito.maven</groupId>
<!-- 项目名子模块 -->
<artifactId>hello-maven</artifactId>
<!-- 模块版本号 snapshot为快照,既不稳定版本 -->
<version>1.0-SNAPSHOT</version>
<!--classfiier标签很少使用,用来帮助定义构建输出的一些附属构件。如一些jar,包含java文档和源代码-->
</project>
2. 编写主代码
直接通过idea新建一个空的Java Maven项目,注意下面main与test文件夹,默认规定测试代码位置
│ pom.xml
└─src
├─main
│ ├─java
│ │ └─com
│ │ └─hito
│ │ └─maven
│ │ HelloMaven.java
│ │
│ └─resources
└─test
└─java
- mvn clean compile 可以发现生成了target目录
2.1 测试代码
-
引入依赖坐标
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
-
测试代码编写
package com.hito.maven; import org.junit.Test; import static org.junit.Assert.*; /** * * @author HitoM * @date 2019/8/20 23:25 **/ public class HelloTest { @Test public void testHello(){ HelloMaven helloMaven = new HelloMaven(); assertEquals(helloMaven.sayHello(), "HelloMaven"); } }
-
mvn clean test
进行测试
3. 依赖范围
在上面的依赖坐标中你应该注意到:scope标签,它表示依赖范围,它有五个值
-
test:依赖只对test中的代码有效。在main中你会发现无法导入junit包,如果改为compile则可以。
-
compile:缺省值,表示在运行,打包,测试几个声明周期中,对应的jar包都是存在可用的。classpath 中可用,同时它们也会被打包
-
runtime:运行时范围。编写代码时,不会参加编译,仅运行时有效。即对于测试和运行class-path有效,编译无效。
比如JDBC驱动,我们在配置文件中只会配置JDBC的驱动类的完整路径,运行时才会使用反射获取真正的实例.
再比如log4j,log4j其实是一套日志框架和规范。apache有api接口定义和core实现,我们可以使用apache的core实现也可以使用其他的实现,如jdk中的Logging。那么我就可以这么去实现pom,正常编码时使用接口,程序真正运行是core
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.11.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.11.1</version> <scope>runtime</scope> </dependency>
在这其中发现:log4j-core中其实是有log4j-api依赖的,为什么没有发生冲突呢?因为两者版本一致。那如果版本不一致log4j-api 2.11.1 log4j-core2.12.1,我们应该怎么处理冲突呢?
版本不一致是要注意,一般高版本是兼容低版本的,所以在使用exclusions时保留的应该是高版本。在这里如果排除log4j-core2.12.1中的log4j-api,在pom dependencies目录中的log4j-api2.11.1是无法满足core要求的。解决办法:升高log4j-core版本号,然后exclusion。
-
provided:类似compile,对于编译和测试class-path有效,运行时无效。期望JDK、容器或使用者会提供这个依赖,正式包中不会有依赖。eg:servlet-api
-
system:与provided的依赖范围完全一致,但是必须使用systemPath显示指定依赖文件的路径,容易造成不可移植性,慎用!
<dependency> <groupId>com.google.code</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> <scope>system</scope> <systemPath>${project.basedir}/libs/kaptcha-2.3.2.jar</systemPath> </dependency>
依赖范围(Scope) 对于编译class-path有效 对于测试class-path有效 对于运行时class-path有效 例子 compile Y Y Y spring-core test N Y N JUnit provided Y Y N servlet-api runtime N Y Y JDBC驱动 system Y Y N 本地的,Maven仓库之外的类库文件
4. 打包和运行
4.1 打包
通过maven-compiler-plugin的package命令mvn clean package
即可打包。mvn install
可以将生成的包安装在本地,这样其他依赖可以直接使用。
4.2 打可运行包
上面的方式生成的是依赖jar包,如果想要生成带有Main函数的可执行jar包需要怎么办呢?有三种常见的Maven打包插件,maven-jar-plugin、maven-shade-plugin和maven-assembly-plugin,这里通过maven-shade-plugin来实现打可执行包操作
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.hito.maven.Main</mainClass>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
mvn package
即可在target下发现打包成功,通过java -jar xxx.shade.jar
即可执行。
三、依赖
1. 依赖传递
A依赖于B,B依赖于C,那么A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。
依赖范围影响传递性依赖如下表,其中第一列表示第一直接依赖范围,第一行表示第二直接依赖范围。
compile | test | provided | runtime | |
---|---|---|---|---|
compile | compile | —— | —— | runtime |
test | test | —— | —— | test |
provided | provided | —— | provided | provided |
runtime | runtime | —— | —— | runtime |
2. 依赖调解
Maven是如何处理依赖冲突的呢?如何调解?
- 第一原则,路径最短优先。A->B->Y(1.0),A->Y(2.0),那么A是依赖于Y(2.0)的
- 第二原则:第一声明优先。如果两者路径相同,谁在pom中先声明,就依赖谁。
3. 可选依赖
B是一个持久层隔离工具包,它支持多种数据库,Mysql、PostgreSQL,构建时需要两种jar,但是B作为依赖被使用时只会依赖一种数据库。这样的话,B中需要声明两种可选依赖,而且是只对B可见可用。A如果想要依赖B,还需显示声明MySQL依赖或PostgreSQL中一种。
<!--B-->
<dependencies>
<dependency>
<groupId>xxx</groupId>
<artifactId>xxx</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>yyy</groupId>
<artifactId>yyy</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<!--A-->
<dependencies>
<dependency>
<groupId>B</groupId>
<artifactId>B</artifactId>
</dependency>
<!--\声明B中所用具体实现-->
<dependency>
<groupId>yyy</groupId>
<artifactId>yyy</artifactId>
</dependency>
</dependencies>
4. 最佳实践
对于Maven的总结,就是最佳实践辣
4.1 排除依赖
传递依赖会给项目带来很多隐式依赖,这就导致会跟项目本身显示添加的依赖产生冲突,可以通过排除依赖的方式使项目使用显示的依赖而不是隐式依赖。
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<!--这里并不需要指定版本号-->
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
4.2 归类依赖
依赖与依赖之间是有关联的,比如Spring全家桶。这样我们就可以在Pom文件的properties标签下声明统一的版本号,然后通过${}
引用,可以做到依赖版本的统一管理。
4.3 优化依赖
可以运用如下的命令查看解析当前项目的依赖:
-
依赖列表
mvn dependency:list
-
查看依赖树
mvn dependency:tree
-
依赖分析
mvn dependency:analyze
这个结果中有两个重要的部分:
- Used undeclared dependencies:项目中使用了未显示声明的依赖,即在代码中使用了由直接依赖传递的依赖,这样在升级直接依赖的时候不易发现版本变化,这是一种隐藏的威胁。
- Unused declared dependencies:明明未使用,但是显示声明了的依赖。注意,不应该将此类依赖简单删除,因为analyze只会分析编译代码和测试代码,运行时需要的依赖它是无法发现的。所以我们应该仔细分析后再处理。
四、生命周期和插件
- clean build compile install 是什么?怎么来的?
- maven为什么能够做到这些?
1. 何为生命周期
在Maven出现之前,项目的生命周期就已经存在了,Developer对项目进行清理、编译、测试和部署,Maven对所有的构建过程进行了抽象和统一。 Maven的生命周期是抽象的,这就意味着生命周期本身不做任何实际的工作,而是由插件来完成。如同Maven声明抽象方法,而具体实现由插件完成。
2. 生命周期详情
2.1 三套生命周期
一开始以为Maven的生命周期为一个整体,其实不然,Maven拥有三套相互独立的生命周期:
- clean:清理项目,包含三个阶段
- pre-clean
- clean
- post-clean
- default:构建项目
- validate: validate the project is correct and all necessary information is available
- initialize
- generate-sources
- process-sources
- generate-resources
- process-resources
- compile:编译项目的主源码
- process-classes
- generate-test-sources
- process-test-sources
- .....
- test:使用单元测试框架运行测试,测试代码不会被打包或部署
- package:接受编译好的代码,打包成可发布的格式,如jar、war等
- verify: run any checks on results of integration tests to ensure quality criteria are met.
- install:将包安装到本地仓库,供本地其他的Maven项目使用
- depoy:将包复制到远程仓库,供其他开发人员和Maven项目使用
- site:建立项目站点(一个用来浏览项目信息的本地小网站一样)
- pre-site
- site
- post-site
- site-deploy
2.2 命令行和生命周期
从命令行执行Maven任务的最主要方式就是调用Maven的生命周期阶段。各个生命周期是相互独立的,而一个生命周期阶段是有前后依赖关系的
- mvn clean:实际执行 pre-clean和clean
- mvn test:调用default的test阶段,实际执行的生命周期为validate、initialize直到test的所有阶段
- mvn install:调用clean和install阶段,实际执行的阶段为pre-clean、clean阶段以及default生命周期的从validate到install的所有阶段,该命令结合了两个生命周期阶段,在执行真正的项目构建之前清理项目是一个很好的实践
- mvn clean deploy site-deploy:该命令调用clean生命周期的clean阶段、default生命周期的deploy阶段以及site生命周期的site-deploy阶段。实际执行为pre-clean、clean阶段,default的所有生命周期阶段以及site的所有生命周期阶段。
3. 插件
我们知道Maven的核心仅仅定义了抽象的生命周期,具体的任务交与插件完成,插件以独立的构件形式存在。Maven的核心分发包3MB左右,Maven会在需要的时候下载并使用插件。插件是如何和生命周期绑定关系的呢?在这之前我们必须了解插件目标
3.1 插件目的
对于插件本身,为了能够复用代码,它往往能够完成多个任务。例如maven-dependency-plugin,它能够分析项目依赖(mvn dependency:analyze
),列出项目依赖树等。为每个这样的功能编写独立的插件显然是不可取的,因为这些任务背后有可以复用的代码,因此将这些功能聚集在一个插件里,每个功能就是一个插件的目标。你可以在Mavne官网插件列表中查看 各个plugin的goals。
3.2 插件绑定
Maven的生命周期与插件相互绑定,用以完成实际的构建任务。具体而言,生命周期的阶段与 插件的目的相互绑定,已完成某个具体的构建任务。例如项目编译这一任务,它对应了default生命周期的compil这一阶段,而maven-compiler-plugin这一插件的compile目标能够完成该任务。因此将它们绑定,就能实现编译的目的。可是我们在pom中并没有配置绑定关系,它们是如何标定的呢?
3.3 内置绑定
为了让用户几乎不用任何配置就能构建Maven项目,Maven在核心为一些主要的生命周期阶段绑定了很多插件的目标,当用户通过命令行调用生命周期阶段的时候,对应的插件目标就会执行相应的任务。
-
clean生命周期与插件目标待定绑定关系
生命周期阶段 插件目标 pre-clean
clean
post-cleanmaven-clean-plugin:clean -
default生命周期的内置插件绑定关系和具体任务
生命周期阶段 插件目标 执行任务 process-resources maven-resources-plugin:resources 复制主资源文件到主输出目录 compile maven-compiler-plugin:compile 编译主代码值主输出目录 test maven-surefire-plugin:test 执行测试用例 package maven-jar-plugin:jar 创建项目jar包 install maven-install-plugin:install 将项目输出构建安装到本地仓库 ... ... ... 这些都可以在Idea中的maven插件中找到。
3.4 自定义绑定
除了内置绑定,当然还可以自己选择将某个插件目标绑定到生命周期的某个阶段。
一个常见的例子就是创建项目的源码jar包,内置的插件并没有涉及到这一任务。maven-source-plugin可以帮助我们完成任务,他的jar-no-fork目标能够将项目的主代码打成jar文件,可以将其绑定到default生命周期的verify阶段上,在执行完集成测试和安装构件之前创建源码jar包。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
执行mvn verify
就会发现target文件下打包成功。
[INFO] --- maven-source-plugin:2.1.1:jar-no-fork (attach-sources) @ hello-maven ---
[INFO] Building jar: E:\IdeaProjects\hellomaven\target\hello-maven-1.0-sources.jar
有时候,即使不用过phase配置生命周期阶段,插件目标仍能绑定到生命周期中去,这是因为很多插件的目标在编写时已经定义了默认的绑定阶段。可以在 maven官网中查看。
3.5 插件配置
用户可以在Maven命令中使用-D参数,并伴随一个参数键=参数值的裤衩,来配置插件目标的参数。下面的例子就会跳过执行测试,
mvn install -Dmaven.test.skip = true
很多时候,有些参数很少变动,我们可以把它配置在pom文件中
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
五、聚合与继承
- 为什么SpringBoot项目中的pom.xml文件中很少的依赖信息,却发现其实依赖了一些未声明的信息?
- 那么多依赖冲突就没有什么管理办法吗?
1. 聚合
一个 项目往往不是单一的模块,比如一个Web项目可能分为Service、Bean和Repositroy等,我们可以方便的使用Maven来管理模块之间的依赖和聚合关系。
<modules>
<module>bean</module>
</modules>
在bean中
<parent>
<artifactId>maven-integration</artifactId>
<groupId>com.hito</groupId>
<version>0.0.1-SNAPSHOT</version>
<!--如果父模块不在上一层目录,可以通过此配置设置-->
<relativePath>xxx</relativePath>
</parent>
2. 继承
2.1 哪些属性是可以被继承的呢
- groupId
- version
- description
- distributionManagement:项目的部署管理
- properties:自定义的Maven属性
- dependencies:依赖配置
- dependencyManagement:依赖管理
- build
2.2 依赖管理
通过声明parent,子模块即可使用父模块中的依赖,可是子模块并不需要父模块中的所有依赖,那就需要依赖管理了。
Maven提供的dependencyManagement元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖的灵活性。在dependencyManagement中声明的依赖不会引入实际的依赖,不过能够约束dependencies下的依赖使用。
<dependencyManagement>
<!--在这里声明的依赖不会被引入-->
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>2.2.0.RC2</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--真正引入,子模块也可以用这种方式引入,统一控制版本信息-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
</dependency>
</dependencies>
build元素下的pluginManagement有同样的功能,子模块可灵活继承父模块中声明的插件。
2.3 super pom
任何一个项目都隐式的继承一个超级pom文件,该文件在MAVEN_HOME/lib/maven-model-builder-x.x.x.jar中,这就是为什么一个只声明proupID和artifactId的maven工程能够运行的原因。
<project>
<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>
<build>
<directory>${project.basedir}/target</directory>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<finalName>${project.artifactId}-${project.version}</finalName>
<testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
</testResources>
<pluginManagement>
<!-- NOTE: These plugins will be removed from future versions of the super POM -->
<!-- They are kept for the moment as they are very unlikely to conflict with lifecycle mappings (MNG-4453) -->
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-5</version>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.8</version>
</plugin>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>2.5.3</version>
</plugin>
</plugins>
</pluginManagement>
</build>
....
</project>
3. 反应堆
一个多模块的项目中,反应堆指所有模块的一个构建结构。
3.1 构建顺序
一般来说,构建顺序是按照pom中声明的顺序自上而下的,但是如果子模块中间存在依赖关系的话,会优先构建依赖的模块。所以mavne的依赖结构是一个有向的非循环图(DAG)
3.2 剪裁反应堆
有时候,我只关心某一个或某些模块的构建,这时候就需要对反应堆进行剪裁。mvn命令后追加参数即可完成
- -am , --also-make构建所列模块的依赖
- -amd,同时构建依赖于所列模块的模块
- -pl, --projects
构建指定的模块 - -rf, -resume-from
从指定的模块恢复反应堆
例子
mvn clean install -pl account-email,account-persist
,只构建声明的模块mvn clean install -pl account-email,account-persist -am
构建声明的模块及其依赖mvn clean install -rf account-email
,从account-email开始构建