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约定的由来,不仅理解了什么是约定,为什么要遵循约定,还能明白约定是如何实现的。