首页 > 代码库 > Maven实战读书笔记(9)

Maven实战读书笔记(9)

现代软件的现状

1、在这个技术飞速发展的时代,各类用户对软件的要求越来越高,软件本身也变得越来越复杂

2、因此,软件设计人员往往会采用各种方式对软件划分模块,以得到更清晰的设计及更高的重用性

3、当把Maven应用到实际项目中的时候,也需要将项目分成不同的模块

 

举个例子

比如,账户注册服务就可以划分成account-emailaccount-persist5个模块

 

Maven的聚合和继承特性

Maven的聚合特性能够把项目的各个模块聚合在一起构建

Maven的继承特性则能帮助抽象各个模块相同的依赖和插件等配置,在简化POM的同时,还能促进各个模块配置的一致性,本章将结合实际的案例阐述Maven的这两个特性

 

account-persist模块的作用?

该模块负责账户数据的持久化,以XML文件的形式保存账户数据,并支持账户的创建、读取、更新、删除等操作

 

maven-resources-plugin插件

使用UTF-8编码处理资源文件

 

为什么Maven要有聚合的功能?

比如,你一个项目有两个模块,如果没有聚合的话,你需要在两个模块的目录下分别执行mvn命令,如果有十个模块,那会疯掉

有了聚合就可以只执行一次mvn命令,每个模块会自动执行

 

如何配置聚合?

1、首先我们有两个模块分别是account-emailaccount-persist,要把它们两个实现聚合

2、创建一个account-aggregator模块,通过该模块构建整个项目的所有模块

 

account-aggregator模块配置如下

<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/maven/-v4_0_0.xsd">

       <modelVersion>4.0.0</modelVersion>

       <groupId>com.juvenxu.mvnbook.account</groupId>

       <artifactId>account-aggregator</artifactId>

       <version>1.0.0-SNAPSHOT</version>

       <packaging>pom</packaging>

       <name>Account Aggregator</name>

       <modules>

              <module>account-email</module>

              <module>account-persist</module>

       </modules>

</project>

对以上配置进行解释:

1、有共同的groupId——com.juvenxu.mvnbook.account

2artifactId为独立的——account-aggregator

3、版本号与其他的两个模块一致

4packaging其值被设置成POM,而其他两个模块都用了默认值jar

5、对于聚合模块来说,其打包方式packaging的值必须为pom,否则就无法构建

6modules元素是实现模块的聚合最核心的配置,module里面配置是当前目录的相对路径

 

一般模块聚合的通用约定

1、为了方便用户构建项目,通常将聚合模块放在项目目录的最顶层,其他模块则作为聚合模块的子目录存在

2、这样当用户得到源码的时候,第一眼发现的就是聚合模块的pom不用从多个模块中去寻找聚合模块来构建整个项目

3、聚合模块只有一个pom.xml文件,没有src/main/javasrc/test/java等目录,这也很容易理解,聚合模块仅仅是帮助聚合其他模块构建的工具,它本身并无实质的内容

注意:聚合模块与其他目录结构并非一定要是父子关系,平行目录结构也是可以的

 

Maven的继承机制

1、大量的前人经验告诉我们,重复往往就意味着更多的劳动和更多潜在的问题

2、在面向对象世界中,程序员可以使用类继承在一定程度上消除重复

3、在Maven的世界中,也有类似的机制能让我们抽出重复的配置,这就是POM的继承

 

如何配置Maven的继承?

1、比如在account-aggregator聚合模块下,创建一个名为account-parent的子目录,然后再该子目录下建立一个所有除account-aggregator之外的模块的父模块

2、在该子目录下创建一个pom.xml文件

 

account-parentPOM配置如下

<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/maven/-v4_0_0.xsd">

       <modelVersion>4.0.0</modelVersion>

       <groupId>com.juvenxu.mvnbook.account</groupId>

       <artifactId>account-parent</artifactId>

       <version>1.0.0-SNAPSHOT</version>

       <packaging>pom</packaging>

       <name>Account Parent</name>

</project>

对上面配置进行说明:

1groupIdversion和其他模块一致

2artifactId为——account-parent

3packagingpom,这一点与聚合模块一样,作为父模块的POM,其打包类型也必须为pom

4、父模块只有pom.xml,没有src/main/java之类的文件夹

 

有了父模块那么子模块如何配置?

修改account-email继承account-parent

<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/maven/-v4_0_0.xsd">

       <modelVersion>4.0.0</modelVersion>

      

       <parent>

              <groupId>com.juvenxu.mvnbook.account</groupId>

              <artifactId>account-parent</artifactId>

              <version>1.0.0-SNAPSHOT</version>

              <relativePath>../account-parent/pom.xml</relativePath>

       </parent>

      

       <artifactId>account-email</artifactId>

       <name>Account Email</name>

 

       <dependencies>

       ...

       </dependencies>

      

       <build>

              <plugins>

              ...

              </plugins>

       </build>

</project>

对上述配置进行解释:

1、上述POM中使用parent元素声明父模块,parent下的子元素groupIdartifactIdversion指定了父模块的坐标,这三个元素是必须的

2、元素relativePath表示父模块POM的相对路径,../account-parent/pom.xml

3、当项目构建时,Maven会首先根据relativePath检查父POM,如果找不到,再从本地仓库查找,relative的默认值是../pom.xml,也就是说,Maven默认父POM在上一层目录

4、这个POM没有为account-email声明groupIdversion,因为其隐式地从父模块继承了这两个元素,这也就消除了一些不必要的配置,但是对于artifactId子模块需要显式声明的

5account-persist模块的配置类似

6、最后还需要把account-parent加入到聚合模块account-aggregator中,比如修改成下面这样的配置:

       <modules>

              <module>account-parent</module>

              <module>account-email</module>

              <module>account-persist</module>

       </modules>

 

还有哪些POM元素可以被继承呢?

是的,除了groupId version等可以继承,还有以下的一些元素可以被继承

1groupId:项目组ID,项目坐标的核心元素

2version:项目版本,项目坐标的核心元素

3description:项目的描述信息

4organization:项目的组织信息

5inceptionYear:项目的创始年份

6url:项目的URL地址

7developers:项目的开发者信息

8contributors:项目的贡献者信息

9distributionManagement:项目的部署配置

10issueManagement:项目的缺陷跟踪系统信息

11ciManagement:项目的持续集成系统信息

12scm:项目的版本控制系统信息

13mailingLists:项目的邮件列表信息

14properties:自定义的Maven属性

15dependencies:项目的依赖配置

16dependencyManagement:项目的依赖管理配置

17repositories:项目的仓库配置

18build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等

19reporting:包括项目的报告输出目录配置、报告插件配置等

 

如何控制依赖的继承?

1Maven提供的dependenyManagement元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性

2、在denpendenyManagement元素下的依赖声明不会引入实际的依赖,不过它能够约束dependencies下的依赖使用


account-parent中配置depencyManagement元素

<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/maven-v4_0_0.xsd">

       <modelVersion>4.0.0</modelVersion>

       <groupId>com.juvenxu.mvnbook.account</groupId>

       <artifactId>account-parent</artifactId>

       <version>1.0.0-SNAPSHOT</version>

       <packaging>pom</packaging>

       <name>Account Parent</name>

       <properties>

              <springframework.version>2.5.6</springframework.version>

              <junit.version>4.7</junit.version>

       </properties>

       <dependencyManagement>

              <dependencies>

                     <dependency>

                            <groupId>org.springframework</groupId>

                            <artifactId>spring-core</artifactId>

                            <version>${springframework.version}</version>

                     </dependency>

                     <dependency>

                            <groupId>org.springframework</groupId>

                            <artifactId>spring-beans</artifactId>

                            <version>${springframework.version}</version>

                     </dependency>

                     <dependency>

                            <groupId>org.springframework</groupId>

                            <artifactId>spring-context</artifactId>

                            <version>${springframework.version}</version>

                     </dependency>

                     <dependency>

                            <groupId>org.springframework</groupId>

                            <artifactId>spring-context-support</artifactId>

                            <version>${springframework.version}</version>

               </dependency>

                     <dependency>

                            <groupId>junit</groupId>

                            <artifactId>junit</artifactId>

                            <version>${junit.version}</version>

                            <scope>test</scope>

                     </dependency>

              </dependencies>

       </dependencyManagement>

</project>

这里使用dependencyManagement声明的依赖既不会给account-parent引入依赖,也不会给它的子模块引入依赖,不过这段配置是会被继承的

 

继承了dependencyManagementaccount-email POM

       <properties>

              <javax.mail.version>1.4.1</javax.mail.version>

              <greenmail.version>1.3.1b</greenmail.version>

       </properties>

 

       <dependencies>

              <dependency>

                     <groupId>org.springframework</groupId>

                     <artifactId>spring-core</artifactId>           

              </dependency>

              <dependency>

                     <groupId>org.springframework</groupId>

                     <artifactId>spring-beans</artifactId>                

              </dependency>

              <dependency>

                     <groupId>org.springframework</groupId>

                     <artifactId>spring-context</artifactId>              

              </dependency>

              <dependency>

                     <groupId>org.springframework</groupId>

                     <artifactId>spring-context-support</artifactId>                

              </dependency>

              <dependency>

                     <groupId>junit</groupId>

                     <artifactId>junit</artifactId>

              </dependency>

              <dependency>

                     <groupId>javax.mail</groupId>

                     <artifactId>mail</artifactId>

                     <version>${javax.mail.version}</version>

              </dependency>             

              <dependency>

                     <groupId>com.icegreen</groupId>

                     <artifactId>greenmail</artifactId>

                     <version>${greenmail.version}</version>

                     <scope>test</scope>

              </dependency>

       </dependencies>

对上述配置进行解释:

1、所有的springframework依赖只配置了groupIdartifactId,省去了version

2junit依赖不仅省去了version,还省去了依赖范围scope

 

配置继承的好处?

1、使用这种依赖管理机制似乎不能减少太多的POM配置

2、不过还是强烈推荐采用这种方法,其主要原因在于在父POM中使用dependencyManagement声明依赖能够统一项目范围中的依赖的版本

3、当依赖版本在父POM中声明之后,子模块在使用依赖的时候就无需声明版本,也就不会发生多个子模块使用依赖版本不一致的情况

4、如果子模块不声明依赖的使用,即使该依赖已经在父POMdependencyManagement中声明了,也不会产生任何实际的效果,也就是说在子pom不声明依赖,那么该依赖就不会被引入。这正是dependencyManagement的灵活性所在

 

关于import的依赖范围

1import的依赖范围只在dependencyManagement元素下才有效果

2、使用该范围的依赖通常指向一个POM,作用是将目标POM中的dependencyManagement配置导入并合并到当前POMdependencyManagement元素中

3、也就是说,除了复制配置或者继承两种方式之外,还可以使用import范围依赖将这一配置导入(dependencyManagement的配置)

 

使用import范围依赖导入依赖管理配置

<dependencyManagement>

       <dependencies>

              <dependency>

                     <groupId>com.juvenxu.mvnbook.account</groupId>

                     <artifactId>account-parent</artifactId>

                     <version>1.0.0-SNAPSHOT</version>
                     <type>pom</type>

                     <scope>import</scope>

              </dependency>

       </dependencies>

</dependencyManagement>

注意,上述代码中依赖的type值为pomimport范围依赖由于其特殊性,一般都是指向打包类型为pom的模块。如果有多个项目,它们使用的依赖版本都是一致的,则就可以定义一个使用dependencyManagement专门管理依赖的POM,然后再各个项目中导入这些依赖管理配置

 

插件管理

1Maven提供了dependencyManagement元素帮助管理依赖,类似地,Maven也提供了pluginManagement元素帮助管理插件

2、在该元素中配置的依赖不会造成实际的插件调用行为,当POM中配置了真正的plugin元素,并且其groupIdartifactIdpluginManagement中配置的插件匹配时,pluginManagement的配置才会影响实际的插件行为

 

在父POM中配置pluginManagement

<build>

       <pluginManagement>

              <plugins>

                     <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>

              </plugins>

       </pluginManagement>

</build>

当子模块需要生成源码包时,只需要如下简单的配置

 

继承了pluginManagement后的插件配置

<build>

       <plugins>

              <plugin>

                     <groupId>org.apache.maven.plugins</groupId>

                     <artifactId>maven-source-plugin</artifactId>

              </plugin>

       </plugins>

</build>

1、如果子模块不需要使用父模块的pluginManagement配置的插件,可以尽管将其忽略

2、如果子模块需要不同的插件配置,自行覆盖,妥妥的

 

聚合与继承的关系

1、多模块的Maven项目中的聚合与继承其实是两个概念,其目的也是完全不同的

2、前者主要是为了方便快速构建项目,后者主要是为了消除重复配置

3、对于聚合模块来说,它知道有哪些被聚合的模块,但哪些聚合的模块不知道这个聚合模块的存在

4、对于继承关系的父POM来说,它不知道哪些子模块继承了它,但哪些子模块都必须知道自己的父POM是什么

5、如果非要说这两个特性的共同点,那么,聚合POM与继承关系中的父POMpackaging都必须是pom,同时,聚合模块与继承关系中的父模块除了POM之外都没有实际的内容

6可以将聚合模块和集成模块合并

 

约定大于配置的优势?

1、所有程序员都基于HTTP协议开发Web应用,不然互联网会乱成什么样子

2Java成功的重要原因之一是它能屏蔽大部分操作系统的差异

3XML流行的原因之一是所有语言都接受它

 

为什么使用约定而不是自己更灵活的配置呢?

使用约定可以大量减少配置

 

构建简单项目使用的Maven配置文件

<project>

       <modelVersion>4.0.0</modelVersion>

       <groupId>com.juvenxu.mvnbook</groupId>

       <artifactId>my-project</artifactId>

       <version>1.0</version>

</project>

这段配置非常的简洁,但用户需要付出一定的代价,那就是遵循Maven的约定,Maven会假设用户的项目是这样的:

1、源码目录为src/main/java/

2、编译输出目录为target/classes/

3、打包方式为jar

4、包输出目录为target/

遵循约定虽然损失了一定的灵活性,用户不能随意安排目录一结构,但是却能减少配置更重要的是,遵循约定能够帮助用户遵守构建标准

 

如果我不想遵守约定该怎么办?

首先先问自己三遍,你真的需要这么做吗?

如果仅仅是因为喜好,就不要耍个性,个性往往意味着牺牲通用性,意味着增加无谓的复杂度

 

使用Maven自定义源码目录

<project>

       <modelVersion>4.0.0</modelVersion>

       <groupId>com.juvenxu.mvnbook</groupId>

       <artifactId>my-project</artifactId>

       <version>1.0</version>

       <sourceDirectory>src/java</sourceDirectory>

</project>

对上面的配置进行解释:

1、这个例子中将源码目录改成了src/java而不是默认的src/main/java

2、这往往会造成交流问题,习惯Maven的人会奇怪,源代码跑那里去了

3、当这种自定义大量存在的时候,交流成本就会大大提高

4、只有在一些特殊的情况下,这种自定义配置的方式才应该被正确使用以解决实际问题

5、例如你在处理遗留代码,并且没有办法更改原来的目录结构,这个时候就之只能让Maven妥协

 

关于超级POM

1、任何一个Maven项目都隐式地继承自该POM

2、大量超级POM的配置都会被所有Maven项目继承,也是Maven所提倡的

3、在Maven 3中,超级POM在文件MAVEN_HOME/lib/maven-model-builder-x.x.x.jar中的org/apache/maven/model/pom-4.0.0.xml路径下

4、在Maven 2中,超级POM在文件MAVEN_HOME/lib/maven-x.x.x-uber.jar中的org/apache/maven/model/pom-4.0.0.xml目录下,这里的x.x.x表示Maven的具体版本

 

超级POM中关于仓库的定义

<repositories>

       <repository>

              <id>central</id>

              <name>Maven Repository Switchboard</name>

              <url>http://repo1.maven.org/maven2</url>
              <layout>default</layout>

              <snapshots>

                     <enable>false</enable>

              </snapshots>

       </repository>

</repositories>

 

<pluginRepositories>

       <pluginRepository>

              <id>central</id>

              <name>Maven Plugin Repository</name>

              <url>http://repo1.maven.org/maven2</url>

              <layout>default</layout>

              <snapshots>

                     <enabled>false</enabled>

              </snapshots>

              <releases>

                     <updatePolicy>never</updatePolicy>

              </releases>

       </pluginRepository>

</pluginRepositories>

 

超级POM中关于项目结构的定义

<build>

       <directory>${project.basedir}/target</directory>

       <outputDirectory>${project.build.directory}/classes</outputDirectory>

       <finalName>${project.artifactId}-${project.version}</finalName>

       <testOutputDirectory>${project.build.directory}</testOutputDirectory>

       <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>

       <scriptSourceDirectory>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>

</build>

对上面代码进行解释:

1、这里依次定义了,项目的主输出目录、主代码输出目录、最终构件的名称格式、测试代码输出目录、主源码目录、脚本源码目录、测试源码目录、主源码目录和测试资源目录

2、这就是Maven项目结构的约定

 

超级POM中关于插件版本的定义
<pluginManagement>

       <plugins>

              <plugin>

                     <artifactId>maven-antrun-plugin</artifactId>

                     <version>1.3</version>

              </plugin>

              <plugin>

                     <artifactId>maven-assembly-plugin</artifactId>

                     <version>2.2-beta-4</version>

              </plugin>

 

              <plugin>

                     <artifactId>maven-clean-plugin</artifactId>

                     <version>2.3</version>

              </plugin>

 

              <plugin>

                     <artifactId>maven-compiler-plugin</artifactId>

                     <version>2.0.2</version>

              </plugin>

              ...

       </plugins>

</pluginManagement>

 

什么是反应堆?

1、在一个多模块的Maven项目中,反应堆(Reactor)是指所有模块组成的一个构建结构

2、对于单模块的项目,反应堆就是该模块本身,但对于多模块项目来说,反应堆就包含了各模块之间继承与依赖的关系,从而能够自动计算出合理的模块构建顺序

 

反应堆的构建顺序

比如account-aggregator的聚合配置如下

       <modules>

              <module>account-email</module>

              <module>account-persist</module>

              <module>account-parent</module>

       </modules>

然后开始构建account-aggregator会看到如下的输出

[INFO] ------------------------------------------------------------------------

[INFO] Reactor Build Order:

[INFO]

[INFO] Account Aggregator

[INFO] Account Parent

[INFO] Account Email

[INFO] Account Persist

[INFO] ------------------------------------------------------------------------

上述输出依次为account-aggregatoraccount-parentaccount-emailaccount-persist

我们看到输出顺序不是按modules元素中定义的顺序

 

那么,实际的构建顺序是按什么规则呢?

实际的构建顺序是这样形成的:Maven按序读取POM,如果该POM没有依赖模块,那么就构建该模块,否则就先构建其依赖模块,如果该依赖还依赖于其他模块,则进一步先构建依赖的依赖

 

有向非循环图 (Directed Acyclic Graph, DAG)

模块间的依赖关系会将反应堆构成一个有向非循环图 (Directed Acyclic Graph, DAG)

各个模块是该图的节点,依赖关系构成了有向边。这个图不允许出现循环,因此,当出现模块A依赖于B,而B又依赖于A的情况时,Maven就会报错

 

什么是裁剪反应堆?

一般来说,用户会选择构建整个项目或者选择构建单个模块,但有些时候,用户会想要仅仅构建完整反应堆中的某些个模块,换句话说,用户需要实时地裁剪反应堆。

 

通过命令行选项支持裁剪反应堆

输入mvn -h 可以看到这些选项:

n         -am--also-make 同时构建所列模块的依赖模块

n         -amd-also-make-dependents 同时构建依赖于所列模块的模块

n         -pl--projects <arg> 构建指定的模块,模块间用逗号分隔

n         -rf -resume-from <arg> 从指定的模块回复反应堆

 

使用-pl选项指定构建某几个模块

mvn clean install -pl account-email, account-persist

 

使用-am选项可以同时构建所列模块的依赖模块

mvn clean install -pl account-email -am

构建account-parentaccount-email

 

使用-amd选项可以同时构建依赖于所列模块的模块

mvn clean install -pl account-parent -amd

构建account-parent和其依赖account-emailaccount-persist

 

使用-rf选项可以在完整的反应堆构建顺序基础上指定从哪个模块开始构建

mvn clean install -rf account-email

只会看到account-emailaccount-persist的构建

 

-pl -am 或者-pl -amd的基础上,还能应用-rf参数,以对裁剪后的反应堆再次裁剪

mvn clean install -pl account-parent -amd -rf account-email

 

 


Maven实战读书笔记(9)