基本介绍

Maven是什么:一款用于Java项目的项目管理和自动构建的Apache基金会开源工具,可以管理项目之间的各种关系,包括依赖、继承、聚合关系。Maven关注两个方面:软件的构建过程和软件依赖关系。使用xml文件来描述一个软件的基本信息、依赖项、构建顺序、目录、以及需要的插件。

Maven怎么来的:ANT项目管理工具的改进版,贯彻coc(约定大于配置)思想,将一些常用的构建步骤预定义给用户使用(如compile、package、test等)。

怎么安装和使用。如果使用IDEA,则IDEA内置了Maven,不需要再额外安装。可以在设置中找到内部绑定的版本,如图所示。

image-20230425112316703

也可以自己下载最新版本的maven,官网地址:https://maven.apache.org/download.cgi,解压后设置环境变量即可。要在IDEA中使用,只要在IDEA的设置中设置自定义maven的路径就可以使用了。Maven的安装目录有如下结构:

  • bin 命令行可执行文件
  • conf 其中的settings.xml是核心(全局)配置文件。最开始没有.m2目录,要第一次运行过mvn命令才创建。可以运行mvn help:system。有了.m2目录之后,把settings.xml拷贝到.m2即可。
  • boot maven启动需要的jar包。

Maven项目标识

在Maven中,通常通过groupId, artifactId, version唯一标识一个项目。groupId类似namespace,一般是公司名称或者个人域名。artifactId可以理解为项目名,version表示项目版本,给予项目时间上的精确指定。

一个典型的项目标识如下:

<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>17.0.0</version>

Maven项目结构

大部分maven项目组织源代码都会包含一些Maven项目固定的结构:

  • src-main
    • java 源代码文件
    • resources 用到的yaml xml等资源文件
  • src-test
    • java 测试源代码
    • resources 测试用到的yaml xml等
  • target 编译后内容所在的文件

根目录中最重要的文件是pom.xml,是对项目的描述,称为项目对象模型。其中包含的信息有:

  • 项目的名称和版本
  • 项目如何构建(包括使用的插件)
  • 项目和其他项目的关系:聚合、继承、依赖
  • 属性(变量)定义
  • 管理信息(如可以约束统一的依赖版本)

Maven项目分类

Maven项目有三种类型:

  • pom项目。逻辑项目,用在父项目或者聚合项目,里面不写代码。用来做jar包版本的控制,工程聚合。用<packaging> pom </packaging>表示这是一个pom项目。
  • jar项目。打包成jar包。
  • war项目。打包成war包。

Maven项目关系

Maven项目管理的核心就在于可以管理多个项目之间的关系。

Maven项目的关系有:

  1. 依赖:项目A中需要导入项目B的jar包,则A依赖于B。在A的pom中需要将B的标识加入dependencies中。
  2. 继承:项目A是B的子项目,会继承B的pom文件,称为A继承B。在A的pom中需要将B的标识加入到parent标签中。
  3. 聚合:项目A是B的父项目,要求构建A的时候会构建B和所有其他子模块,称为A聚合了B。在A的pom中将B的名称加入到modules中即可。

项目依赖关系规则:

  1. 依赖传递:如果B依赖C,A依赖B,则A也依赖于C。
  2. 最短路径优先原则:A->C(2.0) A->B->C(1.0),则还是C(2.0)会被使用。
  3. 最先声明原则:如果依赖的两个包版本冲突,且路径长度一样,则选择先写的那个里的。
  4. 依赖排除:一个dependency下如果配了exclusion,则exclusion中的依赖不会传递过来。
  5. 依赖范围:scope,常用的compile(默认,表示编译和运行时都生效),runtime(只有运行时生效,如JDBC驱动)、provided(不会打到jar包里)、system(通过systemPath手动从文件导入依赖)、test(测试代码中才使用)、import(父工程中dependencyManagement中使用,有import之后子工程必须使用这个版本的依赖)

Tip: 在IDEA中可以在Maven面板中查看每一个项目的依赖项image-20230425115123809

Maven仓库

分为本地仓库和远程仓库。使用jar包时,如果本地仓库没有,maven会自动到远程仓库中下载依赖,下载到本地仓库,然后再使用。

远程仓库:不在本机的一切仓库都是远程仓库,分为 中央仓库 和私服仓库。

  • 中央仓库:mvnrepository.com
  • 私服仓库:自己搭建的仓库

本地仓库用来缓存远程仓库,还有自己部署的一些构件。setting.xml中的localRepository位置配置。

镜像仓库:可以指定给某个远程仓库做镜像的仓库。mirror标签指定。

仓库优先级:

  1. 先在本地仓库中查找
  2. 然后在setting.xml中指定的仓库查找
  3. 镜像仓库
  4. 中央仓库

Settings.xml

这个文件是Maven的全局配置文件,通常位于~/.m2目录下。包含的配置内容通常有:

  • 本地仓库地址(目录)。 <localRepository>/path/to/custom/repository</localRepository>

  • 镜像仓库地址。写在mirrors标签下。

    <mirror>
        <id>aliyunmaven</id>
        <mirrorOf>Central</mirrorOf>
        <name>阿里云公共仓库</name>
        <url>https://maven.aliyun.com/repository/public</url>
    </mirror>
    
  • 远程仓库。先在servers标签下定义server,然后配置相应的仓库。

    <server>
        <id>ClawsGitea</id>
        <configuration>
            <httpHeaders>
                <property>
                    <name>Authorization</name>
                    <value>token xxx</value>
                </property>
            </httpHeaders>
        </configuration>
    </server>
    
    <repositories>
        <repository>
            <id>ClawsGitea</id>
            <url>https://gitea.claws.top/api/packages/jingjiecb/maven</url>
        </repository>
    </repositories>
    
  • 一些profile定义和激活。profile是一组可选的配置,可以定义在什么条件下激活,激活时可以覆盖掉一些默认配置。后面会详细讲解。

Maven插件

绝大多数Maven的功能都是插件提供的(包括常用的compile、test、package)。而运行插件的方式也很简单:mvn <plugin-name>:<goal-name>。因此,编译动作其实可以用mvn compiler:compile来执行。

当然,平时编译的时候没有用过mvn comiler:compile,因为每次都需要自己逐个调用插件的功能真的是太麻烦了。用户使用的mvn compile命令通常就够用了。原理:compile是maven生命周期的一个阶段,compiler插件的compile goal关联在compile阶段中,执行mvn compile时,会自动执行所有关联的插件的goal,就可以触发编译了。

这些默认的插件不需要显式地写在项目的pom文件中,maven在运行时会自动先把这些插件加上(体现COC约定大于配置)。使用mvn help:effective-pom 命令就可以查看maven生成的最终pom文件是什么样子的。

Maven生命周期

maven的生命周期分为了多个有先后顺序的阶段,默认的阶段序列是:

  1. validate
  2. generate-sources
  3. process-sources
  4. generate-resources
  5. process-resources
  6. compile
  7. process-test-sources
  8. process-test-resources
  9. test-compile
  10. test
  11. package
  12. install
  13. deploy

在指定一个阶段进行执行时,maven会顺序执行这个阶段以及之前的所有阶段中管理的goal。例如,compiler插件的compile这个goal关联在compile阶段,而surefire插件的test插件关联在test阶段。因此如果执行mvn compile则会执行compiler:complie,而如果执行mvn test则会执行compiler:compile和surefire:test。

常用的clean阶段并没有出现在这个默认生命周期中,这是因为并不是需要每次构建的时候都一定要清楚之前的产物,这样做有一点浪费。因此clean被放到了自己专用的生命周期lifecycle中。

Maven常用命令

  • clean 清理上次的构建
  • compile 编译
  • package 编译打jar包
  • install 编译打jar包,安装到本地仓库
  • deploy 编译打包,发布到远程仓库
  • mvn clean package -DskipTests 跳过测试打包

在命令行使用mvn创建项目:

mvn archetype:generate -DgroupId=top.claws -DartifactId=testMavenCmd -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false -DproxySet=true -DproxyHost=localhost -DproxyPort=7890

看起来似乎有点复杂,学习了Maven插件的内容就知道,这里执行了archetype插件的generate这个goal,通过一些环境变量的设置生成了一个项目。前面两个变量指定了要创建项目的groupId和artifactId;中间两个参数描述了使用哪个原型来创建,以及原型的版本号;最后interactiveMode表示是否以交互模式,询问一些配置再创建,如果false则插件将不再询问,直接采用默认的配置创建项目。

Maven常用插件

  1. 编译器插件:方便变更jdk版本。org.apache.maven.plugins maven-compiler-plugin插件。示例版本3.2

  2. 资源拷贝插件:正常情况下resources下的资源会被拷贝到target/classes下,其他地方的不会被拷贝,如果想拷贝其他地方的资源文件,就要使用到资源拷贝插件。注意:一旦自己配置了build 的 resources,maven就不会默认把resources下的文件复制到classes下了,需要自己全部配全。拷贝到classes时,会保留原来的目录层级。

  3. tomcat 插件:一个自己可以手动操控运行的插件。先建一个wer项目,在main/webapp路径下放上一个index.jsp页面,之后在pom.xml中进行插件配置。配置完成后,在右侧的Maven面板的plugins下的tomcat7就能找到run命令,运行run就可以看到服务器跑起来了。

    <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
            <port>8080</port>
            <path>/</path>
        </configuration>
    </plugin>
    

以上是一些简单使用和介绍,接下来会从原理上来解释一下maven各项使用。

Maven Properties

属性,可以理解为变量。通过标签声明变量。例如:

<properties>
    <!--最常见的property之一。这个property会被maven-compiler-plugin使用,来判断源代码的JDK版本-->
    <maven.compiler.source>11</maven.compiler.source> 
    <!--这是一个自定义的名为laji的property,值为yes-->
    <laji>yes</laji>
</properties>

学习这部分时,可以用mvn help:evaluate -Dexpression=laji 类似的命令来求laji这个property的最终的值,方便调试

重复定义变量时,后面的变量会覆盖前面的值。

<properties>
    <!--这是一个自定义的名为laji的property,值为yes-->
    <laji>yes</laji>
    <laji>no</laji>
</properties>
<!--结果:laji = no-->

Maven Profile

profile 是可选的配置,可以通过activation定义激活条件。profile既可以定义在settings.xml中,也可以定义在pom.xml中。

<profiles>
    <profile>
        <!--该profile的名字-->
        <id>haha</id>
        <!--该profile的激活条件-->
        <activation>
            <property>
                <name>env</name>
                <value>test</value>
            </property>
        </activation>
        <!--下面是可选的配置内容-->
        <properties>
            <laji>ohno</laji>
        </properties>
    </profile>
</profiles>

以上profile表示:当env变量为test(也就是执行mvn test)时,laji属性的值会赋为ohno。这是一种按照环境变量进行激活的方式。

可以使用 mvn help:active-profiles 来查看目前激活的profile有哪些。

接下来举例说明各个激活方式。

按照环境变量激活

<profiles>
    <profile>
        <id>haha</id>
        <activation>
            <!--当ok环境变量为true时激活-->
            <property>
                <name>ok</name>
                <value>true</value>
            </property>
        </activation>
        <properties>
            <laji>ohno</laji>
        </properties>
    </profile>
</profiles>

例如,使用mvn verify -Dok=true 命令时会激活上面的profile。

在settings.xml中指定激活的profile id

<activateProfiles>
    <activateProfile>haha</activateProfile>
</activateProfiles>

上述settings会自动激活使用profile haha。

指定默认激活

<profiles>
    <profile>
        <id>haha</id>
        <activation>
            <!--默认情况下激活-->
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <laji>ohno</laji>
        </properties>
    </profile>
</profiles>

上面的profile会默认激活。

根据操作系统类型激活

<profiles>
    <profile>
        <id>haha</id>
        <activation>
            <!--在指定操作系统环境下激活-->
            <os>
                <name>Windows XP</name>
                <family>Windows</family>
                <arch>x86</arch>
                <version>5.1.2600</version>
            </os>
        </activation>
        <properties>
            <laji>ohno</laji>
        </properties>
    </profile>
</profiles>

根据文件存在情况激活

<profiles>
    <profile>
        <id>haha</id>
        <activation>
            <!--存在a或者确实group时激活-->
            <file>
                <exists>a</exists>
                <missing>target/generated-sources/axistools/wsdl2java/com/companyname/group</missing>
            </file>
        </activation>
        <properties>
            <laji>ohno</laji>
        </properties>
    </profile>
</profiles>

注意:如果有多个激活条件同时存在,则任意一个满足的时候profile就会激活。

运行命令时手动激活

运行mvn verify -P haha时,激活haha profile。

运行mvn verify -P !haha时,禁用haha profile(不管之前是否被激活)。

Maven多JDK配置案例

结合IDEA,实战展示如何给不同的模块配置不同的JDK(本次展示的时jdk1.8和jdk11两种),体现maven管理的灵活。

检查环境

命令行运行:确认已经配置了环境变量JAVA_HOME。这个是maven运行前必须的环境变量。因为maven本身也是java开发的工具,必须运行在java虚拟机上,所以需要先安装好java才能使用。这里使用的是jdk17版本。需要注意的是,由于javac向后兼容,因此jdk17中的javac可以构建jdk8(52)以及java17(55)版本的class文件。

有些时候遇到有些maven插件必须运行在指定版本以上的jre中,就要保证这里的JAVA_HOME给出的是符合要求的jdk。这里的JAVA_HOME版本是maven运行的版本。如果这里使用的jdk版本过低,后面就没救了。

对应在IDEA里面,要在Project Structure中设置对应版本的Project SDK,如下图所示。

image-20230425202640420

配置pom.xml

新建两个modules: part1-8和part2-11,分别计划用jdk8和jdk11进行编写和编译。

在part1的pom中声明:

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

在part2的pom中声明:

<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

之后右键找到reload project,如图:

image-20230425203231558

再看project Structure,发现两个module的Language Level被自动更新了。说明配置起作用了。

image-20230425203331993

编译文件 查看结果

点击右侧maven面板中的compile命令,进行编译。编译完成后来到左侧导航,发现多了target目录。找到classes对应的编译出的class文件,打开可以看到版本。发现part2模块编译确实得到了55(对应java11的版本),而part1模块编译得到了52(对应java8)的class文件版本。

image-20230425203529594

原因maven.compiler.source和maven.compiler.target两个属性的值会被maven-compiler-plugin读取使用,给每个模块构建对应要求的版本的class文件。

JDK版本错误系列问题

首先明确一点:编译时jdk版本和运行时jre版本独立。高版本的JDK可以编译和引用低版本模块,高版本JRE可以运行低版本的模块。一个模块编写的时候使用的jdk版本,和最终系统运行时使用的jdk版本并不一定要相同。例如模块可以使用jdk1.8编写,被系统的其他模块依赖,最终系统可以用jdk11运行,并不矛盾。但是要注意的是,最终运行时,在类加载阶段会检查加载进来的类的版本,如果jre是1.8版本的,但是依赖的一个类(要被加载的类)是55版本(对应java11),则会报Runtime Exception。

因此:

  • 使用Maven等工具构建时,maven构建时环境变量JAVA_HOME指定的jdk版本>=所以使用到的模块的jdk版本,就可以构建成功。否则构建会报错,形如:类文件具有错误的版本 55.0, 应为 52.0
  • 运行时,运行环境使用的jvm版本又要>=所有模块使用的jdk版本就行。否则,类加载时会抛出异常java.lang.UnsupportedClassVersionError

对应在IDEA里面,在Project Structure中指定的模块的Dependencies中的module sdk就是构建时使用的jdk(如图1);而Run configuration中指定的时运行时的jdk版本(如图2),两者相互独立。如果构建jdk版本低,会出现构建时类文件具有错误的版本 55.0, 应为 52.0这样的错误;如果运行时jdk版本低,则会出现java.lang.UnsupportedClassVersionError。

构建时JDK版本配置

运行时JDK版本配置

所以,遇到类文件错误版本的构建报错,则应该升级本地JAVA_HOME中的版本,或者对应更换IDEA的模块JDK;遇到java.lang.UnsupportedClassVersionError则应该提高运行使用的JRE版本,或者对应修改IDEA中Run Configuration的版本(通常这个IDEA会自动使用合适的,因此这类错误比较少见)。并不需要真正更改项目的依赖

其他技巧

使用代理

mvn clean package -DproxySet=true -DproxyHost=localhost -DproxyPort=7890