8.5 约定优于配置
标准的重要性已不用过多强调,想象一下,如果不是所有程序员都基于HTTP协议开发Web应用,互联网会乱成怎样。各个版本的IE、Firefox等浏览器之间的差别已经让很多开发者头痛不已。而Java成功的重要原因之一就是它能屏蔽大部分操作系统的差异,XML流行的原因之一是所有语言都接受它。Maven当然还不能和这些既成功又成熟的技术相比,但Maven的用户都应该清楚,Maven提倡“约定优于配置”(Convention Over Configuration),这是Maven最核心的设计理念之一。
那么为什么要使用约定而不是自己更灵活的配置呢?原因之一是,使用约定可以大量减少配置。先看一个简单的Ant配置文件,见代码清单8-23。
代码清单8-23 构建简单项目使用的Ant配置文件
<project name="my-project"default="dist"basedir=".">
<description>
simple example build file
</description>
<!-设置构建的全局属性——>
<property name="src"location="src/main/java"/>
<property name="build"location="target/classes"/>
<property name="dist"location="target"/>
<target name="init">
<!-创建时间戳——>
<tstamp/>
<!-创建编译使用的构建目录——>
<mkdir dir="${build}"/>
</target>
<target name="compile"depends="init"
description="compile the source">
<!-将java代码从目录${src}编译至${build}——>
<javac srcdir="${src}"destdir="${build}"/>
</target>
<target name="dist"depends="compile"
description="generate the distribution">
<!-创建分发目录——>
<mkdir dir="${dist}/lib"/>
<!-将${build}目录的所有内容打包至MyProject-${DSTAMP}.jar file——>
<jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar"basedir="${build}"/>
</target>
<target name="clean"
description="clean up">
<!-删除${build}和${dist}目录树——>
<delete dir="${build}"/>
<delete dir="${dist}"/>
</target>
</project>
这段代码做的事情就是清除构建目录、创建目录、编译代码、复制依赖至目标目录,最后打包。这是一个项目构建要完成的最基本的事情,不过为此还是需要写很多的XML配置:源码目录是什么、编译目标目录是什么、分发目录是什么,等等。用户还需要记住各种Ant任务命令,如delete、mkdir、javac和jar。
做同样的事情,Maven需要什么配置呢?Maven只需要一个最简单的POM,见代码清单8-24。
代码清单8-24 构建简单项目使用的Maven配置文件
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>my-project</artifactId>
<version>1.0</version>
</project>
这段配置简单得令人惊奇,但为了获得这样简洁的配置,用户是需要付出一定的代价的,那就是遵循Maven的约定。Maven会假设用户的项目是这样的:
源码目录为src/main/java/
编译输出目录为target/classes/
打包方式为jar
包输出目录为target/
遵循约定虽然损失了一定的灵活性,用户不能随意安排目录结构,但是却能减少配置。更重要的是,遵循约定能够帮助用户遵守构建标准。
如果没有约定,10个项目可能使用10种不同的项目目录结构,这意味着交流学习成本的增加,当新成员加入项目的时候,它就不得不花时间去学习这种构建配置。而有了Maven的约定,大家都知道什么目录放什么内容。此外,与Ant的自定义目标名称不同,Maven在命令行暴露的用户接口是统一的,像mvn clean install这样的命令可以用来构建几乎任何的Maven项目。
也许这时候有读者会问,如果我不想遵守约定该怎么办?这时,请首先问自己三遍,你真的需要这么做吗?如果仅仅是因为喜好,就不要耍个性,个性往往意味着牺牲通用性,意味着增加无谓的复杂度。例如,Maven允许你自定义源码目录,如代码清单8-25所示。
代码清单8-25 使用Maven自定义源码目录
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>my-project</artifactId>
<version>1.0</version>
<build>
<sourceDirectory>src/java</sourceDirectory>
</build>
</project>
该例中源码目录就成了src/java而不是默认的src/main/java。但这往往会造成交流问题,习惯Maven的人会奇怪,源代码去哪里了?当这种自定义大量存在的时候,交流成本就会大大提高。只有在一些特殊的情况下,这种自定义配置的方式才应该被正确使用以解决实际问题。例如你在处理遗留代码,并且没有办法更改原来的目录结构,这个时候就只能让Maven妥协。
本书曾多次提到超级POM,任何一个Maven项目都隐式地继承自该POM,这有点类似于任何一个Java类都隐式地继承于Object类。因此,大量超级POM的配置都会被所有Maven项目继承,这些配置也就成为了Maven所提倡的约定。
对于Maven 3,超级POM在文件$MAVEN_HOME/lib/maven-model-builder-x.x.x.jar中的org/apache/maven/model/pom-4.0.0.xml路径下。对于Maven 2,超级POM在文件$MAVEN_HOME/lib/maven-x.x.x-uber.jar中的org/apache/maven/project/pom-4.0.0.xml目录下。这里的x.x.x表示Maven的具体版本。
超级POM的内容在Maven 2和Maven 3中基本一致,现在分段看一下,见代码清单8-26。
代码清单8-26 超级POM中关于仓库的定义
<repositories>
<repository>
<id>central</id>
<name>Maven Repository Switchboard</name>
<url>http://repo1.maven.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</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定义了仓库及插件仓库,两者的地址都为中央仓库http://repo1.maven.org/maven2,并且都关闭了SNAPSHOT的支持。这也就解释了为什么Maven默认就可以按需要从中央仓库下载构件。
再看以下内容,见代码清单8-27。
代码清单8-27 超级POM中关于项目结构的定义
<build>
<directory>${project.basedir}/target</directory>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<finalName>${project.artifactId}-${project.version}</finalName>
<testOutputDirectory>${project.build.directory}/test-classes</testOut-
putDirectory>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDi-
rectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
</testResources>
这里依次定义了项目的主输出目录、主代码输出目录、最终构件的名称格式、测试代码输出目录、主源码目录、脚本源码目录、测试源码目录、主资源目录和测试资源目录。这就是Maven项目结构的约定。
紧接着超级POM为核心插件设定版本,见代码清单8-28。
代码清单8-28 超级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>
</build>
由于篇幅原因,这里不完整罗列,读者可自己找到超级POM了解插件的具体版本。Maven设定核心插件的原因是防止由于插件版本的变化而造成构建不稳定。
超级POM的最后是关于项目报告输出目录的配置和一个关于项目发布的profile,这里暂不深入解释。后面会有相关的章节讨论这两项配置。
可以看到,超级POM实际上很简单,但从这个POM我们就能够知晓Maven约定的由来,不仅理解了什么是约定,为什么要遵循约定,还能明白约定是如何实现的。