目录
前言
之前特地学习过Maven的相关基本知识点,但是当时并未动笔记录,导致很多基本标签的含义根本不记得又无处寻找,只能重新翻看书本,非常的繁琐,特对自己需要的东西做一些简单的入门总结。
Maven快速入门
pom.xml编写
首先创建一个maven工程,然后我们来看下maven的核心即pom.xml。POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何被构建,声明项目依赖等等。如下是我刚新建的hello-world项目的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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qingcha.maven</groupId>
<artifactId>hello-world</artifactId>
<version>1.0-SNAPSHOT</version>
</project>
代码的第一行是XML头,指定了该xml文档的版本和编码方式。紧接着是project元素,project是所有pom.xml的根元素,它还声明了一些pom相关的命名空间及xsd元素,虽然这些属性并不是必须的,但使用这些属性能够让第三方工具(如ide中的xml编辑器)帮助我们快速的编辑pom。
根元素下的第一个子元素modelVersion指定了当前pom模型的版本,对于Maven 2 及 Maven 3 来说,它只能是4.0.0。
最重要的是接下来的三个子元素groupId、artifactId、version,他们共同定义了一个项目基本的坐标,在Maven的世界里,任何的jar、pom或者war都是以基于这些基本的坐标来进行区分的。
groupId定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联。譬如在googlecode上建立了一个名为myapp的项目,那么groupId就应该是com.googlecode.myapp。
artifactId定义了当前Maven项目在组中唯一的ID,组中不同的子项目(模块)往往分配不同的唯一标识。
version指定了当前项目当前的版本---1.0-SNAPSHOT,SNAPSHOT意为快照,说明该项目还处于开发中,是不稳定的版本。随着项目的发展,version会不断更新,如升级为1.0、1.1-SNAPSHOT、1.1等。
编写项目主代码
项目主代码和测试代码不同,主代码会被打包到最终的构件中(如jar),而测试代码只在测试运行时用到,不会被打包。默认情况下,Maven假设项目主代码位于 src/main/java 目录,只要遵循该约定,无需额外的配置,Maven会自动搜寻该目录找到项目主代码,因此我们需要遵循Maven的约定在该目录下创建我们自己的文件:com/qingcha/maven/helloworld/HelloWorld.java,其内容如下:
package com.qingcha.maven.helloworld;
public class HelloWorld {
public String sayHello() {
return "hello, world";
}
public static void main(String[] args) {
System.out.println(new HelloWorld().sayHello());
}
}
这里需要注意的一点是,项目中Java类的包都应该基于项目的groupId和artifactId,这样更加清晰,更加符合逻辑,也方便搜索构件或者Java类。
代码编写完毕后,使用maven进行编译,在项目根目录下运行命令:mvn clean compile,然后会看到控制台输出如下信息
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.qingcha.maven:hello-world >--------------------
[INFO] Building hello-world 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-world ---
[INFO] Deleting /project/Open-source/helloworld/target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-world ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /project/Open-source/helloworld/target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.050 s
[INFO] Finished at: 2020-01-04T12:46:11+08:00
[INFO] ------------------------------------------------------------------------
从输出信息来看,maven主要做了三个操作:
- clean任务:运用clean插件首先删除target/目录,默认情况下maven构建的所有输出都在target/目录中
- resources任务:由于我们未定义项目资源,此处略过
- compile任务:将项目主代码编译至target/classes目录下
编写测试代码
为了使项目结构清晰,项目主代码与测试代码应该分别位于独立的目录中,上文提到主代码的目录为 src/main/java ,对应的Maven
项目中默认的测试代码目录是 src/test/java ,因此我们也需要遵循约定在该目录下创建我们的测试类。当然为了测试首先还需要引入依赖,在java中一般都是使用junit项目,依赖如下
<?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.qingcha.maven</groupId>
<artifactId>hello-world</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
上述xml中添加了dependencies元素,该元素下可以包含多个dependency元素以声明项目的依赖,这里添加了junit的依赖,由前面知识我们已经知道groupId、artifactId、version三者构成了一个Maven项目最基本的坐标,有了这段声明,Maven就能够自动下载junit-4.12.jar(从Maven的中央仓库进行下载)。
后面还有一个scope元素,scope为依赖范围,若依赖范围为test,则表示该依赖只对测试有效。换句话说,在测试代码中使用junit代码是没有问题的,但是在主代码中使用junit代码就会造成编译错误。如果不声明该元素,默认的范围为compile,表示该依赖对主代码和测试代码都有效。下文会详细介绍
接下来就是创建测试类了,在mac版idea下,将鼠标放至要创建测试类的类名下,然后按 option+enter 可快速创建测试类,测试类简单代码如下:
package com.qingcha.maven.helloworld;
import org.junit.Assert;
import org.junit.Test;
public class HelloWorldTest {
@Test
public void sayHello() {
HelloWorld helloWorld = new HelloWorld();
Assert.assertEquals("hello, world", helloWorld.sayHello());
}
}
然后我们使用Maven执行测试,运行 mvn clean test ,输出信息如下:
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.qingcha.maven:hello-world >--------------------
[INFO] Building hello-world 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-world ---
[INFO] Deleting /project/Open-source/helloworld/target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-world ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /project/Open-source/helloworld/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-world ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /project/Open-source/helloworld/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello-world ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /project/Open-source/helloworld/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-world ---
[INFO] Surefire report directory: /project/Open-source/helloworld/target/surefire-reports
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.qingcha.maven.helloworld.HelloWorldTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.069 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.017 s
[INFO] Finished at: 2020-01-04T13:10:58+08:00
[INFO] ------------------------------------------------------------------------
由最下方的success表明我们的测试类执行通过了,分析上面的输出信息,Maven一共执行了六个任务:
- clean任务:运用clean插件首先删除target/目录,默认情况下maven构建的所有输出都在target/目录中
- resources任务:由于我们未定义项目资源,此处略过
- compile任务:将项目主代码编译至target/classes目录下
- testResources任务:资源未定义,略过
- testCompile任务:测试代码编译
- test任务:执行测试
打包和运行
将项目进行编译、测试之后,下一个重要步骤就是打包(package),当前新建的项目并没有指定打包类型,使用默认打包类型jar,简单的执行命令 mvn clean package 进行打包,可以看到输出
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello-world ---
[INFO] Building jar: /project/Open-source/helloworld/target/hello-world-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.010 s
[INFO] Finished at: 2020-01-04T13:41:38+08:00
[INFO] ------------------------------------------------------------------------
类似的,Maven会在打包之前执行编译、测试等操作,然后在最后执行jar任务即使用jar插件将项目主代码生成一个名为hello-world-1.0-SNAPSHOT.jar的包,该包同样位于/target 目录下。
至此,我们得到了项目的输出,如果有需要的话就可以复制这个jar文件到其他项目的classpath中从而使用HelloWorld类。但是如何才能让其他的Maven项目直接引用这个jar呢?还需要一个安装的步骤,执行 mvn clean install :
[INFO] --- maven-install-plugin:2.4:install (default-install) @ hello-world ---
[INFO] Installing /project/Open-source/helloworld/target/hello-world-1.0-SNAPSHOT.jar to /Users/xujia/Downloads/mydocument/maven/maven-repository/com/qingcha/maven/hello-world/1.0-SNAPSHOT/hello-world-1.0-SNAPSHOT.jar
[INFO] Installing /project/Open-source/helloworld/pom.xml to /Users/xujia/Downloads/mydocument/maven/maven-repository/com/qingcha/maven/hello-world/1.0-SNAPSHOT/hello-world-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.009 s
[INFO] Finished at: 2020-01-04T13:48:19+08:00
[INFO] ------------------------------------------------------------------------
同样的,执行安装命令时也会自动执行编译、测试、打包任务,从输出信息可以看到install任务将项目输出的jar包安装到了Maven本地仓库中,你可以打开相应的文件夹看到HelloWorld项目的pom和jar。之前讲述junit的pom及jar的下载的时候,我们说过只有构件被下载到本地仓库后,才能由所有Maven项目使用,这里是同样的道理,只有将HelloWorld的构件安装到本地仓库后,其他Maven项目才能使用它。
默认打包生成的jar包是不能够直接运行的,因为带有main方法的类信息不会添加到manifest中(打开jar文件中的META-INF/MANIFEST.MF文件,将无法看到Main-Class一行),为了生成可执行的jar文件,我们需要借助maven-shade-plugin插件(有多种方式均可以生成可执行的jar包,有兴趣可自行google),配置该插件如下:
<build>
<!-- 两种方式可以生成可执行jar包 -->
<!--<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.qingcha.maven.helloworld.HelloWorld</mainClass>-->
<!--</transformer>-->
<!--</transformers>-->
<!--</configuration>-->
<!--</execution>-->
<!--</executions>-->
<!--</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<mainClass>com.qingcha.maven.helloworld.HelloWorld</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</build>
此时再运行 mvn clean install ,在/target 目录下将会看到 hello-world-1.0-SNAPSHOT.jar 和 original-hello-world-1.0-SNAPSHOT.jar,前者是带有Main-Class信息的可运行jar,后者是原始的jar,打开 hello-world-1.0-SNAPSHOT.jar 的 META-INF/MANIFEST.MF,可以看到它其中便包含了Main-Class信息,便能确定程序的入口在哪里(解压jar包的命令为:jar xvf hello-world-1.0-SNAPSHOT.jar)
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: xujia
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_172
Main-Class: com.qingcha.maven.helloworld.HelloWorld
最后运行该jar包:java -jar hello-world-1.0-SNAPSHOT.jar,可以看到控制台输出hello, world,完美~
Maven坐标和依赖
坐标详解
Maven坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而一组Maven坐标是通过一些元素定义的,它们是groupId、artifactId、version、packaging、classifier,先给出一组坐标定义,如下:
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
下面详细解释一下各个坐标元素
- groupId:定义当前Maven项目隶属的实际项目。首先,Maven项目和实际项目不一定是一对一的关系。比如springframework这一实际项目,其对于的Maven项目会有很多,如spring-core、spring-context等,因此一个实际项目往往会被划分成很多个模块,就类似于我们上面说的组的概念。其次,groupId不应该对应项目隶属的组织或公司,因为一个组织下会有很多实际项目,如果groupId只定义到组织级别,那么artifactId只能定义Maven项目,那么实际项目这个层将难以定义。最后,groupId的表示方式与Java包名的表示方式类似,通常与域名反向一一对应。上例中,groupId 为 org.sonatype.nexus,org.sonatype表示Sonatype公司建立的一个非盈利性组织,nexus标识Nexus这一实际项目,该groupId与域名 nexus.sonatype.org 对应。
- artifactId:该元素定义实际项目中的一个Maven项目(模块),推荐的做法是使用实际项目名称作为 artifactId 的前缀,如上例中的nexus-indexer。
- version:该元素定义Maven项目当前所处的版本,如上例中 nexus-indexer 的版本是2.0.0。
- packaging:该元素定义Maven项目的打包方式。首先,打包方式通常与所生成构件的文件扩展名对应,如上例中 packaging 为 jar,则最终的文件名为 nexus-indexer-2.0.0.jar。而使用war打包方式的Maven项目,最终生成的构件中会有一个 .war 文件,不过这不是绝对的。其次,打包方式会影响到构建的生命周期,比如jar打包和war打包会使用不同的命令。最后,当不定义 packaging 的时候,Maven会使用值 jar
- classifier:该元素用来帮助定义构建输出的一些附属构件。附属构件与主构件对应,如上例中的主构件是 nexus-indexer-2.0.0.jar,该项目可能还会通过使用一些插件生成 nexus-indexer-2.0.0-javadoc.jar、 nexus-indexer-2.0.0-sources.jar 这样一些附属构件,其包含了java文档和源代码,这时候,javadoc和sources就是这两个附属构件的 classifier。这样,附属构件也就拥有了自己唯一的坐标。注意,不能直接定义项目的classifier,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。
上述5个元素中,groupId、artifactId、version是必须定义的,packaging 是可选的(默认为jar),classifier 是不能直接定义的。
依赖的基本配置
一个依赖声明可以包含如下的一些元素:
<project>
...
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
...
<exclusion>
...
</exclusion>
</exclusions>
</dependency>
...
</dependencies>
...
</project>
根元素project下的 dependencies 可以包含一个或者多个 dependency 元素,以声明一个或多个项目依赖,每个依赖可以包含的元素有:
1、groupId、artifactId和version:依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven根据坐标才能找到所需要的依赖
2、type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值为jar。
3、scope:依赖的范围,比较常用的有三种:
- compile:编译依赖范围,如果没有指定,就会默认使用该依赖范围。使用此依赖范围的Maven依赖,对于编译、测试、运行三种classpath都有效
- test:测试依赖范围,只对于测试classpath有效,在编译主代码或者运行项目的时候无法使用此类依赖
- provided:已提供依赖范围,使用此依赖范围的Maven依赖,对于编译和测试classpath有效,但在运行时无效。典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要Maven重复的引入一遍
- runtime:运行时依赖范围,对于测试和运行时有效,编译时无效
4、optional:标记依赖是否可选,为true的时候表示是可选依赖,如果项目A依赖于项目B,项目B依赖于项目X和Y,B对于X和Y的依赖都是可选依赖,那么X和Y将对A没有任何影响,即不会进行依赖传递。
5、exclusions:用来排除传递性依赖,需要注意的是,声明exclusion的时候只需要groupId和artifactId,而不需要version元素,这是因为只需要groupId和artifactId就能唯一定位依赖图中的某个依赖。
依赖这一块东西很多,对于一个好的项目来说,管理好多个项目之间的依赖也很重要,入门篇就先记录这么点把。
Maven常用命令总结
- mvn clean compile:删除/target目录并编译项目主代码
- mvn clean test:执行全部测试代码
- mvn clean package -D maven.test.skip=true:打包,但是不执行测试用例,也不编译测试用例
- mvn clean package -U -D skipTests:打包并更新快照,不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下。
- mvn clean install:将打完的包部署到本地maven仓库,以供其他项目使用
- mvn clean deploy:将打完的包部署到本地maven仓库和远程maven私服仓库
- mvn dependency:tree:查看依赖树
- mvn dependency:tree -D output=*.txt:查看依赖树并将树输出到文本中