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 测试代码

  1. 引入依赖坐标

    <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
     </dependency>
  2. 测试代码编写

    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");
        }
    }
  3. 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,我们应该怎么处理冲突呢?

    Maven实战 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 优化依赖

可以运用如下的命令查看解析当前项目的依赖:

  1. 依赖列表 mvn dependency:list

  2. 查看依赖树 mvn dependency:tree

  3. 依赖分析 mvn dependency:analyze

    这个结果中有两个重要的部分:

    • Used undeclared dependencies:项目中使用了未显示声明的依赖,即在代码中使用了由直接依赖传递的依赖,这样在升级直接依赖的时候不易发现版本变化,这是一种隐藏的威胁。
    • Unused declared dependencies:明明未使用,但是显示声明了的依赖。注意,不应该将此类依赖简单删除,因为analyze只会分析编译代码和测试代码,运行时需要的依赖它是无法发现的。所以我们应该仔细分析后再处理。

四、生命周期和插件

  1. clean build compile install 是什么?怎么来的?
  2. maven为什么能够做到这些?

1. 何为生命周期

在Maven出现之前,项目的生命周期就已经存在了,Developer对项目进行清理、编译、测试和部署,Maven对所有的构建过程进行了抽象和统一。 Maven的生命周期是抽象的,这就意味着生命周期本身不做任何实际的工作,而是由插件来完成。如同Maven声明抽象方法,而具体实现由插件完成。

2. 生命周期详情

2.1 三套生命周期

一开始以为Maven的生命周期为一个整体,其实不然,Maven拥有三套相互独立的生命周期:

  1. clean:清理项目,包含三个阶段
    • pre-clean
    • clean
    • post-clean
  2. 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项目使用
  3. 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-clean
    maven-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>

五、聚合与继承

  1. 为什么SpringBoot项目中的pom.xml文件中很少的依赖信息,却发现其实依赖了一些未声明的信息?
  2. 那么多依赖冲突就没有什么管理办法吗?

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 从指定的模块恢复反应堆

例子

  1. mvn clean install -pl account-email,account-persist,只构建声明的模块
  2. mvn clean install -pl account-email,account-persist -am构建声明的模块及其依赖
  3. mvn clean install -rf account-email,从account-email开始构建
上一篇:makefile实验二 对目标的深入理解 以及rebuild build clean的实现


下一篇:Maven安装以及Idea安装