maven是一個非常經典的和通用的項目管理工具,雖然現在熱炒gradle將作爲下一代 項目管理工具來取代maven,但是 由於maven強大和健全的功能,maven還有很強的生命力。
本文將介紹maven對於項目生命週期的設計以及原理。
讀完本文,你將瞭解到:一、maven對項目生命週期的抽象--三大項目生命週期
二、maven對項目默認生命週期的抽象
三、maven指令與生命週期階段的關係
四、maven生命週期各個階段的行爲與maven默認行爲
五、maven項目的目錄結構
六、maven爲生命週期階段綁定特定行爲動作的機制即插件原理
一、 maven對項目生命週期的抽象--三大項目生命週期
maven從項目的三個不同的角度,定義了單套生命週期,三套生命週期是相互獨立的,它們之間不會相互影響。
默認構建生命週期(Default Lifeclyle): 該生命週期表示這項目的構建過程,定義了一個項目的構建要經過的不同的階段。
清理生命週期(Clean Lifecycle): 該生命週期負責清理項目中的多餘信息,保持項目資源和代碼的整潔性。一般拿來清空directory(即一般的target)目錄下的文件。
站點管理生命週期(Site Lifecycle) :向我們創建一個項目時,我們有時候需要提供一個站點,來介紹這個項目的信息,如項目介紹,項目進度狀態、項目組成成員,版本控制信息,項目javadoc索引信息等等。站點管理生命週期定義了站點管理過程的各個階段。
本文只介紹maven項目默認的生命週期,其他兩個生命週期將另起博文介紹。
二、 maven對項目默認生命週期的抽象
如何查看maven對默認生命週期的定義?
maven將其架構和結構的組織放置到了components.xml 配置文件中,該配置文件的路徑是:
apache-maven-${version}\lib\maven-core-${version}.jar\META-INFO\plexus\conponents.xml文件中。其中,我們可以看到關於default生命週期XML節點配置信息:
<component> <role>org.apache.maven.lifecycle.Lifecycle</role> <implementation>org.apache.maven.lifecycle.Lifecycle</implementation> <role-hint>default</role-hint> <configuration> <id>default</id> <phases> <phase>validate</phase> <phase>initialize</phase> <phase>generate-sources</phase> <phase>process-sources</phase> <phase>generate-resources</phase> <phase>process-resources</phase> <phase>compile</phase> <phase>process-classes</phase> <phase>generate-test-sources</phase> <phase>process-test-sources</phase> <phase>generate-test-resources</phase> <phase>process-test-resources</phase> <phase>test-compile</phase> <phase>process-test-classes</phase> <phase>test</phase> <phase>prepare-package</phase> <phase>package</phase> <phase>pre-integration-test</phase> <phase>integration-test</phase> <phase>post-integration-test</phase> <phase>verify</phase> <phase>install</phase> <phase>deploy</phase> </phases> </configuration> </component>
maven根據一個項目的生命週期的每個階段,將一個項目的生命週期抽象成了如上圖所示的23個階段。而每一個階段應該幹什麼事情由用戶決定。換句話說,maven爲每一個階段設計了接口,你可以爲每一階段自己定義一個接口,進而實現對應階段應該有的行爲。關於如何爲某個生命週期階段綁定自定義的行爲,我將在後面的章節介紹。
三、 maven指令與生命週期階段的關係
四、maven生命週期各個階段的行爲與maven默認行爲
使用過maven的讀者會經常使用這些maven指令:
mvn compile //讓當前項目經歷生命週期中的1-->7 階段 :完成編譯主源代碼編譯 mvn package //讓當前項目經歷生命週期中的1-->17階段 :完成打包 mvn install //讓當前項目經歷生命週期中的1-->22階段 :完成包安裝到本地倉庫 mvn deploy //讓當前生命經歷生命週期中的1-->23階段 :完成包部署到中心庫中
在經歷這些生命週期的階段中,每個階段會理論上會有相應的處理操作。但是,在實際的項目開發過程中, 並不是所有的生命週期階段都是必須的。
然而,在實際的開發過程中,往往我們的項目的一些生命週期的階段不需要相應的行爲,我們只需要關心其中某些重要的生命週期階段而已。下面,請看一下日常開發中,我們需要關注的生命週期階段,即廣大開發人員對項目週期階段處理的約定:
1).應該將resource資源文件準備好,放到指定的target目錄下----process-resources 階段;
2).將java源文件編譯成.class文件,然後將class 文件放置到對應的target目錄下----compile階段;
3).將test類型的resource移動到指定的 target目錄下------process-test-resource階段;
4).將test類型的java 源文件編譯成class文件,然後放置到指定的target目錄下------test-compile階段;
5).運行test測試用例-------test階段;
6).將compile階段編譯的class文件和resource資源打包成jar包或war包--------package階段;
7).將生成的包安裝到本地倉庫中------install階段
8).將生成的包部署到遠程倉庫中-----deploy階段
由上面的約定可以看出,在大多數情況下,大家關心的項目生命週期階段僅僅是上面的8個而已。跟上面maven對生命週期階段23個的抽象相比,這就少的很多了。
基於類似的約定,maven默認地爲一些不同類型的maven項目生命週期的階段實現了默認的行爲。
maven 在設計上將生命週期階段的抽象和對應階段應該執行的行爲實現分離開,maven這些實現放到了插件中,這些插件本質上是實現了maven留在各個生命週期階段的接口。關於插件的問題,我將另外寫一篇博文介紹。
如下圖所示,maven針對不同打包類型的maven項目的生命週期階段綁定了對應的默認行爲:
如上圖所示,對於不同的打包格式的項目而言,maven爲特定類型的包格式項目在不同的生命週期階段的默認行爲。
而對於我們經常使用的jar和war包格式的項目而言,maven總共爲其規定了以下幾個生命週期階段的默認行爲:
五、 maven項目的目錄結構
well,每個項目工程,都有非常繁瑣的目錄結構,每個目錄都有不同的作用。請記住這一點,目錄的劃分是根據需要來的,每個目錄有其特定的功能。目錄本質上就是一個文件或文件夾路徑而已。那麼,我們換一個思路考慮:一個項目的文件結構需要組織什麼信息呢?讓我們來看一下功能的劃分:
如上圖所示,你會看到maven項目裏不同功能類型的目錄定義以及maven默認的目錄的路徑。
如何修改默認的目錄配置在maven項目工程對應project的 pom.xml中,在<project>--><build>節點下,你可以指定自己的目錄路徑信息:
<build> <!-- 目錄信息維護,用戶可以指定自己的目錄路徑 --> <sourceDirectory>E:\intellis\maven-principle\phase-echo\src\main\java</sourceDirectory> <scriptSourceDirectory>E:\intellis\maven-principle\phase-echo\src\main\scripts</scriptSourceDirectory> <testSourceDirectory>E:\intellis\maven-principle\phase-echo\src\test\java</testSourceDirectory> <outputDirectory>E:\intellis\maven-principle\phase-echo\target\classes</outputDirectory> <testOutputDirectory>E:\intellis\maven-principle\phase-echo\target\test-classes</testOutputDirectory> <!-- 注意,對resource而言,可以有很多個resource路徑的配置,你只需要指定對應的路徑是resource即可 --> <resources> <resource> <directory>E:\intellis\maven-principle\phase-echo\src\main\resources</directory> </resource> </resources> <!-- 注意,對resource而言,可以有很多個resource路徑的配置,你只需要指定對應的路徑是resource即可 --> <testResources> <testResource> <directory>E:\intellis\maven-principle\phase-echo\src\test\resources</directory> </testResource> </testResources> <directory>E:\intellis\maven-principle\phase-echo\target</directory> </build>
請注意:對於maven管理項目工程的生命週期的操作上, 都發生在上述的幾種目錄中。換句話說,實質上,maven的項目管理的整個過程,就是圍繞着對上述幾種文件目錄中內容的操作。
六、maven爲生命週期階段綁定特定行爲動作的機制(即插件原理)
爲maven生命週期的某些階段綁定特定行爲或動作,簡單點就是調用一段代碼而已,maven將需要執行的邏輯抽象成了一個接口,接口爲 Mojo。Mojo是 Maven Old plain Java Object的簡寫,表示的意思是Mojo是maven的一個簡單的對象。如下圖所示:
maven通過爲某一個項目的生命週期階段綁定若干個Mojo,然後依次執行Mojo.execute()方法,從而實現特定生命週期應該執行的動作。
舉例:比如,對於生命週期階段process-resources,maven默認地爲其綁定了一個Mojo: org.apache.maven.plugin.resources.ResourcesMojo。
當需要經歷process-resources階段時,maven將會創建一個ResourcesMojo 實例instance,然後調用instance.execute()方法。
@Mojo( name = "resources", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, threadSafe = true ) public class ResourcesMojo extends AbstractMojo implements Contextualizable { /** 下面若干個attribute ,maven 讀取pom.xml內的配置信息,這裏的attribute對應着pom.xml裏的配置,如果在這裏聲明瞭,maven在創建Mojo 實例 instance的時候,會將這些值注入到instance內,供Mojo instance使用 */ /** * The character encoding scheme to be applied when filtering resources. */ @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" ) protected String encoding; /** * The output directory into which to copy the resources. */ @Parameter( defaultValue = "${project.build.outputDirectory}", required = true ) private File outputDirectory; /** * The list of resources we want to transfer. */ @Parameter( defaultValue = "${project.resources}", required = true, readonly = true ) private List<Resource> resources; /** * */ @Parameter( defaultValue = "${project}", required = true, readonly = true ) protected MavenProject project; /** * The list of additional filter properties files to be used along with System and project * properties, which would be used for the filtering. * <br/> * See also: {@link ResourcesMojo#filters}. * * @since 2.4 */ @Parameter( defaultValue = "${project.build.filters}", readonly = true ) protected List<String> buildFilters; /** * The list of extra filter properties files to be used along with System properties, * project properties, and filter properties files specified in the POM build/filters section, * which should be used for the filtering during the current mojo execution. * <br/> * Normally, these will be configured from a plugin's execution section, to provide a different * set of filters for a particular execution. For instance, starting in Maven 2.2.0, you have the * option of configuring executions with the id's <code>default-resources</code> and * <code>default-testResources</code> to supply different configurations for the two * different types of resources. By supplying <code>extraFilters</code> configurations, you * can separate which filters are used for which type of resource. */ @Parameter protected List<String> filters; /** * If false, don't use the filters specified in the build/filters section of the POM when * processing resources in this mojo execution. * <br/> * See also: {@link ResourcesMojo#buildFilters} and {@link ResourcesMojo#filters} * * @since 2.4 */ @Parameter( defaultValue = "true" ) protected boolean useBuildFilters; /** * */ @Component( role = MavenResourcesFiltering.class, hint = "default" ) protected MavenResourcesFiltering mavenResourcesFiltering; /** * */ @Parameter( defaultValue = "${session}", required = true, readonly = true ) protected MavenSession session; /** * Expression preceded with the String won't be interpolated * \${foo} will be replaced with ${foo} * * @since 2.3 */ @Parameter( property = "maven.resources.escapeString" ) protected String escapeString; /** * Overwrite existing files even if the destination files are newer. * * @since 2.3 */ @Parameter( property = "maven.resources.overwrite", defaultValue = "false" ) private boolean overwrite; /** * Copy any empty directories included in the Resources. * * @since 2.3 */ @Parameter( property = "maven.resources.includeEmptyDirs", defaultValue = "false" ) protected boolean includeEmptyDirs; /** * Additional file extensions to not apply filtering (already defined are : jpg, jpeg, gif, bmp, png) * * @since 2.3 */ @Parameter protected List<String> nonFilteredFileExtensions; /** * Whether to escape backslashes and colons in windows-style paths. * * @since 2.4 */ @Parameter( property = "maven.resources.escapeWindowsPaths", defaultValue = "true" ) protected boolean escapeWindowsPaths; /** * <p> * Set of delimiters for expressions to filter within the resources. These delimiters are specified in the * form 'beginToken*endToken'. If no '*' is given, the delimiter is assumed to be the same for start and end. * </p><p> * So, the default filtering delimiters might be specified as: * </p> * <pre> * <delimiters> * <delimiter>${*}</delimiter> * <delimiter>@</delimiter> * </delimiters> * </pre> * <p> * Since the '@' delimiter is the same on both ends, we don't need to specify '@*@' (though we can). * </p> * * @since 2.4 */ @Parameter protected List<String> delimiters; /** * @since 2.4 */ @Parameter( defaultValue = "true" ) protected boolean useDefaultDelimiters; /** * <p> * List of plexus components hint which implements {@link MavenResourcesFiltering#filterResources(MavenResourcesExecution)}. * They will be executed after the resources copying/filtering. * </p> * * @since 2.4 */ @Parameter private List<String> mavenFilteringHints; /** * @since 2.4 */ private PlexusContainer plexusContainer; /** * @since 2.4 */ private List<MavenResourcesFiltering> mavenFilteringComponents = new ArrayList<MavenResourcesFiltering>(); /** * stop searching endToken at the end of line * * @since 2.5 */ @Parameter( property = "maven.resources.supportMultiLineFiltering", defaultValue = "false" ) private boolean supportMultiLineFiltering; public void contextualize( Context context ) throws ContextException { plexusContainer = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY ); } public void execute() throws MojoExecutionException { try { if ( StringUtils.isEmpty( encoding ) && isFilteringEnabled( getResources() ) ) { getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING + ", i.e. build is platform dependent!" ); } //獲取資源文件過濾器配置 List filters = getCombinedFiltersList(); //根據現有配置信息創建Resources處理器對象實例 MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution( getResources(), getOutputDirectory(), project, encoding, filters, Collections.<String>emptyList(), session ); //windows路徑處理 mavenResourcesExecution.setEscapeWindowsPaths( escapeWindowsPaths ); // never include project build filters in this call, since we've already accounted for the POM build filters // above, in getCombinedFiltersList(). mavenResourcesExecution.setInjectProjectBuildFilters( false ); mavenResourcesExecution.setEscapeString( escapeString ); mavenResourcesExecution.setOverwrite( overwrite ); mavenResourcesExecution.setIncludeEmptyDirs( includeEmptyDirs ); mavenResourcesExecution.setSupportMultiLineFiltering( supportMultiLineFiltering ); // if these are NOT set, just use the defaults, which are '${*}' and '@'. if ( delimiters != null && !delimiters.isEmpty() ) { LinkedHashSet<String> delims = new LinkedHashSet<String>(); if ( useDefaultDelimiters ) { delims.addAll( mavenResourcesExecution.getDelimiters() ); } for ( String delim : delimiters ) { if ( delim == null ) { // FIXME: ${filter:*} could also trigger this condition. Need a better long-term solution. delims.add( "${*}" ); } else { delims.add( delim ); } } mavenResourcesExecution.setDelimiters( delims ); } if ( nonFilteredFileExtensions != null ) { mavenResourcesExecution.setNonFilteredFileExtensions( nonFilteredFileExtensions ); } mavenResourcesFiltering.filterResources( mavenResourcesExecution ); //執行Resource文件拷貝 executeUserFilterComponents( mavenResourcesExecution ); } catch ( MavenFilteringException e ) { throw new MojoExecutionException( e.getMessage(), e ); } } /** * @since 2.5 */ protected void executeUserFilterComponents( MavenResourcesExecution mavenResourcesExecution ) throws MojoExecutionException, MavenFilteringException { if ( mavenFilteringHints != null ) { for ( Iterator ite = mavenFilteringHints.iterator(); ite.hasNext(); ) { String hint = (String) ite.next(); try { mavenFilteringComponents.add( (MavenResourcesFiltering) plexusContainer.lookup( MavenResourcesFiltering.class.getName(), hint ) ); } catch ( ComponentLookupException e ) { throw new MojoExecutionException( e.getMessage(), e ); } } } else { getLog().debug( "no use filter components" ); } if ( mavenFilteringComponents != null && !mavenFilteringComponents.isEmpty() ) { getLog().debug( "execute user filters" ); for ( MavenResourcesFiltering filter : mavenFilteringComponents ) { filter.filterResources( mavenResourcesExecution ); } } } protected List<String> getCombinedFiltersList() { if ( filters == null || filters.isEmpty() ) { return useBuildFilters ? buildFilters : null; } else { List<String> result = new ArrayList<String>(); if ( useBuildFilters && buildFilters != null && !buildFilters.isEmpty() ) { result.addAll( buildFilters ); } result.addAll( filters ); return result; } } /** * Determines whether filtering has been enabled for any resource. * * @param resources The set of resources to check for filtering, may be <code>null</code>. * @return <code>true</code> if at least one resource uses filtering, <code>false</code> otherwise. */ private boolean isFilteringEnabled( Collection<Resource> resources ) { if ( resources != null ) { for ( Resource resource : resources ) { if ( resource.isFiltering() ) { return true; } } } return false; } }上面只是介紹了Maven生命週期階段綁定執行代碼的基本模式,由於maven的生命週期衆多,並且每個生命週期內有可能綁定多個Mojo,如果使用上述的模式簡單關聯的話,會顯得結構組織很亂。
maven會根據Mojo功能的劃分,將具有相似功能的Mojo放到一個插件中。並且某一個特定的Mojo能實現的功能稱爲 goal,即目標,表明該Mojo能實現什麼目標。
例如,我們項目生命週期有兩個階段:compile 和 test-compile,這兩階段都是需要將java源代碼編譯成class文件中,相對應地,compile和test-compiler分別被綁定到了org.apache.maven.plugin.compiler.CompilerMojo 和org.apache.maven.plugin.compiler.TestCompilerMojo上:
如何查看maven各個生命週期階段和插件的綁定情況maven默認實現上,會爲各個常用的生命週期根據約定綁定特定的插件目標。maven將這些配置放置到了:
apache-maven-${version}\lib\maven-core-${version}.jar\META-INFO\plexus\default-binds.xml文件中,針對不同打包類型的項目,其默認綁定情況也會不一樣,我們先看一下常用的jar包類型和war包類型的項目默認綁定情況:
<!-- jar包格式的項目生命週期各個階段默認綁定情況 --> <component> <role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role> <role-hint>jar</role-hint> <implementation>org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping</implementation> <configuration> <lifecycles> <lifecycle> <id>default</id> <!-- START SNIPPET: jar-lifecycle --> <phases> <!-- 插件綁定的格式: <plugin-groupid>:<plugin-artifactid>:<version>:goal --> <process-resources> org.apache.maven.plugins:maven-resources-plugin:2.6:resources </process-resources> <compile> org.apache.maven.plugins:maven-compiler-plugin:3.1:compile </compile> <process-test-resources> org.apache.maven.plugins:maven-resources-plugin:2.6:testResources </process-test-resources> <test-compile> org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile </test-compile> <test> org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test </test> <package> org.apache.maven.plugins:maven-jar-plugin:2.4:jar </package> <install> org.apache.maven.plugins:maven-install-plugin:2.4:install </install> <deploy> org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy </deploy> </phases> <!-- END SNIPPET: jar-lifecycle --> </lifecycle> </lifecycles> </configuration> </component> <!-- war包格式的項目生命週期各個階段默認綁定情況 --> <component> <role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role> <role-hint>war</role-hint> <implementation>org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping</implementation> <configuration> <lifecycles> <lifecycle> <id>default</id> <!-- START SNIPPET: war-lifecycle --> <phases> <process-resources> org.apache.maven.plugins:maven-resources-plugin:2.6:resources </process-resources> <compile> org.apache.maven.plugins:maven-compiler-plugin:3.1:compile </compile> <process-test-resources> org.apache.maven.plugins:maven-resources-plugin:2.6:testResources </process-test-resources> <test-compile> org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile </test-compile> <test> org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test </test> <package> org.apache.maven.plugins:maven-war-plugin:2.2:war </package> <install> org.apache.maven.plugins:maven-install-plugin:2.4:install </install> <deploy> org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy </deploy> </phases> <!-- END SNIPPET: war-lifecycle --> </lifecycle> </lifecycles> </configuration> </component>
如果你想查看當前maven有多少插件,每個插件都幹了什麼,你可以訪問 maven plugin主頁瞭解 : http://maven.apache.org/plugins/index.html
寫在最後
本文旨在向讀者介紹maven默認生命週期的工作機制,以及maven在項目構建過程中的基本原理和機制。
本文介紹的比較寬泛,還沒有深入到maven插件底層,隨後一篇博文將會詳細介紹插件,解析插件的工作原理,分析當前maven默認定義的插件,並最終自己定義插件完成特定功能,敬請關注!
由於本人技術有限,如果本人有任何錯誤和紕漏,請讀者慷慨指出,共同學習,共同進步!
2016.1.15