在前三篇實作我們完成了 core, desktop, web 三個專案,接下來要給大家介紹 Maven 最典型的一種應用 — 多模組專案
標籤: Maven
Maven-實作篇 (3)
上一回我們完成了用戶查詢系統–視窗程式的練習,這次我們從Web版開始吧!
請先確認Eclipse 已經整合了Tomcat,如果還沒,請參考Java web app 開發第一步 – 整合Eclipse + Tomcat
Web
- 依照以下資訊,建立 Maven 專案 (使用 maven-archetype-webapp)
- groupId: com.appx
- artifactId: user-service-web
- package: com.appx.userservice.web
- version: 1.0-SNAPSHOT
- 將 src/main/webapp/WEB-INF/web.xml 取代為以下內容
123456<?xml version="1.0" encoding="UTF-8"?><web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"version="3.1"><display-name>Archetype Created Web Application</display-name></web-app>
* webapp archetype 預設生成的 web.xml 定義了舊版的 Servlet 2.3的dtd,會使得一些比較新的JSP特性像是EL不能正常運作,因此必須置換為上面的xml - 將user-service-web 專案放上 Tomcat執行
- 在 http://localhost:8080/user-service-web/ 看到 “Hello World!” 表示新建的 Maven web 專案運作正常!
- 在POM.xml 加入以下 compiler plugin
1234567891011<plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.6.1</version><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins> - 移除JUnit dependency ,並加入下列 dependency (servlet-api 的 scope 為 provided,如果忘記的同學可以參考Maven-10-核心概念 dependency management )
12345678910111213<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope></dependency><dependency><groupId>com.appx</groupId><artifactId>user-service-core</artifactId><version>1.0-SNAPSHOT</version></dependency> - 在 src/main 底下建立 java 資料夾,下載以下程式,並放至 src/main/java 底下這個package com.appx.userservice.web
https://drive.google.com/open?id=0B0C215CJ0AjobThwTlg3WXFkNmM - 在 src/main/webapp 放置以下檔案
https://drive.google.com/open?id=0B0C215CJ0AjoOHJOeUVzV2JnQWc - 重啟 Tomcat ,確認搜尋功能正常運作
- 輸入 user id 為 1
-
搜尋結果為 tom
- 執行 mvn package,產生可直接部署的war檔
Maven-實作篇 (2)
延續上一回的實作,現在我們將從桌面視窗程式開始!
Desktop
- 依照以下資訊,建立 Maven 專案 (使用 maven-archetype-quickstart)
- groupId: com.appx
- artifactId: user-service-desktop
- package: com.appx.userservice.desktop
- version: 1.0-SNAPSHOT
- 這次不實作unit test,所以把 src/test/java 底下 AppTest.java 刪除,同時也移除 POM.xml 裡面 junit 的 dependency
- 在 POM.xml 增加以下 plugin 設定,除了 compiler plugin,也使用了 maven-shade-plugin 用來把整個桌面視窗程式打包成單一 jar 檔(包含dependency),可以看到在 package 這個lifecycle build phase 綁定了 shade goal,並且必須指定 main 方法所在的class
(maven lifecycle的概念非常重要,請參考之前的文章)
1234567891011121314151617181920212223242526272829303132333435363738<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.6.1</version><configuration><source>1.8</source><target>1.8</target></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.0.0</version><executions><!-- Run shade goal on package phase --><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><!-- add Main-Class to manifest file --><transformerimplementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>com.appx.userservice.desktop.App</mainClass></transformer></transformers></configuration></execution></executions></plugin></plugins></build> - 加入 core 專案作為 dependency
12345<dependency><groupId>com.appx</groupId><artifactId>user-service-core</artifactId><version>1.0-SNAPSHOT</version></dependency> - 下載以下程式,並放至 src/main/java 底下這個package com.appx.userservice.desktop
https://drive.google.com/open?id=0B0C215CJ0AjoX2tPLVpVZlJYQkk - 執行 mvn package ,可以發現 target 資料夾底下多了兩個 jar,其中 user-service-desktop-1.0-SNAPSHOT.jar 包含了相關的 dependencies
- 可以使用 jar -tf 指令來比較兩個 jar 的內容
jar -tf user-service-desktop-1.0-SNAPSHOT.jar
jar -tf original-user-service-desktop-1.0-SNAPSHOT.jar - 執行 jar ,確認一切都 ok,id可以輸入 1 ~ 3,會看到查詢結果
Maven-實作篇 (1)
接下來會帶大家動手實作幾個 Maven 專案,從練習過程中我們會更進一步了解到 Maven 所帶來各種方便的特性!
我們將會建立一個簡單的用戶查詢系統,由三個專案所組成,分別是 core, desktop, web,其中 desktop, web 專案會將core 當作 dependency,如下圖所示:
- core 專案代表整個系統的核心功能所在
- desktop 專案負責視窗程式的操作介面
- web 專案負責網頁的操作介面
Maven-10-核心概念 dependency management
對於開發者來說,Maven最強大的功能莫過於 dependency management了!
在之前文章 Maven-05-一個簡單的POM範例 我們看到POM裡定義了一組 junit 的dependency,所以在執行 mvn install 時,Maven 會先檢查本機的 repository 是否有 junit 相關的 artifact ,如果沒有就會從網路上下載,然後放到 ~/.m2/repository/junit/junit/3.8.1/junit-3.8.1.jar
這邊我們可以觀察到 Maven 自動下載的東西除了jar 檔,還包括了其他檔案像是包了原始碼的jar,還有一個很關鍵的 junit-3.8.1.pom,為什麼 Maven 還會下載 junit的POM呢?
大家可以試著想想,如果今天我們需要的不是junit 這麼單純的套件,我們需要在專案裡使用 Hibernate,從它的 POM 我們得知 Hibernate 本身也有許多 dependencies,難道我們必須在自己專案的 POM 把這些 Hibernate 的 dependency 也全部都寫出來嗎??
答案當然是:NO !!
我們只需要定義 Hibernate dependency,Maven 就會自動把它關聯的 dependency 也一併下載回來,這種機制稱為 “transitive dependencies”, 實際上的運作方式是 Maven 會去查看 dependency 的 POM,並確認是否有相關的 dependency
在junit 的 <dependency> 定義裡,有一個 <scope>test</scope>,這是另一個重要的概念 ,常用的 dependency scope 有下列幾種:
- compile: 如果沒有定義<scope>, 預設值就是 compile,顧名思義指的是在編譯專案程式碼時需要這個 dependency
- test: 指的是在這兩個 goal compiler:testCompile (編譯測試程式碼), surefire:test (執行unit test) 才會引用的dependency,正式產出的 jar (或其它格式) 並不會包含 scope 為 test 的 dependency
- provided: 通當在 web 專案比較常看到這種 scope,指的是在編譯時有需要,但在正式產出war時並不會包含在一起,像是 jsp, servlet 這種 dependency,我們會在開發時用到相關的api,但實際提供這些 dependency 的是運行 web app 的 ap server
更多 dependency scope 可參考
https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Scope
Maven-09-核心概念 repositories
我們之前在 Maven-04-建置專案 裡執行了 mvn install 指令,如果是初次執行的話,會發現 maven 會自動從遠端 repository 下載很多 plugin 相關的檔案,這些檔案會存放在本機個人目錄下 .m2/repository 資料夾裡。現在我們一起來了解有關 repository 更多資訊吧!
Maven 官方預設的遠端 repository 在 http://repo1.maven.org/maven2/ ,如果有需要的話,像是公司內部私有專案的需求, Maven 也支援自行架設的 repository,其中比較有名的像是:
只要修改 maven 安裝目錄下的 conf/settings.xml 就可以指向自行架設的 repository
Maven repository 儲存了大大小小專案的成品 (project artifact),其中的目錄結構完全符合我們先前介紹過的 Maven coordinates,大家可以試著從官方的 http://repo1.maven.org/maven2/ 來觀察,這邊以 hibernate-core 版本 5.1.0.Final 為例子來說明
- hibernate-core 版本 5.1.0.Final 的 coordinates 如下,完整 POM 可參考這裡
123<groupId>org.hibernate</groupId><artifactId>hibernate-core</artifactId><version>5.1.0.Final</version>
(packaging 預設 為 jar) - jar 檔的完整路徑如下
http://repo1.maven.org/maven2/org/hibernate/hibernate-core/5.1.0.Final/hibernate-core-5.1.0.Final.jar - 路徑的組成,都遵照一定的格式
repository_location/groupId/artifactId/version/artifactId–version.packaging
Maven 從遠端下載的 project artifacts 會存在放本機 local repository 裡 (預設是 個人目錄 .m2/repository,也可以透過 settings.xml 修改),之後當有需要在專案裡引用這些 artifacts 時,Maven 就會直接參考 local repository 而不再從網路上尋找,順帶一提,local repository 裡面的資料夾結構,也和遠端 repository 一樣
mvn install 執行之後產生的project artifact,最後就會放置在 local repository 裡,這麼做的目的是日後可以供其它專案來參照引用。在下一篇,我們將為大家介紹對開發人員來說 Maven 最最最重要的功能 — dependency 管理
Maven-08-核心概念 coordinates
在 Maven-05-一個簡單的POM範例 我們稍微給大家介紹了最基本的 POM 內容,也提到了 POM 一開始的 groupId, artifactId, version 這三個資訊可以構成一個該專案特有的識別名稱
這回就針對這幾項做更進一步的說明
- groupId: 團隊名稱,通常的命令慣例是把網址倒過來寫的表示方式,例如 com.appx, org.apache 等等
- artifactId: 專案名稱
- version: 版本號,如果是還在開發中的專案,通常會以 SNAPSHOT 結尾
以上三個資訊可以做為一個專案的唯一識別名稱,不會有兩個專案具有同樣的groupId:artifactId:version
packaging (打包方式) 不是唯一識別名稱的一部份,不過也是 maven coordinates 的組成之一,這幾個資訊就構成了我們在各個公私有的 maven repositories 茫茫大海中,找尋到特定專案的唯一識別名稱
除了 POM 一開始宣告了該專案的 coordinates,如果想為專案增加其它 dependencies ,也是透過 coordinates 來指向其它專案,如上圖 <dependency> 內容所示,甚至在使用 plugin 時,同樣也是透過 coordinates 來指明特定的 plugin,如下所示
Maven-07-核心概念 lifecycle
還記得我們在 Maven-04-建置專案 裡所執行的 mvn install 嗎?
這個指令讓我們完成了專案的編譯、測試、打包、安裝(到repository)等步驟,而在上一篇 Maven-06-核心概念 plugins and goals 我們知道這些任務都是由各種 plugins 來負責的。那為什麼這樣一個簡短的指令可以一次完成這麼多任務呢?
我們必須要先了解所謂的 maven lifecycle,區分為以下三個種類:
maven lifecycle 裡還定義了多個 build phase,我們之前所執行的 mvn install,就是執行了 default lifecycle 裡 install 這個 build phase,有關 lifecycle build phases 的詳細定義,可以參考 https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference
從以上的 lifecycle 官方文件,我們可以看到 default lifecycle 有20幾個 build phase,而 install 是倒數第二個,這邊有一點要特別注意的是,雖然我們下的指令是 mvn install,但 maven 會從第一個 build phase (validate) 一路按順序執行到 install,這個現象其實也很容易理解,畢竟沒有之前 build phase 的產物的話,maven 也不知道應該 install 什麼東西
回到最一開始的問題:為什麼 mvn install 一個指令可以完成這麼多任務呢?原因在於 plugin goals 可以指定這個 goal 會在哪個 lifecycle build phase 裡執行 (如果沒有特別指定的話,也會有預設值)。所在在執行 mvn install 時,maven 會去找找有哪些 plugin goals 指定了 build phase,然後就依照 lifecycle 的順序執行下去。lifecycle 與 plugin goals 的對應關係,可參考 https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Built-in_Lifecycle_Bindings
我們可以非常靈活地使用 plugin goals 來完成各種任務,但不同的專案可能會有不同的plugin goals 使用方式,只有透過 lifecycle 這麼一套 maven 制定的標準流程,之後在轉換到開發其他 maven 專案時,我們還是可以使用一致的指令來建置專案,無形之中為開發人員減輕了不少負擔!
Maven-06-核心概念 plugins and goals
這一篇要給大家講講 Maven 之所以能夠完成各種任務的關鍵元件:plugins and goals
我們在 Maven-03-建立第一個 Maven專案 裡一開始使用了一道指令來建立專案,
- 其中的 archetype 就是 plugin 名稱, archetype plugin 讓我們可以依照既有的專案樣版來產生專案,或是由既有專案產生出樣版,以方便之後重複使用
- generate 代表 goal 名稱,四個參數 groupId, artifactId, package, version 是要傳給 generate 這個 goal 的參數。
一個 Maven plugin 其實就是多組 goal 所集合而成的,goal 所代表的是一個任務,並且允許我們在執行時輸入參數,下列出幾個常用的 plugin
- Archetype – 使用事先訂好的專案樣版產生專案
https://maven.apache.org/archetype/maven-archetype-plugin/ - Compiler
https://maven.apache.org/plugins/maven-compiler-plugin/plugin-info.html - Surefire – 執行unit test
https://maven.apache.org/surefire/maven-surefire-plugin/plugin-info.html - Jar – 把專案打包成jar檔
https://maven.apache.org/plugins/maven-jar-plugin/
更多的 plugins 可參考 https://maven.apache.org/plugins/maven-jar-plugin/
在了解了 plugin and goals 的定義之後,我們或許可以從另一個角度來解釋 maven:maven 本身是一個具有基本功能的核心平台,它只知道如何解析mvn指令、解析POM、下載所需的plugins並依照設定執行,所以像是對程式碼進行編繹、或打包專案檔,這些事情就會交由plugins 來完成。
我們已經知道了 maven plugins 可以完成開發專案時的各項任務,從以上範例大致也可以看出要產生一個jar檔需經過哪些步驟,但是如果每次都要依照這些步驟一個一個執行,恐怕會影響開發人員的工作效率。因此,下一篇將給大家介紹 maven 另一個核心概念 “lifecycle”,讓我們透過簡短的指令操作,就可以完成一連串相關的任務!
Maven-05-一個簡單的POM範例
我們先來看看Maven自動產生的pom.xml
- pom.xml 包含了整個 maven專案許多資訊,一般狀況下專案裡的pom.xml 遠比上面這個範例複雜的多,還會包含更多的dependency或是自定義的plugin等等。
- 一開始的groupId, artifactId, version, packaging 通稱為maven coordinates (座標),前三個資訊可以構成一個該專案特有的識別名稱
- name, url 則是定義了專案名稱、專案網站資訊。這邊的name是讓開發人員自己看的。通常我們使用各種支援maven的開發工具時(Eclipse, NetBeans, IntelliJ IDEA) ,這些開發工具會把artifactId當作專案名稱
- properties 定義了 pom.xml 裡面的「全域屬性」,如上所示,定義了一個 “project.build.sourceEncoding = UTF-8″的property,如果在pom.xml其它地方需要參照”UTF-8″這個設定,我們可以直接使用 ${project.build.sourceEncoding} 這樣的表示方式
- 一組dependency 包含了 groupId, artifactId, version 以及
- packaging 可忽略不寫,默認為jar
- scope 如果不寫,默認為compile,這邊的junit scope 為test,意思是說這個dependency只有在跑unit test時才會使用到,之後專案要打包釋出時,並不會包含scope為test的dependency,有關dependency scope ,會在之後的文章中詳細說明!
maven專案可以具有上下從屬關係,pom.xml 會繼承每一階層的設定。而單一階層的專案也會繼承自maven最上層的 super-POM (如同Java裡所有物件都繼承自Object),大家可以試著在專案根目錄執行 mvn help:effective-pom,看看「完整版」的pom會長什麼樣子!
咱們下回見囉~