首先,用 Maven Archetype 插件创建这个 simple weather 项目的基本轮廓。 运行下面的命令,创建新项目
mvn archetype:create \ -DgroupId=org.sonatype.mavenbook.ch04 \ -DartifactId=simple-weather \ -DpackageName=org.sonatype.mavenbook \ -Dversion=1.0
* 给 archetype:create 目标传入了 version 参数。它覆写了默认值 1.0-SNAPSHOT
添加组织,法律和开发人员信息
<project> [...] <!--项目信息--> <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>[email protected]</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>
添加项目所需要的依赖
<project> [...] <!--定义依赖项--> <dependencies> <!--log4j--> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.14</version> </dependency> <!--dom4j--> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <!--jaxen--> <dependency> <groupId>jaxen</groupId> <artifactId>jaxen</artifactId> <version>1.1.1</version> </dependency> <!--velocity--> <dependency> <groupId>velocity</groupId> <artifactId>velocity</artifactId> <version>1.5</version> </dependency> <!--junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> [...] </project>
编写项目代码
Simple weather 应用程序完成以下三个任务:从 Yahoo! Weather 获取 XML 数据,解析 XML 数据,打印格式化的输出至标准输出。
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 模板生成结果。
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
模型对象只保存温度,湿度和当前天气情况的文字描述等信息。
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) String zipcode = "02101"; try { zipcode = args[0]; } catch( Exception e ) {} // Start the program new Main(zipcode).start(); } private String zip; public Main(String 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
,传给
YahooParser
。
YahooParser
解析
XML 数据并返回
Weather
对象。 最后,
WeatherFormatter
接受一个
Weather
对象并返回一个格式化的
String
,打印到标准输出。
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(String 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
用以解析这个数据源。
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 的
parse()
方法接受一个
InputStrem
然后返回一个
Weather
对象。 为了完成这一目标,它需要用 Dom4J 来解析
XML 文档。因为我们对 Yahoo! Weather
XML 命名空间的元素感兴趣,我们需要用
createXmlReader()
方法创建一个包含命名空间信息的
SAXReader
。 一旦我们创建了这个 reader 并且解析了文档,得到了返回的
org.dom4j.Document
,只需要简单的使用 XPath 表达式来获取需要的信息,而不是遍历所有的子元素。 本例中 Dom4J 提供了
XML 解析功能,而 Jaxen 提供了 XPath 功能。
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 然后返回格式化好的
String
。
format()
方法做的第一件事是从 classpath 载入名字为
output.vm
的 Velocity 模板。 然后我们创建一个
VelocityContext
,它需要一个
Weather
对象来填充。 一个
StringWriter
被创建用来存放模板生成的结果数据。通过调用
Velocity.evaluate()
,给模板赋值,结果作为 String 返回。
添加资源
本项目依赖于两个 classpath 资源: Main
类通过 classpath 资源 log4j.preoperties
来配置 Log4J , WeatherFormatter
引用了一个在 classpath 中的名为 output.vm
的 Velocity 模板。这两个资源都需要在默认包中(或者 classpath 的根目录)。
为了添加这些资源,我们需要在项目的基础目录下创建一个新的目录—— src/main/resources
。 由于任务 archetype:create
没有创建这个目录,我们需要通过在项目的基础目录下运行下面的命令来创建它:
log4j.properties
# Set root category priority to INFO and its only appender to CONSOLE. log4j.rootCategory=INFO, CONSOLE # Set the enterprise logger category to FATAL and its only appender to CONSOLE. #log4j.logger.org.apache.axis.enterprise=FATAL, 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
output.vm
********************************* Current Weather Conditions for: ${weather.city}, ${weather.region}, ${weather.country} Temperature: ${weather.temp} Condition: ${weather.condition} Humidity: ${weather.humidity} Wind Chill: ${weather.chill} *********************************
mvn install
这里发生一个错误,提示jvm1.3版本不支持泛型,所以需要手动将目标编译器
jvm版本换成1.5以上。
<project> [...] <build> <finalName>simple-weather</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
Maven Exec 插件进行命令行测试
mvn exec:java -Dexec.mainClass=org.sonatype.mavenbook.weather.Main
*想要查看 Exec 插件的完整描述,运行
mvn help:describe -Dplugin=exec -Dfull
浏览你的项目依赖
使用Exec是可以不安装依赖的情况下运行,想要知道程序都有哪些依赖则运行
mvn dependency:resolve
*开控制台打印所有依赖情况
*如果你想知道你项目的整个依赖树,你可以运行
mvn dependency:tree
*或者想要查看完整的依赖踪迹,包含那些因为冲突或者其它原因而被拒绝
引入的构件,打开 Maven 的调试标记运行
mvn install -X
编写单元测试代码
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() ); } }
通过解析一个值已知的 XML 文档来测试 YahooParser 。 测试 XML 文档命名为 ny-weather.xml ,从 classpath 载入。我们将在Section 4.11, “添加单元测试资源”添加测试资源。 在我们这个 Maven 项目的目录布局中,文件 ny-weather.xml 可以从包含测试资源的目录—— /usr/local/hudson/hudson-home/jobs/maven-guide-zh-to-production/workspace/content-zh/src/test/resources ——中找到,路径为 org/sonatype/mavenbook/weather/yahoo/ny-weather.xml 。 该文件作为一个 InputStream 被读入,传给 YahooParser 的 parse() 方法。 parse() 方法返回一个 Weather 对象,该对象通过一系列 由 TestCase 定义的 assertEquals() 调用而被测试。
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 ), formattedResult ); } }
WeatherFormatterTest 首先调用 YahooParser
解析出 Weather
对象,然后用 WeatherFormatter
格式化这个对象。 我们的期望输出被存储在一个名为 format-expected.dat
的文件中,该文件存放在和 ny-weather.xml
同样的目录中。 要比较测试输出和期望输出,我们将期望输出作为 InputStream
读入,然后使用 Commons IO 的 IOUtils
类来把文件转化为 String
。 然后使用 assertEquals()
比较这个 String 和测试输出。
添加测试范围依赖
在类 WeatherFormatterTest
中我们用了一个来自于 Apache Commons IO 的工具—— IOUtils
类。 IOUtils
提供了许多很有帮助的静态方法,能帮助让很多工作摆脱繁琐的 I/O 操作。在这个单元测试中我们使用了 IOUtils.toString()
来复制 classpath 中资源 format.expected.dat
中的数据至 String
。 不用 Commons IO 我们也能完成这件事情,但是那需要额外的六七行代码来处理像 InputStreamReader
和 StringWriter
这样的对象。我们使用 Commons IO 的主要原因是,能有理由添加对 Commons IO 的测试范围依赖。
<project> ... <dependencies> ... <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-io</artifactId> <version>1.3.2</version> <scope>test</scope> </dependency> ... </dependencies> </project>
添加单元测试资源
format-expected.dat
********************************* Current Weather Conditions for: New York, NY, US Temperature: 39 Condition: Fair Humidity: 67 Wind Chill: 39 *********************************
ny-weather.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/*http://weather.yahoo.com/forecast/10002_f.html</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/*http://weather.yahoo.com/forecast/10002_f.html</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 /> <a href="http://us.rd.yahoo.com/dailynews/rss/weather/New_York__NY/*http://weather.yahoo.com/forecast/10002_f.html">Full Forecast at Yahoo! Weather</a><BR/> (provided by The Weather Channel)<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><!-- p7.weather.re3.yahoo.com compressed/chunked Sat Nov 10 17:57:31 PST 2007 -->
该文件包含了一个给 YahooParserTest
用的 XML 文档。有了这个文件,我们不用从 Yahoo! Weather 获取 XML 响应就能测试 YahooParser
了。
执行单元测试
mvn test
构建一个打包好的命令行应用程序
配置 Maven 装配描述符
<project> [...] <build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build> [...] </project>
装配命令
mvn assembly:assembly
*装配完成后就可以直接分发执行了。但是这个包将所有文件拆开后重新打成一个整体,不太标准。
执行
java -cp simple-weather-1.0-jar-with-dependencies.jar org.sonatype.mavenbook.weather.Main 10002