Archive

Posts Tagged ‘Maven’

InfoQ Maven专栏(六)——Gradle,构建工具的未来?

April 7th, 2011 2 comments

Maven面临的挑战

软件行业新旧交替的速度之快往往令人咂舌,不用多少时间,你就会发现曾经大红大紫的技术已经成为了明日黄花,当然,Maven也不会例外。虽然目前它基本上是Java构建的事实标准,但我们也能看到新兴的工具在涌现,比如基于Goovy的Gradle,而去年Hibernate宣布从Maven迁移至Gradle这一事件更是吸引了不少眼球。在此之前,我也听到了不少对Maven的抱怨,包括XML的繁冗,不够灵活,学习曲线陡峭等等。那Gradle是否能够在继承 Maven优点的基础上,克服这些缺点呢?带着这个疑问,我开始阅读Gradle的文档并尝试着将一个基于Maven的项目转成用Gradle构建,本文所要讲述大概就是这样的一个体验。需要注意的是,本文完全是基于Maven的角度来看Gradle的,因此对于Ant用户来说,视角肯定会大有不同。

Gradle初体验

Gradle的安装非常方便,下载ZIP包,解压到本地目录,设置 GRADLE_HOME 环境变量并将 GRADLE_HOME/bin 加到 PATH 环境变量中,安装就完成了。用户可以运行gradle -v命令验证安装,这些初始的步骤和Maven没什么两样。Gradle目前的版本是1.0-milestone-1,根据其Wiki上的Roadmap,在1.0正式版发布之前,还至少会有3个里程碑版本,而1.0的发布日期最快也不会早于6月份。而正是这样一个看起来似乎还不怎么成熟的项目,却有着让很多成熟项目都汗颜的文档,其包括了安装指南、基本教程、以及一份近300页的全面用户指南。这对于用户来说是非常友好的,同时也说明了Gradle的开发者对这个项目非常有信心,要知道编写并维护文档可不是件轻松的工作,对于Gradle这样未来仍可能发生很大变动的项目来说尤为如此。

类似于Maven的pom.xml文件,每个Gradle项目都需要有一个对应的build.gradle文件,该文件定义一些任务(task)来完成构建工作,当然,每个任务是可配置的,任务之间也可以依赖,用户亦能配置缺省任务,就像这样:

defaultTasks 'taskB'

task taskA << {
    println "i'm task A"
}

task taskB << {
    println "i'm task B, and I depend on " + taskA.name
}

taskB.dependsOn taskA

运行命令$ gradle -q之后(参数q让Gradle不要打印错误之外的日志),就能看到如下的预期输出:

i'm task A
i'm task B, and I depend on taskA

这不是和Ant如出一辙么?的确是这样,这种“任务”的概念与用法与Ant及其相似。Ant任务是Gradle世界的第一公民,Gradle对Ant做了很好的集成。除此之外,由于Gradle使用的Grovvy脚本较XML更为灵活,因此,即使我自己不是Ant用户,我也仍然觉得Ant用户会喜欢上Gradle。

依赖管理和集成Maven仓库

我们知道依赖管理、仓库、约定优于配置等概念是Maven的核心内容,抛开其实现是否最优不谈,概念本身没什么问题,并且已经被广泛学习和接受。那Gradle实现了这些优秀概念了么?答案是肯定的。

先看依赖管理,我有一个简单的项目依赖于一些第三方类库包括SpringFramework、JUnit、Kaptcha等等。原来的Maven POM配置大概是这样的(篇幅关系,省略了部分父POM配置):

    <properties>
        <kaptcha.version>2.3</kaptcha.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.google.code.kaptcha</groupId>
            <artifactId>kaptcha</artifactId>
            <version>${kaptcha.version}</version>
            <classifier>jdk15</classifier>
        </dependency>
        <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>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>

然后我将其转换成Gradle脚本,结果是惊人的:

dependencies {
    compile('org.springframework:spring-core:2.5.6')
    compile('org.springframework:spring-beans:2.5.6')
    compile('org.springframework:spring-context:2.5.6')
    compile('com.google.code.kaptcha:kaptcha:2.3:jdk15')
    testCompile('junit:junit:4.7')
}

注意配置从原来的28行缩减至7行!这还不算我省略的一些父POM配置。依赖的groupId、artifactId、 version,scope甚至是classfier,一点都不少。较之于Maven或者Ant的XML配置脚本,Gradle使用的Grovvy脚本杀伤力太大了,爱美之心,人皆有之,相比于七旬老妇松松垮垮的皱纹,大家肯定都喜欢少女紧致的脸蛋,XML就是那老妇的皱纹。

关于Gradle的依赖管理起初我有一点担心,就是它是否有传递性依赖的机制呢?经过文档阅读和实际试验后,这个疑虑打消了,Gradle能够解析现有的Maven POM或者Ivy的XML配置,从而得到传递性依赖的信息,并且引入到当前项目中,这实在是一个聪明的做法。在此基础上,它也支持排除传递性依赖或者干脆关闭传递性依赖,其中第二点是Maven所不具备的特性。

自动化依赖管理的基石是仓库,Maven中央仓库已经成为了Java开发者不可或缺的资源,Gradle既然有依赖管理,那必然也得用到仓库,这当然也包括了Maven中央仓库,就像这样:

repositories {
    mavenLocal()
    mavenCentral()
    mavenRepo urls: "http://repository.sonatype.org/content/groups/forge/"
}

这段代码几乎不用解释,就是在Gradle中配置使用Maven本地仓库、中央仓库、以及自定义地址仓库。在我实际构建项目的时候,能看到终端打印的下载信息,下载后的文件被存储在 USER_HOME/.gradle/cache/ 目录下供项目使用,这种实现的方法与Maven又是及其类似了,可以说Gradle不仅最大限度的继承Maven的很多理念,仓库资源也是直接拿来用。

Gradle项目使用Maven项目生成的资源已经不是个问题了,接着需要反过来考虑,Maven用户是否能够使用 Gradle生成的资源呢?或者更简单点问,Gradle项目生成的构件是否可以发布到Maven仓库中供人使用呢?这一点非常重要,因为如果做不到这一点,你可能就会丢失大量的用户。幸运的是Gradle再次给出了令人满意的答案。使用Gradle的Maven Plugin,用户就可以轻松地将项目构件上传到Maven仓库中:

apply plugin: 'maven'
...
uploadArchives {
    repositories.mavenDeployer {
        repository(url: "http://localhost:8088/nexus/content/repositories/snapshots/") {
            authentication(userName: "admin", password: "admin123")
            pom.groupId = "com.juvenxu"
            pom.artifactId = "account-captcha"
        }
    }
}

在上传的过程中,Gradle能够基于build.gradle生成对应的Maven POM文件,用户可以自行配置POM信息,比如这里的groupId和artifactId,而诸如依赖配置这样的内容,Gradle是会自动帮你进行转换的。由于Maven项目之间依赖交互的直接途径就是仓库,而Gradle既能够使用Maven仓库,也能以Maven的格式将自己的内容发布到仓库中,因此从技术角度来说,即使在一个基于Maven的大环境中,局部使用Gradle也几乎不会是一个问题。

约定优于配置

如同Ant一般,Gradle给了用户足够的自由去定义自己的任务,不过同时Gradle也提供了类似Maven的约定由于配置方式,这是通过Gradle的Java Plugin实现的,从文档上看,Gradle是推荐这种方式的。Java Plugin定义了与Maven完全一致的项目布局:

  • src/main/java
  • src/main/resources
  • src/test/java
  • src/test/resources

区别在于,使用Groovy自定义项目布局更加的方便:

sourceSets {
    main {
        java {
            srcDir 'src/java'
        }
        resources {
            srcDir 'src/resources'
        }
    }
}

Gradle Java Plugin也定义了构建生命周期,包括编译主代码、处理资源、编译测试代码、执行测试、上传归档等等任务:

Figure 1. Gradle的构建生命周期

相对于Maven完全线性的生命周期,Gradle的构建生命周期略微复杂,不过也更为灵活,例如jar这个任务是用来打包的,它不像Maven那样依赖于执行测试的test任务,类似的,从图中可以看到,一个最终的build任务也没有依赖于uploadArchives任务。这个生命周期并没有将用户限制得很死,举个例子,我希望每次build都发布 SNAPSHOT版本到Maven仓库中,而且我只想使用最简单的$ gradle clean build命令,那只需要添加一行任务依赖配置即可:

build.dependsOn 'uploadArchives'

由于Gradle完全是基于灵活的任务模型,因此很多事情包括覆盖现有任务,跳过任务都非常易于实现。而这些事情,在Maven的世界中,实现起来就比较的麻烦,或者说Maven压根就不希望用户这么做。

小结

一番体验下来,Gradle给我最大的感觉是两点。其一是简洁,基于Groovy的紧凑脚本实在让人爱不释手,在表述意图方面也没有什么不清晰的地方。其二是灵活,各种在Maven中难以下手的事情,在Gradle就是小菜一碟,比如修改现有的构建生命周期,几行配置就完成了,同样的事情,在Maven中你必须编写一个插件,那对于一个刚入门的用户来说,没个一两天几乎是不可能完成的任务。

不过即使如此,Gradle在未来能否取代Maven,在我看来也还是个未知数。它的一大障碍就是Grovvy,几乎所有 Java开发者都熟悉XML,可又有几个人了解Groovy呢?学习成本这道坎是很难跨越的,很多人抵制Maven就是因为学起来不容易,你现在让因为一个构建工具学习一门新语言(即使这门语言和Java非常接近),那得到冷淡的回复几乎是必然的事情。Gradle的另外一个问题就是它太灵活了,虽然它支持约定优于配置,不过从本文你也看到了,破坏约定是多么容易的事情。人都喜欢自由,爱自定义,觉得自己的需求是多么的特别,可事实上,从Maven的流行来看,几乎95%以上的情况你不需要自行扩展,如果你这么做了,只会让构建变得难以理解。从这个角度来看,自由是把双刃剑,Gradle给了你足够的自由,约定优于配置只是它的一个选项而已,这初看起来很诱人,却也可能使其重蹈Ant的覆辙。Maven在Ant的基础上引入了依赖管理、仓库以及约定优于配置等概念,是一个很大的进步,不过在我现在看来,Gradle并没有引入新的概念,给我感觉它是一个结合Ant和Maven理念的优秀实现。

如果你了解Groovy,也理解Maven的约定优于配置,那试试Gradle倒也不错,尤其是它几乎能和现有的Maven系统无缝集成,而且你也能享受到简洁带来的极大乐趣。其实说到简洁,也许在不久的将来Maven用户也能直接享受到,Polyglot Maven在这方面已经做了不少工作。本文完全基于Maven的视角介绍Gradle这一构建工具的新秀,不过限于篇幅原因,无法深入Gradle的方方面面,例如Gradle也支持多模块构建,它提供了GUI操作界面,支持Grovvy(理所当然)和Scala项目等等。有兴趣的读者可以自行进一步了解。

本文已经首发于InfoQ中文站,版权所有,原文为《Maven实战(六)——Gradle,构建工具的未来?》如需转载,请务必附带本声明,谢谢。
InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如架构师》等。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2011/04/07/infoq-maven-gradle/

Categories: Maven Tags: , , ,

InfoQ Maven专栏(五)——自动化Web应用集成测试

March 13th, 2011 5 comments

自动化集成测试的角色

本专栏的上一篇文章讲述了Maven与持续集成的一些关系及具体实践,我们都知道,自动化测试是持续集成必不可少的一部分,基本上,没有自动化测试 的持续集成,都很难称之为真正的持续集成。我们希望持续集成能够尽早的暴露问题,但这远非配置一个 Hudson/Jenkins服务器那么简单,只有真正用心编写了较为完整的测试用例,并一直维护它们,持续集成才能孜孜不倦地运行测试并第一时间报告问 题。

自动化测试这个话题很大,本文不想争论测试先行还是后行,这里强调的是测试的自动化,并基于具体的技术(Maven、 JUnit、Jetty等)来介绍一种切实可行的自动化Web应用集成测试方案。当然,自动化测试还包括单元测试、验收测试、性能测试等,在不同的场景 下,它们都能为软件开发带来极大的价值。本文仅限于讨论集成测试,主要是因为笔者觉得这是一个非常重要却常常被忽略的实践。

基于Maven的一般流程

集成测试与单元测试最大的区别是它需要尽可能的测试整个功能及相关环境,对于测试Web应用而言,通常有这么几步:

  1. 启动Web容器
  2. 部署待测试Web应用
  3. 以Web客户端的角色运行测试用例
  4. 停止Web容器

启动Web容器可以有很多方式,例如你可以通过Web容器提供的API采用编程的方式来启动容器,但在Maven的环境下,配置插件显得更简单。如果你了解Maven的生命周期模型, 就可能会想到,我们可以在pre-integration-test阶段启动容器,部署待测试应用,然后在integration-test阶段运行集成 测试用例,最后在post-integrate-test阶段停止容器。也就是说,对于步骤1,2和4我们只须进行一些简单的配置,不必编写额外的代码。 第3步是以黑盒的形式模拟客户端进行测试,需要注意的是,这里通常要求你理解一些基本的HTTP协议知识,例如服务端在什么情况下应该返回HTTP代码 200,什么时候应该返回401错误,以及所支持的Content-Type是什么等等。

至于测试用例该怎么写,除了需要用到一些用来访问Web以及解析响应详细的基础设施工具类之外,其他内容与单元测试大同小异,基本就是准备测试数据、访问服务、验证返回值等等。

一个简单的例子

谈了不少理论,现在该给个具体的例子了,譬如现在有个简单的Servlet,它接受参数a和b,做加法后返回二者之和,如果参数不完整,则返回HTTP 400错误,表示客户端的请求有问题。

public class AddServlet
    extends HttpServlet
{
    @Override
    protected void doGet( HttpServletRequest req, HttpServletResponse resp )
        throws ServletException,
            IOException
    {
        String a = req.getParameter( "a" );
        String b = req.getParameter( "b" );

        if ( a == null || b == null )
        {
            resp.setStatus( 400 );
            return;
        }

        int result = Integer.parseInt( a ) + Integer.parseInt( b );

        resp.setStatus( 200 );
        resp.getWriter().print( result );
    }
}

为了测试这段代码,我们需要一个Web容器,这里暂且使用Jetty,因为目前来说它与Maven集成的相对最好。Jetty提供了一个Jetty Maven Plugin,借助该插件,我们可以随时启动Jetty并部署Maven默认目录布局的Web项目,实现快速开发和测试。这里我们需要的是在pre-integration-test阶段启动Jetty,在post-integrate-test阶段停止容器,对应的POM配置如下:

      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>7.3.0.v20110203</version>
        <configuration>
          <stopPort>9966</stopPort>
          <stopKey>stop-jetty-for-it</stopKey>
        </configuration>
        <executions>
          <execution>
            <id>start-jetty</id>
            <phase>pre-integration-test</phase>
            <goals>
              <goal>run</goal>
            </goals>
            <configuration>
              <daemon>true</daemon>
            </configuration>
          </execution>
          <execution>
            <id>stop-jetty</id>
            <phase>post-integration-test</phase>
            <goals>
              <goal>stop</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

XML代码中第一处configuration是插件的全局配置,stopPort和 stopKey是该插件用来停止Jetty需要用到的TCP端口及消息关键字。接着是两个executation元素,第一个executation将 jetty-maven-plugin的run目标绑定至Maven的pre-integration-test生命周期阶段,表示启动容器,第二个 executation将stop目标绑定至post-integration-test生命周期阶段,表示停止容器。需要注意的是,启动Jetty时我 们需要配置deamon为true,让Jetty在后台运行以免阻塞mvn命令。此外,jetty-maven-plugin的run目标也会自动部署当 前Web项目。

准备好Web容器环境之后,我们接着看一下测试用例代码:

public class AddServletIT
{
    @Test
    public void addWithParametersAndSucceed()
        throws Exception
    {
        HttpClient httpclient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet( "http://localhost:8080/add?a=1&b=2" );
        HttpResponse response = httpclient.execute( httpGet );

        Assert.assertEquals( 200, response.getStatusLine().getStatusCode() );
        Assert.assertEquals( "3", EntityUtils.toString( response.getEntity() ) );
    }

    @Test
    public void addWithoutParameterAndFail()
        throws Exception
    {
        HttpClient httpclient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet( "http://localhost:8080/add" );
        HttpResponse response = httpclient.execute( httpGet );

        Assert.assertEquals( 400, response.getStatusLine().getStatusCode() );
    }
}

为了能够访问应用,这里用到了HttpClient, 两个测试方法都初始化一个HttpClient,然后创建HttpGet对象用来访问Web地址。第一个测试方法顾名思义用来测试成功的场景,它提供参数 a=1和b=2,执行请求后,验证返回结果成功(HTTP状态码200)并且内容为正确的值3。第二个测试方法则用来测试失败的场景,当不提供参数的时 候,服务器应该返回一个HTTP 400错误。该测试类其实是相当粗糙的,例如有硬编码的服务器URL,这里的目的仅仅是通过尽可能简单的代码来展现一个自动化集成测试的实现过程。

上述代码中,测试类的名称为AddServletIT,而不是一般的**Test,IT表示IntegrationTest,这么命名是为了和单元测试区分开来,这样,鉴于Maven默认的测试命名约定,Maven在test生命周期阶段执行单元测试时,就不会涉及集成测试。现在,我们希望Maven在integration-test阶段执行所有以IT结尾命名的测试类,配置Maven Surefire Plugin如下:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.7.2</version>
        <executions>
          <execution>
            <id>run-integration-test</id>
            <phase>integration-test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <includes>
                <include>**/*IT.java</include>
              </includes>
            </configuration>
          </execution>
        </executions>
      </plugin>

通过命名规则和插件配置,我们优雅地分离了单元测试和集成测试,而且我们知道在integration-test阶段,Jetty容器已经启动完成 了。如果你在使用TestNG,那你还可以使用其测试组的特性来分离单元测试和集成测试,Maven Surefire Plugin对其也有着很好的支持

一切就绪了,运行 mvn clean install 以自动运行集成测试,我们可以看到如下的输出片段:

[INFO] --- jetty-maven-plugin:7.3.0.v20110203:run (start-jetty) @ webapp-demo ---
[INFO] Configuring Jetty for project: webapp-demo
[INFO] webAppSourceDirectory /home/juven/git_juven/webapp-demo/src/main/webapp does not exist. Defaulting to /home/juven/git_juven/webapp-demo/src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] Classes = /home/juven/git_juven/webapp-demo/target/classes
[INFO] Context path = /
...
2011-03-06 14:55:15.676:INFO::Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (run-integration-test) @ webapp-demo ---
[INFO] Surefire report directory: /home/juven/git_juven/webapp-demo/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.juvenxu.webapp.demo.AddServletIT
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.344 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- jetty-maven-plugin:7.3.0.v20110203:stop (stop-jetty) @ webapp-demo ---

可以看到jetty-maven-plugin:7.3.0.v20110203:run对应了start-jetty,maven- surefire- plugin:2.7.2:test对应了run-integration-test,jetty-maven- plugin:7.3.0.v20110203:stop对应了stop-jetty,与我们的配置和期望完全一致。此外两个测试也都成功了!

小结

相对于单元测试来说,集成测试更难编写,因为需要准备更多的环境,本文只涉及了Web容器最简单的情形,实际的开发情形中,你可能会遇到数据库,第 三方Web服务,更复杂的容器配置和数据格式等等,这都使得编写集成测试变得让人畏惧。然而反过来考虑,无论如何你都需要测试,虽然这个自动化过程的投入 很大,但收益往往更加可观,这不仅仅是手动测试时间的节省,更重要的是,你无法保证手动测试能被高频率的反复执行,也就无法保证问题能被尽早暴露。

对于Web应用来说,编写集成测试有助于你考虑和设计Web应用对外暴露的接口,这种“开发实现”/“测试审察”之间的角色转换往往能造就更清晰的设计,这也是编写测试最大的好处之一。

Maven用户能够得益于Maven的插件系统,不仅能节省大量的编码,还能得到稳定的工具,Jetty Maven Plugin和Maven Surefire Plugin就是最好的例子。本文只涉及了Jetty,如果读者的环境是Tomcat或者JBoss等其他容器,则需要查阅相关的文档以得到具体的实现细 节,你可能对Tomcat Maven PluginJBoss Maven Plugin、或者Cargo Maven2 Plugin感兴趣。

本文已经首发于InfoQ中文站,版权所有,原文为《Maven实战(五)——自动化Web应用集成测试》如需转载,请务必附带本声明,谢谢。
InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如架构师》等。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2011/03/13/infoq-maven-webapp-integration-test/

InfoQ Maven专栏(四)——基于Maven的持续集成实践

February 4th, 2011 5 comments

Martin的《持续集成》

相信很多读者和我一样,最早接触到持续集成的概念是来自Martin的著名文章《持续集成》,该文最早发布于2000年9月,之后在2006年进行了一次修订,它清晰地解释了持续集成的概念,并总结了10条实践,它们分别为:

  • 只维护一个源码仓库
  • 自动化构建
  • 让构建自行测试
  • 每人每天向主干提交代码
  • 每次提交都应在持续集成机器上构建主干
  • 保持快速的构建
  • 在模拟生产环境中测试
  • 让每个人都能轻易获得最新的可执行文件
  • 每个人都能看到进度
  • 自动化部署

原始文章距今已10年有余,这在软件行业中算是很长的时间了,但我们都能看到Martin总结的这些实践依旧闪耀着光芒,依旧有很多团队在努力实践它们并得到了丰厚的回报,当然也有很多团队因为各种原因拒绝实践持续集成从而无法体会到个中好处。

从这10条实践中我们能找到很多流行开源工具的影子,例如版本控制工具cvs、svn、git,自动化构建工具Maven、Ant,自动化测试框架JUnit、TestNG,以及持续集成服务器CruiseControl和Hudson等等。其实不论你是否实践持续集成,单独使用这其中的很多工具都能发挥极大的价值,持续集成的一大意义在于它引入了一个有效的流程,能让这些工具有机融合,并相互促进。 关于持续集成还有一本获得Jolt大奖的图书,名为《持续集成——软件质量改进和风险降低之道》。但无论是Martin的文章,还是这本图书,都没有阐述使用Maven作为自动化构建工具实施持续集成的细节。本文旨在介绍一些基于Maven实施持续集成的实践,希望这些经验能从具体处帮助到读者。

架设私有Maven仓库

Martin的文章并没有涉及到依赖管理的内容,但在Java的世界中,依赖管理是开发人员不得不面对的问题。无论是外部的开源类库依赖,还是项目内部的模块间依赖,都需要有效地管理。可以说依赖管理是持续集成核心的内容之一。Maven通过其依赖管理机制和随处可用的中央仓库有效地解决了这个问题,用户只需要在POM中声明项目所需要的依赖,Maven就能在构建的时候自动从仓库解析依赖。

不过仅仅这样是不够的,我们知道,持续集成的最大好处在于降低风险,简单地来说就是尽早暴露问题,能让开发人员及早发现并修复,从而降低修复成本。可是,如果每个人都从中央仓库重复下载依赖,这是非常耗时的,集成的反馈周期肯定会延长。我已经无数次听到有人抱怨“Maven在下载整个Internet!”。构建要快!持续集成反馈要快!Maven你不能拖慢这个流程。

幸运的是开源世界有很好的解决方案,只要使用Maven仓库管理器软件如Nexus建立一个私有的Maven仓库,问题就能迎刃而解。原理很简单,这个位于局域网内的Maven仓库能够代理所有外部仓库,从而避免所有人从Internet重复下载依赖文件。这样Maven解析依赖的时候仅限于局域网,构建速度就大大地加快了。例如大家都需要使用junit-4.8.2.jar,当第一个人向私有仓库请求的时候,私有仓库从中央库下载并缓存下来,假设耗时10s,之后其他人需要junit-4.8.2.jar的时候,私有仓库直接使用缓存的文件,这个耗时可能就是1s。如果有100个开发人员使用该文件,那节省的时间就是 100 * 10 – ( 10 + 99 * 1 ) = 891s ,实际情况中依赖的数量可能会是成百上千,那节省的时间就变得非常的可观。

也许有人会说,我也完全可以将项目依赖加入到版本控制中,这一点甚至在《卓有成效的程序员》中都被明确提及,在该书第5章的”DRY版本控制”一节中,Neal Ford有这么一段话:“所有用来构建项目的东西都应该被放入版本控制,包括二进制文件(类库,框架,JAR文件,构建脚本等等)”。作者进一步解释了其目的,这么做能够保证项目不受外部因素影响(如依赖版本变化,甚至丢失),保证构建的稳定,作者也同时提及了一般版本控制工具处理二进制文件的性能问题。抛开这条结论性的实践,仔细考虑其目的,我们就能发现,私有Maven仓库同样能保证构建的稳定,而且能避免版本控制工具处理二进制文件而造成的潜在性能问题。所以,我斗胆说一句,Neal Ford所提的这条实践OUT了!

私有Maven的仓库的意义还不仅限于此,结合自动化部署和Maven的SNAPSHOT机制,它能大大促进项目集成的效率。

在模块化的开发环境中,大家各司其职,专注于自己所负责的模块,持续集成的规则是,在往版本控制提交代码前,需要先保证本地构建没有问题,那一般的做法就是更新所有模块的代码并构建。可是,真的需要构建那些其实你并不怎么关心的模块么?且不谈一旦构建他人代码时出错,你往往会不知所措,这种做法同时也增加了本地构建的时间。

Maven有SNAPSHOT版本的概念,其目的就是让你能够构建一个临时的版本,供团队他人使用,这样他们就不必在代码的层次关心自己的依赖。于是私有Maven仓库就充当了一个中介的作用,而持续集成服务器就多了一个职责,每次它成功构建一个模块,都应该将该模块的SNAPSHOT版本发布到Maven仓库中。现在,大家就不用去构建别人的代码了,Maven能自动帮你从私有仓库解析下载依赖的最新SNAPSHOT(使用mvn命令的-U参数强制更新)。注意,除了持续集成服务器外,任何其他人都不应该发布SNAPSHOT版本到Maven仓库,因为只有持续集成服务器的环境是可信任的,你能在本地成功执行mvn clean install并不代表持续集成服务器上该命令能成功,由于每个人的本地环境各有差异,因此集成的成功与否应当以持续集成服务器为准,而只有集成成功后,SNAPSHOT才可以被部署到私有仓库供他人使用。

鉴于上述的原因分析,我认为在基于Maven的持续集成环境中,再怎么强调私有Maven仓库的重要性都是不为过的。

正确的集成命令

在持续集成服务器上使用怎样的 mvn 命令集成项目,这个问题乍一看答案很显然,不就是 mvn clean install 么?事实上比较好的集成命令会稍微复杂些,下面是一些总结:

  • 不要忘了clean: clean能够保证上一次构建的输出不会影响到本次构建。
  • 使用deploy而不是install: 构建的SNAPSHOT输出应当被自动部署到私有Maven仓库供他人使用,这一点在前面已经详细论述。
  • 使用-U参数: 该参数能强制让Maven检查所有SNAPSHOT依赖更新,确保集成基于最新的状态,如果没有该参数,Maven默认以天为单位检查更新,而持续集成的频率应该比这高很多。
  • 使用-e参数:如果构建出现异常,该参数能让Maven打印完整的stack trace,以方便分析错误原因。
  • 使用-Dmaven.repo.local参数:如果持续集成服务器有很多任务,每个任务都会使用本地仓库,下载依赖至本地仓库,为了避免这种多线程使用本地仓库可能会引起的冲突,可以使用-Dmaven.repo.local=/home/juven/ci/foo-repo/这样的参数为每个任务分配本地仓库。
  • 使用-B参数:该参数表示让Maven使用批处理模式构建项目,能够避免一些需要人工参与交互而造成的挂起状态。

综上,持续集成服务器上的集成命令应该为 mvn clean deploy -B -e -U -Dmaven.repo.local=xxx 。此外,定期清理持续集成服务器的本地Maven仓库也是个很好的习惯,这样可以避免浪费磁盘资源,几乎所有的持续集成服务器软件都支持本地的脚本任务,你可以写一行简单的shell或bat脚本,然后配置以天为单位自动清理仓库。需要注意的是,这么做的前提是你有私有Maven仓库,否则每次都从Internet下载所有依赖会是一场噩梦。

用好Profile

如果不需要考虑各种不同的环境, 而且你的自动测试(包括集成测试)跑得飞快,那你就不用为项目建立多个集成任务。但实际的情况是,集成的时候可能要考虑各种环境,例如开发环境、测试环境、产品环境。而当项目越来越大,测试越来越多,控制构建时间在一个可接受的范围内(例如10分钟)变得越来越不现实。《持续集成——软件质量改进和风险降低之道》中介绍了一种名为分阶段构建(staged build)的解决方案,例如你可以将构建分为两个部分,第一部分包括了编译和单元测试等能够快速结束的任务,第二个部分包括集成测试等耗时较长的任务,只有第一部分成功完成后,才触发第二部分集成。这么做的意义在于让持续集成的反馈尽可能的快。

Maven的Profile机制能够很好的支持分阶段构建。例如,借助Maven Surefire Plugin,你可以统一单元测试命名为**UT,统一集成测试命名为**IT,然后配置Maven Surefire Plugin默认只运行单元测试,然后再编写一个名为integrationTest的Profile,在其中配置Maven Surefire Plugin运行集成测试。然后再以此为基础分阶段构建项目,第一个构建为 mvn clean install -B -e -U ,第二个构建任务为 mvn clean deploy -B -e -U -PintegrationTest 。前一个构建成功后再触发第二个构建,然后才部署至Maven仓库。值得一提的是,Maven Surefire Plugin能够很好支持JUnit 3、JUnit 4和TestNG,你可以按照最适合自己的方式来划分单元测试和集成测试。

另一个常见的分阶段构建案例是生成Maven站点,使用 mvn clean site 生成站点往往比较耗时且耗资源,这样的任务对应的持续集成中的持续审查阶段,该阶段往往不需要很高的集成频率。你会希望每10分钟就检查源代码变更并编译测试,但很少有人会希望每10分钟让系统生成一次测试覆盖率报告、CheckStyle报告等内容,因此合理的做法是使用一个较低的频率,例如每天,这样可以避免无谓的资源消耗,更重要的是,这样不会拖慢本该很快的编译和单元测试等反馈内容。

还有一些情况是系统需要基于不同环境进行集成,这时候就需要用到Maven的属性机制、资源过滤、以及前面提到的Profile。篇幅原因,这里不再展开。

小结

持续集成是敏捷最重要的实践之一,但如何在基于Maven的环境下实践持续集成却鲜有文章详述,本文介绍了一些该主题的最佳实践,包括架设私有仓库、使用正确的集成命令、利用Profile等技术处理分阶段构建等等。本文旨在让广大Maven用户认识到这些实践的存在及重要性,并没有详细解释一些诸如Nexus安装配置、Maven Surefire Plugin配置、或者说Profile配置使用方面的细节,如果你希望看到更细节的介绍,可以参考我的《Maven实战》一书。除了上面的内容之外,该书还详细解释了如何使用Hudson(也许该改称Jenkins了)这一最流行的开源持续集成服务器。当然,如果你有关于Maven和持续集成方面的经验,也请不吝分享。

本文已经首发于InfoQ中文站,版权所有,原文为Maven实战(四)——基于Maven的持续集成实践如需转载,请务必附带本声明,谢谢。
InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如架构师》等。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2011/02/04/infoq-maven-ci-best-practices/

InfoQ Maven专栏(三)——多模块项目的POM重构

January 10th, 2011 5 comments

在本专栏的上一篇文章POM重构之增还是删中,我们讨论了一些简单实用的POM重构技巧,包括重构的前提——持续集成,以及如何通过添加或者删除内容来提高POM的可读性和构建的稳定性。但在实际的项目中,这些技巧还是不够的,特别值得一提的是,实际的Maven项目基本都是多模块的,如果仅仅重构单个POM而不考虑模块之间的关系,那就会造成无谓的重复。本文就讨论一些基于多模块的POM重构技巧。

重复,还是重复

程序员应该有狗一般的嗅觉,要能嗅到重复这一最常见的坏味道,不管重复披着怎样的外衣,一旦发现,都应该毫不留情地彻底地将其干掉。不要因为POM不是产品代码而纵容重复在这里发酵,例如这样一段代码就有重复:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactid>spring-beans</artifactId>
  <version>2.5</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactid>spring-context</artifactId>
  <version>2.5</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactid>spring-core</artifactId>
  <version>2.5</version>
</dependency>

你会在一个项目中使用不同版本的SpringFramework组件么?答案显然是不会。因此这里就没必要重复写三次<version>2.5</version>,使用Maven属性将2.5提取出来如下:

<properties>
  <spring.version>2.5</spring.version>
</properties>
<depencencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactid>spring-beans</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactid>spring-context</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactid>spring-core</artifactId>
    <version>${spring.version}</version>
  </dependency>
</depencencies>

现在2.5只出现在一个地方,虽然代码稍微长了点,但重复消失了,日后升级依赖版本的时候,只需要修改一处,而且也能避免漏掉升级某个依赖。

读者可能已经非常熟悉这个例子了,我这里再啰嗦一遍是为了给后面做铺垫,多模块POM重构的目的和该例一样,也是为了消除重复,模块越多,潜在的重复就越多,重构就越有必要。

消除多模块依赖配置重复

考虑这样一个不大不小的项目,它有10多个Maven模块,这些模块分工明确,各司其职,相互之间耦合度比较小,这样大家就能够专注在自己的模块中进行开发而不用过多考虑他人对自己的影响。(好吧,我承认这是比较理想的情况)那我开始对模块A进行编码了,首先就需要引入一些常见的依赖如JUnit、Log4j等等:

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
    <version>4.8.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
    <version>1.2.16</version>
  </dependency>

我的同事在开发模块B,他也要用JUnit和Log4j(我们开会讨论过了,统一单元测试框架为JUnit而不是TestNG,统一日志实现为Log4j而不是JUL,为什么做这个决定就不解释了,总之就这么定了)。同事就写了如下依赖配置:

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
    <version>3.8.2</version>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
    <version>1.2.9</version>
  </dependency>

看出什么问题来没有?对的,他漏了JUnit依赖的scope,那是因为他不熟悉Maven。还有什么问题?对,版本!虽然他和我一样都依赖了JUnit及Log4j,但版本不一致啊。我们开会讨论没有细化到具体用什么版本,但如果一个项目同时依赖某个类库的多个版本,那是十分危险的!OK,现在只是两个模块的两个依赖,手动修复一下没什么问题,但如果是10个模块,每个模块10个依赖或者更多呢?看来这真是一个泥潭,一旦陷进去就难以收拾了。

好在Maven提供了优雅的解决办法,使用继承机制以及dependencyManagement元素就能解决这个问题。注意,是dependencyMananget而非dependencies。也许你已经想到在父模块中配置dependencies,那样所有子模块都自动继承,不仅达到了依赖一致的目的,还省掉了大段代码,但这么做是有问题的,例如你将模块C的依赖spring-aop提取到了父模块中,但模块A和B虽然不需要spring-aop,但也直接继承了。dependencyManagement就没有这样的问题,dependencyManagement只会影响现有依赖的配置,但不会引入依赖。例如我们可以在父模块中配置如下:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactid>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactid>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>
  </dependencies>
</dependencyManagement>

这段配置不会给任何子模块引入依赖,但如果某个子模块需要使用JUnit和Log4j的时候,我们就可以简化依赖配置成这样:

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
  </dependency>

现在只需要groupId和artifactId,其它元素如version和scope都能通过继承父POM的dependencyManagement得到,如果有依赖配置了exclusions,那节省的代码就更加可观。但重点不在这,重点在于现在能够保证所有模块使用的JUnit和Log4j依赖配置是一致的。而且子模块仍然可以按需引入依赖,如果我不配置dependency,父模块中dependencyManagement下的spring-aop依赖不会对我产生任何影响。

也许你已经意识到了,在多模块Maven项目中,dependencyManagement几乎是必不可少的,因为只有它是才能够有效地帮我们维护依赖一致性

本来关于dependencyManagement我想介绍的也差不多了,但几天前和Sunng的一次讨论让我有了更多的内容分享。那就是在使用dependencyManagement的时候,我们可以不从父模块继承,而是使用特殊的import scope依赖。Sunng将其列为自己的Maven Recipe #0,我再简单介绍下。

我们知道Maven的继承和Java的继承一样,是无法实现多重继承的,如果10个、20个甚至更多模块继承自同一个模块,那么按照我们之前的做法,这个父模块的dependencyManagement会包含大量的依赖。如果你想把这些依赖分类以更清晰的管理,那就不可能了,import scope依赖能解决这个问题。你可以把dependencyManagement放到单独的专门用来管理依赖的POM中,然后在需要使用依赖的模块中通过import scope依赖,就可以引入dependencyManagement。例如可以写这样一个用于依赖管理的POM:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.juvenxu.sample</groupId>
  <artifactId>sample-dependency-infrastructure</artifactId>
  <packaging>pom</packaging>
  <version>1.0-SNAPSHOT</version>
  <dependencyManagement>
    <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactid>junit</artifactId>
          <version>4.8.2</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>log4j</groupId>
          <artifactid>log4j</artifactId>
          <version>1.2.16</version>
        </dependency>
    </dependencies>
  </dependencyManagement>
</project>

然后我就可以通过非继承的方式来引入这段依赖管理配置:

  <dependencyManagement>
    <dependencies>
        <dependency>
          <groupId>com.juvenxu.sample</groupId>
          <artifactid>sample-dependency-infrastructure</artifactId>
          <version>1.0-SNAPSHOT</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
    </dependencies>
  </dependencyManagement>

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
  </dependency>

这样,父模块的POM就会非常干净,由专门的packaging为pom的POM来管理依赖,也契合的面向对象设计中的单一职责原则。此外,我们还能够创建多个这样的依赖管理POM,以更细化的方式管理依赖。这种做法与面向对象设计中使用组合而非继承也有点相似的味道。

消除多模块插件配置重复

与dependencyManagement类似的,我们也可以使用pluginManagement元素管理插件。一个常见的用法就是我们希望项目所有模块的使用Maven Compiler Plugin的时候,都使用Java 1.5,以及指定Java源文件编码为UTF-8,这时可以在父模块的POM中如下配置pluginManagement:

<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

这段配置会被应用到所有子模块的maven-compiler-plugin中,由于Maven内置了maven-compiler-plugin与生命周期的绑定,因此子模块就不再需要任何maven-compiler-plugin的配置了。

与依赖配置不同的是,通常所有项目对于任意一个依赖的配置都应该是统一的,但插件却不是这样,例如你可以希望模块A运行所有单元测试,模块B要跳过一些测试,这时就需要配置maven-surefire-plugin来实现,那样两个模块的插件配置就不一致了。这也就是说,简单的把插件配置提取到父POM的pluginManagement中往往不适合所有情况,那我们在使用的时候就需要注意了,只有那些普适的插件配置才应该使用pluginManagement提取到父POM中。

关于插件pluginManagement,Maven并没有提供与import scope依赖类似的方式管理,那我们只能借助继承关系,不过好在一般来说插件配置的数量远没有依赖配置那么多,因此这也不是一个问题。

小结

关于Maven POM重构的介绍,在此就告一段落了。基本上如果你掌握了本篇和上一篇Maven专栏讲述的重构技巧,并理解了其背后的目的原则,那么你肯定能让项目的POM变得更清晰易懂,也能尽早避免一些潜在的风险。虽然Maven只是用来帮助你构建项目和管理依赖的工具,POM也并不是你正式产品代码的一部分。但我们也应该认真对待POM,这有点像测试代码,以前可能大家都觉得测试代码可有可无,更不会去用心重构优化测试代码,但随着敏捷开发和TDD等方式越来越被人接受,测试代码得到了开发人员越来越多的关注。因此这里我希望大家不仅仅满足于一个“能用”的POM,而是能够积极地去修复POM中的坏味道。

本文已经首发于InfoQ中文站,版权所有,原文为《Maven实战(三)——多模块项目的POM重构》,如需转载,请务必附带本声明,谢谢。
InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如架构师》等。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2011/01/10/infoq-maven-pom-refactoring-multi-module/

Categories: Maven Tags: , , ,

InfoQ Maven专栏(二)——POM重构之增还是删

December 31st, 2010 2 comments

重构是广大开发者再熟悉不过的技术,在Martin Fowler的《重构——改善既有代码的设计》一书中,其定义为“重构(名词):对软件内部结构的一种调整,目的是在不改变软件之可察行为前提下,提高其可理解性,降低其修改成本.”以及“重构(动词):使用一系列重构准则(手法),在不改变软件之可察行为前提下,调整其结构.”。重构能够改善软件设计,使代码更易读,更容易找出bug,并帮助你更快速地编码。较之于一般的代码来说,Maven的POM简单很多,不过随着项目的成长,模块的增多,POM的内容也会变多,这个时候,我们可以对POM进行重构,在保持构建成功的前提下,简化POM内容,使其更简洁易懂。

前提

大家都知道,如果没有单元测试为前提,对代码进行重构是非常危险的。同样,在重构POM之前,项目应该有足够的自动化测试保证POM重构不会破坏构建。例如,重构POM的时候可能会删除或添加依赖,造成依赖版本变化,依赖范围变化,插件版本变化等等,这些变化可能会导致项目编译失败,或者测试失败。在自动化测试及构建的基础上,最好能够有持续集成环境,以保证POM的修改可能造成的问题能够及时的被发现和修复。笔者目前工作的项目有一个对应的持续集成任务,该任务基于Hudson,每10分钟检查一次SCM,如果发现变更则构建项目,并反馈结果。这样,我就不用担心自己修改POM会引入潜在的问题。

增还是删

有时候这个答案是很显然的,当你的POM中存在一些依赖或者插件配置,但实际代码没有用到这些配置的时候,应该尽早删掉它们以免给人带来困惑。

还有一种常见的情况,我们可以删掉一些POM的元素,例如POM中配置了继承,当前模块与父模块使用同样的groupId和version时,就可以将<groupId>和<version>元素删除,因为它们可以从父模块继承而来,重复配置没有什么意义。

<project>
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.juvenxu.sample</groupId>
    <artifactId>sample-parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>sample-foo</artifactId>
  <packaging>jar</packaging>
...
</project>

上述配置就sample-foo就没有groupId和version,需要注意的是,artifactId是不能被删除的,因为该元素不能也不应该被继承,父子模块应当使用不同的artifactId值。

除了删之外,有些时候我们还需要在POM中增加一些XML元素,目的是为了让POM更清晰易读,且保证Maven构建的稳定性。考虑如下的插件配置:

<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <source>1.5</source>
    <target>1.5</target>
  </configuration>
</plugin>

虽然没有groupId及version,但这段配置是完全合法的。当插件没有groupId配置的时候,Maven会认为它是官方插件而自动使用org.apache.maven.plugins作为groupId,当插件没有version配置的时候,Maven则会使用最新的版本(Maven 2会使用最新的版本,包括SNAPSHOT,而Maven 3则只使用最新的非SNAPSHOT版本)。这段配置有两个问题,首先,如果让一个不熟悉Maven的开发者来看这段配置,他会感到费解,groupId和version究竟是什么呢?这与不清晰的代码是一个意思,有时候一些程序员为了让代码更短,会采用一些奇怪的语法和变量名,虽然代码量是少了,但沟通成本增加了,得不偿失。其次,让Maven猜测版本会有潜在的风险,因为插件的最新版本可能会变化,而这种变化对于Maven使用者来说通常是隐藏的,特别是在Maven 2中,甚至可能引入SNAPSHOT版本的插件,这是非常危险的。基于这两个原因,使用插件的时候,我们应当配置清楚groupId和version,如:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>2.3.2</version>
  <configuration>
    <source>1.5</source>
    <target>1.5</target>
  </configuration>
</plugin>

基于类似的原因,在配置项目依赖的时候,我们也应当一直显式地写明依赖版本,以避免Maven在不同的时刻引入不同版本的依赖而导致项目构建的不稳定。

除了上面提到的增删点之外,Maven官方还提供了一个非常有用的Maven Dependency Plugin来帮助我们分析项目中哪些依赖配置应该删除,那些依赖配置应该增加。Maven Dependency Plugin的analyze目标能够帮助分析项目依赖,例如运行命令 mvn dependency:analyze ,可以看到如下输出:

[INFO] --- maven-dependency-plugin:2.1:analyze (default-cli) @ sample-bar ---
[WARNING] Used undeclared dependencies found:
[WARNING]    org.springframework:spring-context:jar:2.5.6:compile
[WARNING] Unused declared dependencies found:
[WARNING]    org.springframework:spring-core:jar:2.5.6:compile
[WARNING]    org.springframework:spring-beans:jar:2.5.6:compile
[INFO] ------------------------------------------------------------------------

这里的 Used undeclared dependencies 是指那些在项目中直接使用到的,但没有在POM中配置的依赖。例如该例中可能项目中的一些类有关于spring-context的Java import声明,但spring-context这个依赖实际是通过传递性依赖进入classpath的,这就意味者潜在的风险。一般来说我们对直接依赖的版本变化会比较清楚,因为那是我们自己直接配置的,但对于传递性依赖的版本变化,就会比较模糊,当这种变化造成构建失败的时候,就很难找到原因。因此我们应当增加这些 Used undeclared dependencies 。

依赖分析还提供了 Unused declared dependencies 供我们参考,这表示那些我们配置了,但并未直接使用的依赖。需要注意的时,对于这些依赖,我们不该直接简单地删除。由于dependency:analyze只分析编译主代码和测试代码使用的依赖,一些执行测试和运行时的依赖它发现不了,因此还需要人工分析。通常情况,Unused declared dependencies 还是能帮助我们发现一些无用的依赖配置。

最后,还一些重要的POM内容通常被大多数项目所忽略,这些内容不会影响项目的构建,但能方便信息的沟通,它们包括项目URL,开发者信息,SCM信息,持续集成服务器信息等等,这些信息对于开源项目来说尤其重要。对于那些想了解项目的人来说,这些信息能他们帮助找到想要的信息,基于这些信息生成的Maven站点也更有价值。相关的POM配置很简单,如:

<project>
  <description>...</description>
  <url>...</url>
  <licenses>...</licenses>
  <organization>...</organization>
  <developers>...</developers>
  <issueManagement>...</issueManagement>
  <ciManagement>...</ciManagement>
  <mailingLists>...</mailingLists>
  <scm>...</scm>
</project>

小结

无论是对POM内容进行增还是删,其目的都是一样的,就是为了让POM更清晰易懂且让构建更稳定。从这点来说,POM重构与一般的代码重构是类似的。需要谨记的是,重构的前提是完善的自动化测试和持续集成。本文介绍的单个POM规模的重构,下篇文章笔者会介绍多模块项目的POM重构等内容。

本文已经首发于InfoQ中文站,版权所有,原文为《Maven实战(二)——POM重构之增还是删》,如需转载,请务必附带本声明,谢谢。
InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如架构师》等。


原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2010/12/31/infoq-maven-pom-refactoring-add-or-delete/

Categories: Maven Tags: , , ,

InfoQ Maven专栏(一)——坐标规划

December 13th, 2010 9 comments

坐标是什么?为什么要规划?

坐标是Maven最基本的概念,它就像每个构件的身份证号码,有了它我们就可以在数以千万计的构件中定位任何一个我们感兴趣的构件。举个最简单的例子,如果没有坐标,使用JUnit的时候,用户就需要去下载依赖jar包,用依赖的方式,简单配置使用如junit:junit:4.8.2就可以了。这里第一个junit是groupId,第二个junit是artifactId,4.8.2是version。

Maven的很多其他核心机制都依赖于坐标,其中最显著的就是仓库和依赖管理。对于仓库来说,有了坐标就知道在什么位置存储构件的内容,例如junit:junit:4.8.2就对应仓库中的路径/junit/junit/4.8.2/junit-4.8.2.pom/junit/junit/4.8.2/junit-4.8.2.jar这样的文件,读者可以直接访问中央仓库地址看到这样的仓库布局,或者浏览本地仓库目录~/.m2/repository/以获得直观的体验。

依赖的配置也是完全基于坐标的,例如:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.8.2</version>
  <scope>test</scope>
</dependency>

有了正确的坐标,Maven才能够在正确的位置找到依赖文件并使用,这里值为test的scope是用来控制该依赖只在测试时可用,与坐标无关。

正因为坐标是Maven核心的核心,因此规划正确的坐标至关重要,如果你使用了模糊不清的坐标,那么你的用户就很难找到你的构件,或者即使找到了, 也容易写错。错误的使用坐标,还会造成冲突,如果你也使用junit这样的groupId,那会发生什么?下面先看一些不是很规范的坐标使用方式。

坐标规划的原则

滥用坐标、错用坐标的样例比比皆是,在中央仓库中我们能看到SpringFramework有两种坐标,其一是直接使用 springframework作为groupId,如springframework:spring-beans:1.2.6,另一种是用 org.springframework作为groupId,如org.springframework:spring-beans:2.5。细心看看, 前一种方式显得比较随意,后一种方式则是基于域名衍生出来的,显然后者更合理,因为用户能一眼根据域名联想到其Maven坐标,方便寻找。因此新版本的 SpringFramework构件都使用org.springframework作为groupId。由这个例子我们可以看到坐标规划一个原则是基于项目域名衍生。其实很多流行的开源项目都破坏了这个原则,例如JUnit,这是因为Maven社区在最开始接受构件并部署到中央仓库的时候,没有很很严格的限制,而对于这些流行的项目来说,一时间更改坐标会影响大量用户,因此也算是个历史遗留问题了。

还有一个常见的问题是将groupId直接匹配到公司或者组织名称,因为乍一看这是显而易见的。例如组织是zoo.com,有个项目是dog,那有 些人就直接使用groupId com.zoo了。如果项目只有一个模块,这是没有什么问题的,但现实世界的项目往往会有很多模块,Maven的一大长处就是通过多模块的方式管理项目。 那dog项目可能会有很多模块,我们用坐标的哪个部分来定义模块呢?groupId显然不对,version也不可能是,那只有artifactId。因 此要这里有了另外一个原则,用artifactId来定义模块,而不是定义项目。接下来,很显然的,项目就必须用groupId来定义。因此对于dog项目来说,应该使用groupId com.zoo.dog,不仅体现出这是zoo.com下的一个项目,而且可以与该组织下的其他项目如com.zoo.cat区分开来。

除此之外,artifactId的定义也有最佳实践,我们常常可以看到一个项目有很多的模块,例如api,dao,service,web等等。 Maven项目在默认情况下生成的构件,其名称不会是基于artifactId,version和packaging生成的,例如api-1.0.jardao-1.0.jar等 等,他们不会带有groupId的信息,这会造成一个问题,例如当我们把所有这些构件放到Web容器下的时候,你会发现项目dog有api- 1.0.jar,项目cat也有api-1.0.jar,这就造成了冲突。更坏的情况是,dog项目有api-1.0.jar,cat项目有api- 2.0.jar,其实两者没什么关系,可当放在一起的时候,却很容易让人混淆。为了让坐标更加清晰,又出现了一个原则,即在定义artiafctId时也加入项目的信息, 例如dog项目的api模块,那就使用artifactId dog-api,其他就是dog-dao,dao-service等等。虽然连字号是不允许出现在Java的包名中的,但Maven没这个限制。现在 dog-api-1.0.jar,cat-2.0.jar被放在一起时,就不容易混淆了。

关于坐标,我们还没谈到version,这里不再详述因为读者可以从Maven: The Complete Guide中找到详细的解释,简言之就是使用这样一个格式:

<主版本>.<次版本>.<增量版本>-<限定符>

其中主版本主要表示大型架构变更,次版本主要表示特性的增加,增量版本主要服务于bug修复,而限定符如alpha、beta等等是用来表示里程 碑。当然不是每个项目的版本都要用到这些4个部分,根据需要选择性的使用即可。在此基础上Maven还引入了SNAPSHOT的概念,用来表示活动的开发 状态,由于不涉及坐标规划,这里不进行详述。不过有点要提醒的是,由于SNAPSHOT的存在,自己显式地在version中使用时间戳字符串其实没有必 要。

Classifier

Classifier可能是最容易被忽略的Maven特性,但它确实非常重要,我们也需要它来帮助规划坐标。设想这样一个情况,有一个jar项目,就说是 dog-cli-1.0.jar 吧,运行它用户就能在命令行上画一只小狗出来。现在用户的要求是希望你能提供一个zip包,里面不仅包含这个可运行的jar,还得包含源代码和文档,换句话说,这是比较正式的分发包。这个文件名应该是怎样的呢?dog-cli-1.0.zip?不够清楚,仅仅从扩展名很难分辨什么是Maven默认生成的构件,什么是额外配置生成分发包。如果能是dog-cli-1.0-dist.zip就最好了。这里的dist就是classifier,默认Maven只生成一个构件,我们称之为主构件,那当我们希望Maven生成其他附属构件的时候,就能用上classifier。常见的classifier还有如dog-cli-1.0-sources.jar表示源码包,dog-cli-1.0-javadoc.jar表示JavaDoc包等等。制作classifier的方式多种多样,其中最重要的一种是使用Maven Assembly Plugin,感兴趣的读者可以进一步研究。

小结

本文是InfoQ Maven专栏的第一篇,讨论的是Maven坐标的规划,包括如何正确的使用groupId、artifactId、version,以及 classfier。笔者在维护Maven中央仓库的工作过程中遇到过各种各样模糊的甚至是错误的坐标,它们的存在给广大Maven用户带来的极大的不 便。本文抛出一些较好的实践,帮助大家更好的使用Maven。如果读者有相关的经验总结,也请不吝分享。

本文已经首发于InfoQ中文站,版权所有,原文为《Maven实战(一)——坐标规划》,如需转载,请务必附带本声明,谢谢。
InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如架构师》等。