Recent Maven Blogs

Searching with the Nexus REST API: Ruby

11/19/08 -

When you search for artifacts using http://repository.sonatype.org, the browser is querying the Nexus repository using a REST API. In this post, I’m going to show you some simple Ruby scripts which you can use to search the Maven repository ...

Use repository.sonatype.org to Search Central Maven Repository

11/19/08 -

If you are looking for the easiest way to search for artifacts in the Central Maven Repository, go to http://repository.sonatype.org. Sonatype maintains a publi instance of Nexus that can be used to search for artifacts by GAV (groupId, artifactId...

Searching the Maven Repository

11/11/08 -

A common question from Apache Maven users is “How do I search the central repository?” or “How do I find out what groupId or artifactId I should use for a specific dependency?” Use Sonatype’s Nexus installation at htt...

m2eclipse turns heads at the Stockholm JUG!

11/11/08 -

One of the users on the m2eclipse user list has some great feedback about a presentation he did at the Stockholm JUG on the best of breed Maven tooling which includes Nexus and m2eclipse. Apparently people are impressed with the Maven support insi...

General Framework For Model Inheritance

11/10/08 -

Maven 3.0 uses a new standalone component that handles inheritance and interpolation of a model in any format. The model needn’t even be XML based. If you can translate your model into a list of property-value pairs, you can use this framewo...

MySQL?s Zack Urlocker writes about Sonatype?s new CEO in InfoWorld

11/10/08 -

There’s are lots of people who already know that Mark de Visser is the new CEO of Sonatype, but Zack wrote a nice bit about it today in his InfoWorld blog: http://weblog.infoworld.com/openresource/archives/2008/11/marketing_maven.html I am r...

Chapter 4. 定制一个Maven项目

4.1. 介绍

Favicon

本章在上一章所介绍信息的基础上进行开发。 你将创建一个由 Maven Archetype 插件生成的项目,添加一些依赖和一些源代码,并且根据你的需要定制项目。本章最后,你将知道如何使用 Maven 开始创建真正的项目。

4.1.1. 下载本章样例

Favicon

本章我们将开发一个和 Yahoo! Weather web 服务交互的实用程序。虽然没有样例源码你也应该能够理解这个开发过程,但还是推荐你下载本章样例源码以作为参考。 本章的样例项目包含在本书的样例代码中,你可以从两个地方下载,http://www.sonatype.com/book/mvn-examples-1.0.zip 或者 http://www.sonatype.com/book/mvn-examples-1.0.tar.gz 。解压存档文件至任意目录,然后到 ch04/ 目录。 在 ch04/ 目录你会看到一个名为 simple-weather 的目录,它包含了本章开发出来的 Maven 项目。如果你想要在浏览器里看样例代码,访问 http://www.sonatype.com/book/examples-1.0 ,然后点击 ch04/ 目录。

4.2. 定义Simple Weather项目

Favicon

在定制本项目之前,让我们退后一步,讨论下这个 simple weather 项目。 这个 simple weather 项目是什么? 它是一个被设计成用来示范一些 Maven 特征的样例。 它能代表一类你可能需要构建的应用程序。 这个 simple weather 是一个基本的命令行驱动的应用程序,它接受邮政编码输入,然后从 Yahoo! Weather RSS 源获取数据,然后解析数据并把结果打印到标准输出。 我们选择该项目是有许多因素的。 首先,它很直观;用户通过命令行提供输入,程序读取邮政编码,对 Yahoo! Weather 提交请求,之后解析结果,格式化之后输入到屏幕。 这个样例是个简单的 main() 函数加上一些相关支持的类;没有企业级框架需要介绍或解释,只有 XML 解析和一些日志语句。 其次,它提供很好的机会来介绍一些有趣的类库,如 Velocity, Dom4j 和 Log4j。 虽然本书集中于 Maven ,但我们不会回避那些介绍有趣工具的机会。 最后,这是一个能在一章内介绍,开发及部署的样例。

4.2.1. Yahoo! Weather RSS

Favicon

在开始构建这个应用之前,你需要了解一下 Yahoo! Weather RSS 源。该服务是基于以下条款提供的:

“该数据源免费提供给个人和非营利性组织,作为个人或其它非商业用途。 我们要求你提供给 Yahoo! Weather 连接你数据源应用的权限。”

换句话说,如果你考虑集成该数据源到你的商业 web 站点上,请再仔细考虑考虑,该数据源可作为个人或其它非商业性用途。 本章我们提倡的使用是个人教育用途。 要了解更多的 Yahoo! Weather 服务条款,请参考 Yahoo! Weather API 文档: http://developer.yahoo.com/weather/

4.3. 创建Simple Weather项目

Favicon

首先,让我们用 Maven Archetype 插件创建这个 simple weather 项目的基本轮廓。 运行下面的命令,创建新项目:

$ mvn archetype:create -DgroupId=org.sonatype.mavenbook.ch04 \
                                         -DartifactId=simple-weather \
                                         -DpackageName=org.sonatype.mavenbook \
                                         -Dversion=1.0
[INFO] [archetype:create]
[INFO] artifact org.apache.maven.archetypes:maven-archetype-quickstart: \
       checking for updates from central
[INFO] ------------------------------------------------------------------
[INFO] Using following parameters for creating Archetype: \
       maven-archetype-quickstart:RELEASE
[INFO] ------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.sonatype.mavenbook.ch04
[INFO] Parameter: packageName, Value: org.sonatype.mavenbook
[INFO] Parameter: basedir, Value: ~/examples
[INFO] Parameter: package, Value: org.sonatype.mavenbook
[INFO] Parameter: version, Value: 1.0
[INFO] Parameter: artifactId, Value: simple-weather
[INFO] *** End of debug info from resources from generated POM ***
[INFO] Archetype created in dir: ~/examples/simple-weather

在 Maven Archetype 插件创建好了这个项目之后,进入到 simple-weather 目录,看一下 pom.xml。你会看到如下的 XML 文档:

Example 4.1. simple-wheather 项目的初始 POM

<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>org.sonatype.mavenbook.ch04</groupId>
  <artifactId>simple-weather</artifactId>
  <packaging>jar</packaging>
  <version>1.0</version>
  <name>simple-weather2</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

请注意我们给 archetype:create 目标传入了 version 参数。它覆写了默认值 1.0-SNAPSHOT 。本项目中,正如你从 pom.xmlversion 元素看到的,我们正在开发 simple-weather 项目的 1.0 版本。

4.4. 定制项目信息

Favicon

在开始编写代码之前,让我们先定制一些项目的信息。 我们想要做的是添加一些关于项目许可证,组织以及项目相关开发人员的一些信息。 这些都是你期望能在大部分项目中看到的标准信息。下面的文档展示了提供组织信息,许可证信息和开发人员信息的 XML

Example 4.2. 为 pom.xml 添加组织,法律和开发人员信息

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

  <name>simple-weather</name>
  <url>http://www.sonatype.com</url>

  <licenses>
    <license>
      <name>Apache 2</name>
      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
      <distribution>repo</distribution>
      <comments>A business-friendly OSS license</comments>
    </license>
  </licenses>

  <organization>
    <name>Sonatype</name>
    <url>http://www.sonatype.com</url>
  </organization>

  <developers>
    <developer>
      <id>jason</id>
      <name>Jason Van Zyl</name>
      <email>jason@maven.org</email>
      <url>http://www.sonatype.com</url>
      <organization>Sonatype</organization>
      <organizationUrl>http://www.sonatype.com</organizationUrl>
      <roles>
        <role>developer</role>
      </roles>
      <timezone>-6</timezone>
    </developer>
  </developers>
...
</project>

Example 4.2, “为 pom.xml 添加组织,法律和开发人员信息” 中的省略号是为了使代码清单变得简短。 当你在 pom.xml 中看到 project 元素的开始标签后面跟着 “…” 或者在 project 元素的结束标签前有 “…” ,这说明我们没有展示整个 pom.xml 文件。在上述情况中,licensesorganizationdevelopers 元素是加在 dependencies 元素之前的。

4.5. 添加新的依赖

Favicon

Simple weather 应用程序必须要完成以下三个任务:从 Yahoo! Weather 获取 XML 数据,解析 XML 数据,打印格式化的输出至标准输出。为了完成这三个任务,我们需要为项目的 pom.xml 引入一些新的依赖。 为了解析来自 Yahoo! 的 XML 响应,我们将会使用 Dom4J 和 Jaxen ,为了格式化这个命令行程序的输出,我们将会使用 Velocity ,我们还需要加入对 Log4j 的依赖,用来做日志。加入这些依赖之后,我们的 dependencies 元素就成了以下模样:

Example 4.3. 添加 Dom4J, Jaxen, Velocity 和 Log4J 作为依赖

<project>
  [...]
  <dependencies>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.14</version>
    </dependency>
    <dependency>
      <groupId>dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>1.6.1</version>
    </dependency>
    <dependency>
      <groupId>jaxen</groupId>
      <artifactId>jaxen</artifactId>
      <version>1.1.1</version>
    </dependency>
    <dependency>
      <groupId>velocity</groupId>
      <artifactId>velocity</artifactId>
      <version>1.5</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  [...]
</project>


正如你从上面所看到的,我们在范围为 test 的 JUnit 依赖基础上又加了四个依赖元素。如果你把这些依赖添加到项目的 pom.xml 文件然后运行 mvn install ,你会看到 Maven 下载这些依赖及其它传递性依赖到你的本地 Maven 仓库。 我们如何找这些依赖呢?我们都“知道”适当的 groupIdartifactId 的值吗?有些依赖 (像 Log4J) 被广泛使用,以至于每次你需要使用它们的时候你都会记得它们的 groupIdartifactId。 而 Velocity, Dom4J 和 Jaxen 是通过一个十分有用的站点 http://www.mvnrepository.com 来定位的。 该站点提供了针对 Maven 仓库的搜索接口,你可以用它来搜索依赖。 你可以自己测试一下,载入 http://www.mvnrepository.com 然后搜索一些常用的类库,如 Hibernate 或者 Spring Framework 。当你在这上面搜索构件时,它会显示一个 artifactId 和所有 Maven 中央仓库所知道的版本。 点击某个特定的版本后,它会载入一个页面,这个页面就包括了你需要复制到你自己项目 pom.xml 中的依赖元素。 你经常会发现某个特定的类库拥有多于一个的 groupId,这个时候你需要通过 mvnrepository.com 来帮助确定你到底需要怎样配置你的依赖。

4.6. Simple Weather源码

Favicon

Simple Weather 命令行应用程序包含五个 Java 类。

org.sonatype.mavenbook.weather.Main

这个类包含了一个静态的 main() 函数,即系统的入口。

org.sonatype.mavenbook.weather.Weather

Weather 类是个很简单的 Java Bean,它保存了天气报告的地点和其它一些关键元素,如气温和湿度。

org.sonatype.mavenbook.weather.YahooRetriever

YahooRetriever 连接到 Yahoo! Weather 并且返回来自数据源数据的 InputStream

org.sonatype.mavenbook.weather.YahooParser

YahooParser 解析来自 Yahoo! Weather 的 XML,返回 Weather 对象。

org.sonatype.mavenbook.weather.WeatherFormatter

WeatherFormatter 接受 Weather 对象,创建 VelocityContext ,根据 Velocity 模板生成结果。

这里我们不是想要详细阐述样例中的代码,但解释一下程序中使之运行的核心代码还是必要的。 我们假设大部分读者已经下载的本书的源码,但也不会忘记那些按着书一步一步往下看的读者。 本小节列出了 simple-weather 项目的类,这些类都放在同一个包下面,org.sonatype.mavenbook.weather

让我们删掉由 archetype:create 生成 App 类和 AppTest 类,然后加入我们新的包。在 Maven 项目中,所有项目的源代码都存储在 src/main/java 目录。 在新项目的基础目录下,运行下面的命令:

$ cd src/test/java/org/sonatype/mavenbook
$ rm AppTest.java
$ cd ../../../../../..
$ cd src/main/java/org/sonatype/mavenbook
$ rm App.java
$ mkdir weather
$ cd weather

你已经创建了一个新的包 org.sonatype.mavenbook.weather 。 现在,我们需要把那些类放到这个目录下面。 用你最喜欢的编辑器,创建一个新文件,名字为 Weather.java,内容如下:

Example 4.4. Simple Weather 的 Weather 模型对象

package org.sonatype.mavenbook.weather;


public class Weather {
  private String city;
  private String region;
  private String country;
  private String condition;
  private String temp;
  private String chill;
  private String humidity;
    
  public Weather() {}

  public String getCity() { return city; }
  public void setCity(String city) { this.city = city; }

  public String getRegion() { return region; }
  public void setRegion(String region) { this.region = region; }

  public String getCountry() { return country; }
  public void setCountry(String country) { this.country = country; }

  public String getCondition() { return condition; }
  public void setCondition(String condition) { this.condition = condition; }

  public String getTemp() { return temp; }
  public void setTemp(String temp) { this.temp = temp; }
         
  public String getChill() { return chill; }
  public void setChill(String chill) { this.chill = chill; }

  public String getHumidity() { return humidity; }
  public void setHumidity(String humidity) { this.humidity = humidity; }
}

Weather 类定义了一个简单的 bean ,用来存储由 Yahoo! Weather 数据源解析出来的天气信息。天气数据源提供了丰富的信息,从日出日落时间,到风速和风向。 为了让这个例子保持简单, Weather 模型对象只保存温度,湿度和当前天气情况的文字描述等信息。

在同一目录下,创建 Main.java 文件。Main 这个类有一个静态的 main() 函数——样例程序的入口。

Example 4.5. Simple Weather 的 Main 类

package org.sonatype.mavenbook.weather;

import java.io.InputStream;

import org.apache.log4j.PropertyConfigurator;


public class Main {

  public static void main(String[] args) throws Exception {
    // Configure Log4J
    PropertyConfigurator.configure(Main.class.getClassLoader()
                                       .getResource("log4j.properties"));

    // Read the Zip Code from the Command-line (if none supplied, use 60202)
    int zipcode = 60202;
    try {
      zipcode = Integer.parseInt(args[0]);
    } catch( Exception e ) {}

    // Start the program
    new Main(zipcode).start();
  }

  private int zip;

  public Main(int zip) {
    this.zip = zip;
  }

  public void start() throws Exception {
    // Retrieve Data
    InputStream dataIn = new YahooRetriever().retrieve( zip );

    // Parse Data
    Weather weather = new YahooParser().parse( dataIn );

    // Format (Print) Data
    System.out.print( new WeatherFormatter().format( weather ) );
  }
}

上例中的 main() 函数通过获取 classpath 中的资源文件来配置 Log4J ,之后它试图从命令行读取邮政编码。 如果在读取邮政编码的时候抛出了异常,程序会设置默认邮政编码为 60202 。 一旦有了邮政编码,它初始化一个 Main 对象,调用该对象的 start() 方法。而 start() 方法会调用 YahooRetriever 来获取天气的 XML 数据。 YahooRetriever 返回一个 InputStreem ,传给 YahooParserYahooParser 解析 XML 数据并返回 Weather 对象。 最后,WeatherFormatter 接受一个 Weather 对象并返回一个格式化的 String ,打印到标准输出。

在相同目录下创建文件 YahooRetriever.java ,内容如下:

Example 4.6. Simple Weather 的 YahooRetriever 类

package org.sonatype.mavenbook.weather;

import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

import org.apache.log4j.Logger;

public class YahooRetriever {

  private static Logger log = Logger.getLogger(YahooRetriever.class);

  public InputStream retrieve(int zipcode) throws Exception {
    log.info( "Retrieving Weather Data" );
    String url = "http://weather.yahooapis.com/forecastrss?p=" + zipcode;
    URLConnection conn = new URL(url).openConnection();
    return conn.getInputStream();
  }
}

这个简单的类打开一个连接到 Yahoo! Weather API 的 URLConnection 并返回一个 InputStream 。 我们还需要在该目录下创建文件 YahooParser.java 用以解析这个数据源。

Example 4.7. Simple Weather 的 YahooParser 类

package org.sonatype.mavenbook.weather;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentFactory;
import org.dom4j.io.SAXReader;

public class YahooParser {

  private static Logger log = Logger.getLogger(YahooParser.class);

  public Weather parse(InputStream inputStream) throws Exception {
    Weather weather = new Weather();
  
    log.info( "Creating XML Reader" );
    SAXReader xmlReader = createXmlReader();
    Document doc = xmlReader.read( inputStream );

    log.info( "Parsing XML Response" );
    weather.setCity( doc.valueOf("/rss/channel/y:location/@city") );
    weather.setRegion( doc.valueOf("/rss/channel/y:location/@region") );
    weather.setCountry( doc.valueOf("/rss/channel/y:location/@country") );
    weather.setCondition( doc.valueOf("/rss/channel/item/y:condition/@text") );
    weather.setTemp( doc.valueOf("/rss/channel/item/y:condition/@temp") );
    weather.setChill( doc.valueOf("/rss/channel/y:wind/@chill") );
    weather.setHumidity( doc.valueOf("/rss/channel/y:atmosphere/@humidity") );
  
    return weather;
  }

  private SAXReader createXmlReader() {
    Map<String,String> uris = new HashMap<String,String>();
        uris.put( "y", "http://xml.weather.yahoo.com/ns/rss/1.0" );
        
    DocumentFactory factory = new DocumentFactory();
    factory.setXPathNamespaceURIs( uris );
        
    SAXReader xmlReader = new SAXReader();
    xmlReader.setDocumentFactory( factory );
    return xmlReader;
  }
}

YahooParser 是本例中最复杂的类,我们不会深入 Dom4J 或者 Jaxen 的细节,但是这个类还是需要一些解释。YahooParserparse() 方法接受一个 InputStrem 然后返回一个 Weather 对象。 为了完成这一目标,它需要用 Dom4J 来解析 XML 文档。因为我们对 Yahoo! Weather XML 命名空间的元素感兴趣,我们需要用 createXmlReader() 方法创建一个包含命名空间信息的 SAXReader 。 一旦我们创建了这个 reader 并且解析了文档,得到了返回的 org.dom4j.Document ,只需要简单的使用 XPath 表达式来获取需要的信息,而不是遍历所有的子元素。 本例中 Dom4J 提供了 XML 解析功能,而 Jaxen 提供了 XPath 功能。

我们已经创建了 Weather 对象,我们需要格式化输出以供人阅读。 在同一目录中创建一个名为 WeatherFormatter.java 的文件。

Example 4.8. Simple Weather 的 WeatherFormatter 类

package org.sonatype.mavenbook.weather;

import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;

import org.apache.log4j.Logger;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

public class WeatherFormatter {

  private static Logger log = Logger.getLogger(WeatherFormatter.class);

  public String format( Weather weather ) throws Exception {
    log.info( "Formatting Weather Data" );
    Reader reader = 
      new InputStreamReader( getClass().getClassLoader()
                                 .getResourceAsStream("output.vm"));
    VelocityContext context = new VelocityContext();
    context.put("weather", weather );
    StringWriter writer = new StringWriter();
    Velocity.evaluate(context, writer, "", reader);
    return writer.toString();
  }
}

WeatherFormatter 使用 Veloticy 来呈现一个模板。format() 方法接受一个 Weather bean 然后返回格式化好的 Stringformat() 方法做的第一件事是从 classpath 载入名字为 output.vm 的 Velocity 模板。 然后我们创建一个 VelocityContext ,它需要一个 Weather 对象来填充。 一个StringWriter被创建用来存放模板生成的结果数据。通过调用 Velocity.evaluate() ,给模板赋值,结果作为 String 返回。

在我们能够运行该样例程序之前,我们需要往 classpath 添加一些资源。

4.7. 添加资源

Favicon

本项目依赖于两个 classpath 资源: Main 类通过 classpath 资源 log4j.preoperties 来配置 Log4J , WeatherFormatter 引用了一个在 classpath 中的名为 output.vm 的 Velocity 模板。这两个资源都需要在默认包中(或者 classpath 的根目录)。

为了添加这些资源,我们需要在项目的基础目录下创建一个新的目录—— src/main/resources。 由于任务 archetype:create 没有创建这个目录,我们需要通过在项目的基础目录下运行下面的命令来创建它:

$ cd src/main
$ mkdir resources
$ cd resources

在这个资源目录创建好之后,我们可以加入这两个资源。首先,往目录 resources 加入文件 log4j.properties

Example 4.9. Simple Weather 的 Log4J 配置文件

# Set root category priority to INFO and its only appender to CONSOLE.
log4j.rootCategory=INFO, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=INFO
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%-4r %-5p %c{1} %x - %m%n

这个 log4j.properties 文件简单配置了 Log4J ,使其使用 PatternLayout 往标准输出打印所有日志信息。 最后,我们需要创建 output.vm ,它是这个命令行程序用来呈现输出的 Velocity 模板。 在 resources 目录创建 output.vm

Example 4.10. Simple Weather 的 Output Velocity 模板

*********************************
 Current Weather Conditions for:
  ${weather.city}, ${weather.region}, ${weather.country}
  
 Temperature: ${weather.temp}
   Condition: ${weather.condition}
    Humidity: ${weather.humidity}
  Wind Chill: ${weather.chill}
*********************************

这个模板包含了许多对名为 weather 的变量的引用。 这个 weather 变量是传给 WeatherFormatter 的 那个 Weather bean, ${weather.temp} 语法简化的表示获取并显示 temp 这个bean属性的值。 现在我们已经在正确的地方有了我们项目的所有代码,我们可以使用 Maven 来运行这个样例。


4.8. 运行Simple Weather项目

Favicon

使用来自 Codehaus Mojo 项目 的 Exec 插件,我们可以运行这个程序。在项目的基础目录下运行以下命令,以运行该程序的 Main 类。

$ mvn install
$ mvn exec:java -Dexec.mainClass=org.sonatype.mavenbook.weather.Main
...
[INFO] [exec:java]
0    INFO  YahooRetriever  - Retrieving Weather Data
134  INFO  YahooParser  - Creating XML Reader
333  INFO  YahooParser  - Parsing XML Response
420  INFO  WeatherFormatter  - Formatting Weather Data
*********************************
 Current Weather Conditions for:
  Evanston, IL, US
  
 Temperature: 45
   Condition: Cloudy
    Humidity: 76
  Wind Chill: 38
*********************************
...

我们没有为 Main 类提供命令行参数,因此程序按照默认的邮编执行——60202。 正如你能看到的,我们已经成功的运行了 SImple Weather 命令行工具,从 Yahoo! Weather 获取了一些数据,解析了结果,并且通过 Velocity 格式化了结果数据。 我们仅仅写了项目的源代码,往 pom.xml 添加了一些最少的配置。 注意我们这里没有引入“构建过程”。 我们不需要定义如何或哪里让 Java 编译器编译我们的源代码,我们不需要指导构建系统在运行样例程序的时候如何定位二进制文件, 我们所需要做的是包含一些依赖,用来定位合适的 Maven 坐标。

4.8.1. Maven Exec 插件

Favicon

Exec 插件允许你运行 Java 类和其它脚本。 它不是 Maven 核心插件,但它可以从 CodehausMojo 项目得到。想要查看 Exec 插件的完整描述,运行:

$ mvn help:describe -Dplugin=exec -Dfull

这会列出所有 Maven Exec 插件可用的目标。 Help 插件同时也会列出 Exec 插件的有效参数,如果你想要定制 Exec 插件的行为,传入命令行参数,你应该使用 help:describe 提供的文档作为指南。 虽然 Exec 插件很有用,在开发过程中用来运行测试之外,你不应该依赖它来运行你的应用程序。 想要更健壮的解决方案,使用 Maven Assembly 插件,它在Section 4.13, “构建一个打包好的命令行应用程序”中被描述。

4.8.2. 浏览你的项目依赖

Favicon

Exec 插件让我们能够在不往 classpath 载入适当的依赖的情况下,运行这个程序。 在任何其它的构建系统能够中,我们必须复制所有程序依赖到类似于 lib/ 的目录,这个目录包含一个 JAR 文件的集合。 那样,我们就必须写一个简单的脚本,在 classpath 中包含我们程序的二进制代码和我们的依赖。 只有那样我们才能运行 java org.sonatype.mavenbook.weather.Main 。 Exec 能做这样的工作是因为 Maven 已经知道如何创建和管理你的 classpath 和你的依赖。

了解你项目的 classpath 包含了哪些依赖是很方便也很有用的。这个项目不仅包含了一些类库如 Dom4J, Log4J, Jaxen, 和 Velocity,它同时也引入了一些传递性依赖。 如果你需要找出 classpath 中有什么,你可以使用 Maven Dependency 插件来打印出已解决依赖的列表。 要打印出 Simple Weather 项目的这个列表,运行 dependency:resolve 目标。

$ mvn dependency:resolve
...
[INFO] [dependency:resolve]
[INFO] 
[INFO] The following files have been resolved: 
[INFO]    com.ibm.icu:icu4j:jar:2.6.1 (scope = compile)
[INFO]    commons-collections:commons-collections:jar:3.1 (scope = compile)
[INFO]    commons-lang:commons-lang:jar:2.1 (scope = compile)
[INFO]    dom4j:dom4j:jar:1.6.1 (scope = compile)
[INFO]    jaxen:jaxen:jar:1.1.1 (scope = compile)
[INFO]    jdom:jdom:jar:1.0 (scope = compile)
[INFO]    junit:junit:jar:3.8.1 (scope = test)
[INFO]    log4j:log4j:jar:1.2.14 (scope = compile)
[INFO]    oro:oro:jar:2.0.8 (scope = compile)
[INFO]    velocity:velocity:jar:1.5 (scope = compile)
[INFO]    xalan:xalan:jar:2.6.0 (scope = compile)
[INFO]    xerces:xercesImpl:jar:2.6.2 (scope = compile)
[INFO]    xerces:xmlParserAPIs:jar:2.6.2 (scope = compile)
[INFO]    xml-apis:xml-apis:jar:1.0.b2 (scope = compile)
[INFO]    xom:xom:jar:1.0 (scope = compile)

正如你能看到的,我们项目拥有一个很大的依赖集合。 虽然我们只是为四个类库引入了直接的依赖,看来我们实际共引入了15个依赖。 Dom4J 依赖于 Xerces 和 XML 解析器 API ,Jaxen 依赖于 Xalan,后者也就在 classpath 中可用了。 Dependency 插件将会打印出最终的你项目编译所基于的所有依赖的组合。 如果你想知道你项目的整个依赖树,你可以运行 dependency:tree 目标。

$ mvn dependency:tree
...
[INFO] [dependency:tree]
[INFO] org.sonatype.mavenbook.ch04:simple-weather:jar:1.0
[INFO] +- log4j:log4j:jar:1.2.14:compile
[INFO] +- dom4j:dom4j:jar:1.6.1:compile
[INFO] |  \- xml-apis:xml-apis:jar:1.0.b2:compile
[INFO] +- jaxen:jaxen:jar:1.1.1:compile
[INFO] |  +- jdom:jdom:jar:1.0:compile
[INFO] |  +- xerces:xercesImpl:jar:2.6.2:compile
[INFO] |  \- xom:xom:jar:1.0:compile
[INFO] |     +- xerces:xmlParserAPIs:jar:2.6.2:compile
[INFO] |     +- xalan:xalan:jar:2.6.0:compile
[INFO] |     \- com.ibm.icu:icu4j:jar:2.6.1:compile
[INFO] +- velocity:velocity:jar:1.5:compile
[INFO] |  +- commons-collections:commons-collections:jar:3.1:compile
[INFO] |  +- commons-lang:commons-lang:jar:2.1:compile
[INFO] |  \- oro:oro:jar:2.0.8:compile
[INFO] +- org.apache.commons:commons-io:jar:1.3.2:test
[INFO] \- junit:junit:jar:3.8.1:test
...

如果你还不满足,或者想要查看完整的依赖踪迹,包含那些因为冲突或者其它原因而被拒绝引入的构件,打开 Maven 的调试标记运行:

$ mvn install -X
...
[DEBUG] org.sonatype.mavenbook.ch04:simple-weather:jar:1.0 (selected for null)
[DEBUG]   log4j:log4j:jar:1.2.14:compile (selected for compile)
[DEBUG]   dom4j:dom4j:jar:1.6.1:compile (selected for compile)
[DEBUG]     xml-apis:xml-apis:jar:1.0.b2:compile (selected for compile)
[DEBUG]   jaxen:jaxen:jar:1.1.1:compile (selected for compile)
[DEBUG]     jaxen:jaxen:jar:1.1-beta-6:compile (removed - causes a cycle in the graph)
[DEBUG]     jaxen:jaxen:jar:1.0-FCS:compile (removed - causes a cycle in the graph)
[DEBUG]     jdom:jdom:jar:1.0:compile (selected for compile)
[DEBUG]     xml-apis:xml-apis:jar:1.3.02:compile (removed - nearer found: 1.0.b2)
[DEBUG]     xerces:xercesImpl:jar:2.6.2:compile (selected for compile)
[DEBUG]     xom:xom:jar:1.0:compile (selected for compile)
[DEBUG]       xerces:xmlParserAPIs:jar:2.6.2:compile (selected for compile)
[DEBUG]       xalan:xalan:jar:2.6.0:compile (selected for compile)
[DEBUG]       xml-apis:xml-apis:1.0.b2.
[DEBUG]       com.ibm.icu:icu4j:jar:2.6.1:compile (selected for compile)
[DEBUG]   velocity:velocity:jar:1.5:compile (selected for compile)
[DEBUG]     commons-collections:commons-collections:jar:3.1:compile (selected for compile)
[DEBUG]     commons-lang:commons-lang:jar:2.1:compile (selected for compile)
[DEBUG]     oro:oro:jar:2.0.8:compile (selected for compile)
[DEBUG]   junit:junit:jar:3.8.1:test (selected for test)

从调试输出我们看到一些依赖管理系统工作的内部信息。 你在这里看到的是项目的依赖树。 Maven 正打印出你项目的所有的依赖,以及这些依赖的依赖(还有依赖的依赖的依赖)的完整的 Maven 坐标。 你能看到 simple-weather 依赖于 jaxenjaxen 依赖于 xomxom 接着依赖于 icu4j 。从该输出你能看到 Maven 正在创建一个依赖图,排除重复,解决不同版本之间的冲突。 如果你的依赖有问题,通常在 dependency:tree 所生成的列表基础上更深入一点会有帮助;开启调试输出允许你看到 Maven 工作时的依赖机制。

4.9. 编写单元测试

Favicon

Maven 内建了对单元测试的支持,测试是 Maven 默认生命周期的一部分。让我们给 Simple Weather 项目添加一些单元测试。 首先,在 src/test/java 下面创建包 org.sonatype.mavenbook.weather

$ cd src/test/java
$ cd org/sonatype/mavenbook
$ mkdir -p weather/yahoo
$ cd weather/yahoo

目前,我们将会创建两个单元测试。 第一个单元测试会测试 YahooParser ,第二个会测试 WeatherFormatter。 在 weather 包中,创建一个带有一以下内容的文件,名称为 YahooParserTest.java

Example 4.11. Simple Weather 的 YahooParserTest 单元测试

package org.sonatype.mavenbook.weather.yahoo;

import java.io.InputStream;

import junit.framework.TestCase;

import org.sonatype.mavenbook.weather.Weather;
import org.sonatype.mavenbook.weather.YahooParser;

public class YahooParserTest extends TestCase {

  public YahooParserTest(String name) {
    super(name);
  }
 
  public void testParser() throws Exception {
    InputStream nyData = 
      getClass().getClassLoader().getResourceAsStream("ny-weather.xml");
    Weather weather = new YahooParser().parse( nyData );
    assertEquals( "New York", weather.getCity() );
    assertEquals( "NY", weather.getRegion() );
    assertEquals( "US", weather.getCountry() );
    assertEquals( "39", weather.getTemp() );
    assertEquals( "Fair", weather.getCondition() );
    assertEquals( "39", weather.getChill() );
    assertEquals( "67", weather.getHumidity() );
  }
}

YahooParserTest 继承了 JUnit 定义的 TestCase 类。 它遵循了 JUnit 测试的惯例模式:一个构造函数接受一个单独的 String 参数并调用父类的构造函数,还有一系列以“test”开头的公有方法,做为单元测试被调用。 我们定义了一个单独的测试方法, testParser ,通过解析一个值已知的 XML 文档来测试 YahooParser 。 测试 XML 文档命名为 ny-weather.xml ,从 classpath 载入。我们将在Section 4.11, “添加单元测试资源”添加测试资源。 在我们这个 Maven 项目的目录布局中,文件 ny-weather.xml 可以从包含测试资源的目录—— ${basedir}/src/test/resources ——中找到,路径为 org/sonatype/mavenbook/weather/yahoo/ny-weather.xml 。 该文件作为一个 InputStream 被读入,传给 YahooParserparse() 方法。 parse() 方法返回一个 Weather 对象,该对象通过一系列 由 TestCase 定义的 assertEquals() 调用而被测试。

在同一目录下创建一个名为 WeatherFormatterTest.java 的文件。

Example 4.12. Simple Weather 的 WeatherFormatterTest 单元测试

package org.sonatype.mavenbook.weather.yahoo;

import java.io.InputStream;

import org.apache.commons.io.IOUtils;

import org.sonatype.mavenbook.weather.Weather;
import org.sonatype.mavenbook.weather.WeatherFormatter;
import org.sonatype.mavenbook.weather.YahooParser;

import junit.framework.TestCase;

public class WeatherFormatterTest extends TestCase {

  public WeatherFormatterTest(String name) {
    super(name);
  }

  public void testFormat() throws Exception {
    InputStream nyData = 
      getClass().getClassLoader().getResourceAsStream("ny-weather.xml");
    Weather weather = new YahooParser().parse( nyData );
    String formattedResult = new WeatherFormatter().format( weather );
    InputStream expected = 
      getClass().getClassLoader().getResourceAsStream("format-expected.dat");
    assertEquals( IOUtils.toString( expected ).trim(), formattedResult.trim() );
  }
}

该项目中的第二个单元测试测试 WeatherFormatter。 和 YahooParserTest 一样,WeatherFormatter 同样也继承 JUnit 的 TestCase 类。 这个单独的测试通过单元测试的 classpath 从 ${basedir}/src/test/resourcesorg/sonatype/mavenbook/weather/yahoo 目录读取同样的测试资源文件。 我们将会在Section 4.11, “添加单元测试资源”添加测试资源。 WeatherFormatterTest 首先调用 YahooParser 解析出 Weather 对象,然后用 WeatherFormatter 格式化这个对象。 我们的期望输出被存储在一个名为 format-expected.dat 的文件中,该文件存放在和 ny-weather.xml 同样的目录中。 要比较测试输出和期望输出,我们将期望输出作为 InputStream 读入,然后使用 Commons IO 的 IOUtils 类来把文件转化为 String 。 然后使用 assertEquals() 比较这个 String 和测试输出。

4.10. 添加测试范围依赖

Favicon

在类 WeatherFormatterTest 中我们用了一个来自于 Apache Commons IO 的工具—— IOUtils 类。 IOUtils 提供了许多很有帮助的静态方法,能帮助让很多工作摆脱繁琐的 I/O 操作。在这个单元测试中我们使用了 IOUtils.toString() 来复制 classpath 中资源 format.expected.dat 中的数据至 String。 不用 Commons IO 我们也能完成这件事情,但是那需要额外的六七行代码来处理像 InputStreamReaderStringWriter 这样的对象。我们使用 Commons IO 的主要原因是,能有理由添加对 Commons IO 的测试范围依赖。

测试范围依赖是一个只在测试编译和测试运行时在 classpath 中有效的依赖。如果你的项目是以 war 或者 ear 形式打包的,测试范围依赖就不会被包含在项目的打包输出中。 要添加一个测试范围依赖,在你项目的 dependencies 小节中添加如下 dependency 元素。

Example 4.13. 添加一个测试范围依赖

<project>
  ...
  <dependencies>
    ...
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-io</artifactId>
      <version>1.3.2</version>
      <scope>test</scope>
    </dependency>
    ...
  </dependencies>
</project>

当你往 pom.xml 中添加了这个依赖以后,运行 mvn dependency:resolve 你会看到 commons-io 出现在在依赖列表中,范围是 test 。 在我们可以运行该项目的单元测试之前,我们还需要做一件事情。 那就是创建单元测试依赖的 classpath 资源。测试范围依赖将在 9.4.1节 “依赖范围” 中详细解释。

4.11. 添加单元测试资源

Favicon

一个单元测试需要访问针对测试的一组资源。 通常你需要在测试 classpath 中存储一些包含期望结果的文件,以及包含模拟输入的文件。 在本项目中,我们为 YahooParserTest 准备了一个名为 ny-weather.xml 的测试 XML 文档,还有一个名为 format-expected.dat 的文件,包含了 WeatherFormatter 的期望输出。

要添加测试资源,你需要创建目录 src/test/resources 。 这是 Maven 寻找测试资源的默认目录。 在你的项目基础目录下运行下面的命令以创建该目录。

$ cd src/test
$ mkdir resources
$ cd resources

当你创建好这个资源目录之后,在资源目录下创建一个名为 format-expected.dat 的文件。

Example 4.14. Simple Weather 的 WeatherFormatterTest 期望输出

*********************************
 Current Weather Conditions for:
  New York, NY, US
  
 Temperature: 39
   Condition: Fair
    Humidity: 67
  Wind Chill: 39
*********************************

这个文件应该看起来很熟悉了,它和你用 Maven Exec 插件运行 Simple Weather 项目得到的输出是一样的。你需要在资源目录添加的第二个文件是 ny-weather.xml

Example 4.15. Simple Weather 的 YahooParserTest XML 输入

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<rss version="2.0" xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0" 
     xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">
 <channel>
 <title>Yahoo! Weather - New York, NY</title>
 <link>http://us.rd.yahoo.com/dailynews/rss/weather/New_York__NY/</link>
 <description>Yahoo! Weather for New York, NY</description>
 <language>en-us</language>
 <lastBuildDate>Sat, 10 Nov 2007 8:51 pm EDT</lastBuildDate>

 <ttl>60</ttl>
 <yweather:location city="New York" region="NY" country="US" />
 <yweather:units temperature="F" distance="mi" pressure="in" speed="mph" />
 <yweather:wind chill="39" direction="0" speed="0" />
 <yweather:atmosphere humidity="67" visibility="1609" pressure="30.18" 
                      rising="1" />
  <yweather:astronomy sunrise="6:36 am" sunset="4:43 pm" />
  <image>
 <title>Yahoo! Weather</title>

 <width>142</width>
 <height>18</height>
 <link>http://weather.yahoo.com/</link>
 <url>http://l.yimg.com/us.yimg.com/i/us/nws/th/main_142b.gif</url>
 </image>
 <item>
 <title>Conditions for New York, NY at 8:51 pm EDT</title>

  <geo:lat>40.67</geo:lat>
 <geo:long>-73.94</geo:long>
  <link>http://us.rd.yahoo.com/dailynews/rss/weather/New_York__NY/\</link>
 <pubDate>Sat, 10 Nov 2007 8:51 pm EDT</pubDate>
 <yweather:condition text="Fair" code="33" temp="39" 
                     date="Sat, 10 Nov 2007 8:51 pm EDT" />
 <description><![CDATA[
<img src="http://l.yimg.com/us.yimg.com/i/us/we/52/33.gif" /><br />
 <b>Current Conditions:</b><br />
 Fair, 39 F<BR /><BR />
 <b>Forecast:</b><BR />
  Sat - Partly Cloudy. High: 45 Low: 32<br />
  Sun - Sunny. High: 50 Low: 38<br />
 <br />
 ]]></description>
 <yweather:forecast day="Sat" date="10 Nov 2007" low="32" high="45" 
                    text="Partly Cloudy" code="29" />

<yweather:forecast day="Sun" date="11 Nov 2007" low="38" high="50" 
                   text="Sunny" code="32" />
  <guid isPermaLink="false">10002_2007_11_10_20_51_EDT</guid>
 </item>
</channel>
</rss>

该文件包含了一个给 YahooParserTest 用的 XML 文档。有了这个文件,我们不用从 Yahoo! Weather 获取 XML 响应就能测试 YahooParser 了。

4.12. 执行单元测试

Favicon

既然你的项目已经有单元测试了,那么让它们运行起来吧。 你不必为了运行单元测试做什么特殊的事情, test 阶段是 Maven 生命周期中常规的一部分。 当你运行 mvn package 或者 mvn install 的时候你也运行了测试。 如果你想要运行到 test 阶段为止的所有生命周期阶段,运行 mvn test

$ mvn test
...
[INFO] [surefire:test]
[INFO] Surefire report directory: ~/examples/simple-weather/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.sonatype.mavenbook.weather.yahoo.WeatherFormatterTest
0    INFO  YahooParser  - Creating XML Reader
177  INFO  YahooParser  - Parsing XML Response
239  INFO  WeatherFormatter  - Formatting Weather Data
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.547 sec
Running org.sonatype.mavenbook.weather.yahoo.YahooParserTest
475  INFO  YahooParser  - Creating XML Reader
483  INFO  YahooParser  - Parsing XML Response
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 sec

Results :

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

从命令行运行 mvn test 使 Maven 执行到 test 阶段为止的所有生命周期阶段。 Maven Surefire 插件有一个 test 目标,该目标被绑定在了 test 阶段。 test 目标执行项目中所有能在 src/test/java 找到的并且文件名与 **/Test*.java**/*Test.java**/*TestCase.java 匹配的所有单元测试。 在本例中,你能看到 Surefire 插件的 test 目标执行了 WeatherFormatterTestYahooParserTest 。 在 Maven Surefire 插件执行 JUnit 测试的时候,它同时也在 ${basedir}/target/surefire-reports 目录下生成 XML 和常规文本报告。 如果你的测试失败了,你可以去查看这个目录,里面有你单元测试生成的异常堆栈信息和错误信息。

4.12.1. 忽略测试失败

Favicon

通常,你会开发一个带有很多失败单元测试的系统。 如果你正在实践测试驱动开发(TDD),你可能会使用测试失败来衡量你离项目完成有多远。 如果你有失败的单元测试,但你仍然希望产生构建输出,你就必须告诉 Maven 让它忽略测试失败。 当 Maven 遇到一个测试失败,它默认的行为是停止当前的构建。 如果你希望继续构建项目,即使 Surefire 插件遇到了失败的单元测试,你就需要设置 Surefire 的 testFailureIgnore 这个配置属性为 true

Example 4.16. 忽略单元测试失败

<project>
  [...]
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <testFailureIgnore>true</testFailureIgnore&g