Java微服務開發指南 -- 使用Spring Boot構建微服務
使用Spring Boot構建微服務
Spring Boot是一個廣泛用來構建Java微服務的框架,它基於Spring依賴注入框架來進行工作。Spring Boot允許開發人員使用更少的配置來構建微服務,同時框架本身能夠盡可能的減少開發人員的衝突,它和我們後麵要介紹的兩個框架類似,它通過以下幾個方麵幫助開發人員:
- 自動化配置,一般情況下都有默認配置
- 提供一組流行的starter依賴,方便開發人員使用
- 簡化應用打包
- 提升應用運行時的內省性(例如:Metrics與環境信息)
簡化的配置
Spring以噩夢般的配置而聞名,雖然框架本身相比其他組件模型(EJB 1.x 和 2.x)簡單了不少,但是它也帶來了自己的配置模式。也就是說,如果想要正確的使用Spring,你需要深入了解如何進行XML配置、了解JdbcTemplate
、JmsTemplate
以及BeanFactory
生命周期、了解Servlet
監聽器,你以為掌握了這些就可以開始開發了嗎?實際上問題遠沒有結束,如果你要用Spring MVC編寫一個簡單的hello world
,你還需要了解DispatcherServlet
和一堆Model-View-Controller
相關的類型。
Spring Boot目標就是消除掉這些與業務無關的配置和概念,通過簡單的注解,你就能夠完成這些工作,當然如果你想繼續想以前一樣使用Spring,它也不會攔著你。
Starter依賴
Spring廣泛使用著,包括了大型企業應用,在應用中,用戶將會使用到不同的技術組件,包括:JDBC數據源、消息隊列、文件係統以及應用緩存等。開發人員需要在需要這些功能時,停下來,仔細分析一下自己究竟需要什麼?需要的內容屬於哪個依賴(“哦,我需要JPA依賴”),然後花費大量的時間在依賴組織和排除上。
Spring Boot提供了功能域(一批jar包依賴)的依賴,它讓開發人員聲明需要的功能,而不用去關係究竟如何處理依賴關係。這些starter可以允許開發人員直接使用這些功能:
- JPA持久化
- NoSQL數據庫支持,例如:MongoDB、Cassandra或者CouchBase
- Redis緩存
- Tomcat、Jetty或者Undertow的Servlet引擎
- JTA事務
通過直接添加一個starter,能夠讓開發人員獲得這個特性相關的一組依賴,而這些依賴的組合已經被驗證,省卻了開發人員的不少時間。
應用打包
Spring Boot是一組jar包和符合其約定的配置的構建塊,因此它不會運行在現有的應用服務器中,而使用Spring Boot的大多數開發人員更喜歡的是直接運行的這種自包含的jar
包。這意味著Spring Boot將所有的依賴和應用程序代碼都包裝到一個自包含的jar
中,而這些jar包運行在一個平麵的類加載器中。簡單的類加載體係使得開發人員更容易理解應用程序的啟動、依賴關係和日誌輸出,但更重要的是,它有助於減少應用從構建到生產環境的步驟數量。這意味著開發人員不必將打包好的應用放置到應用服務器中,而是直接運行這個standalone的應用,如果你需要servlet
,那麼完全可以將其打包在應用內,使其為你服務。
沒錯,一個簡單的java -jar <name.jar>
就可以啟動你的應用了!Spring Boot、Dropwizard和WildFly Swarm都遵循將所有內容打包成可執行的jar
模式。但是傳統的應用服務器包含的管理能力,怎麼在這種模式下實現呢?
為生產環境而準備
Spring Boot推出了一個叫做actuator
的模塊,它可以實現應用的指標統計。例如:我們可以收集日誌、查看指標、生成執行線程dump、顯示環境變量、了解gc以及顯示BeanFactory中配置的bean。可以通過HTTP
或者JMX
暴露這些信息或者進行日誌輸出。借助Spring Boot,我們可以利用Spring框架的功能、減少配置並快速開發應用並上線。
說了這麼多,讓我們看看怎麼使用它。
開始使用
我們接下來使用Spring Boot的命令行工具(CLI)來創建第一個Spring Boot程序(CLI底層使用了Spring Initializer)。你也可以使用自己喜歡的方式,比如使用集成了Spring Initializer的IDE,或者直接訪問web來創建一個工程。
Spring Boot CLI 的安裝方式,可以參考 這裏
Homebrew下:
brew tap pivotal/tap
brew install springboot
一旦你安裝了Spring Boot CLI,你可以這樣檢查一下。
$ spring --version
Spring CLI v1.5.4.RELEASE
如果你能看到版本的輸出,恭喜你,安裝成功了。接下來,在你希望創建工程的目錄下運行命令:spring init --build maven --groupId com.murdock.examples --version 1.0 --java-version 1.8 --dependencies web --name hola-springboot hola-springboot
在microservices-camp下運行。
運行該命令後,將會在當前目錄下創建一個hola-springboot
目錄,同時該目錄下包含了一個完整的Spring Boot程序,簡單的介紹一下這個命令中包含的內容。
- --build
使用的構建工具,示例中是:maven - --groupId
maven坐標中的組Id,也就是代碼的包名,如果你想改包名,隻有在IDE中修改 - --version
maven坐標中的version - --java-version
Java版本 - --dependencies
這是一個有趣的參數,我們可以指定某種開發類型的依賴。比如:web就是指當前項目使用Spring MVC框架,默認基於內嵌的Tomcat(Jetty和Undertow作為可選)。其他的依賴或者starter,比如:jpa
、security
和cassandra
進入到hola-springboot
目錄中, 執行命令:mvn spring-boot:run
,如果程序啟動,沒有報錯,你就能看到如下的日誌:
2017-06-18 10:46:51.070 INFO 3397 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-06-18 10:46:51.081 INFO 3397 --- [ main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0
2017-06-18 10:46:51.253 INFO 3397 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-06-18 10:46:51.262 INFO 3397 --- [ main] c.m.e.h.HolaSpringbootApplication : Started HolaSpringbootApplication in 13.988 seconds (JVM running for 17.985)
恭喜你!你快速的創建了一個Spring Boot應用,並且啟動了它,你甚至可以訪問https://localhost:8080
,你會看到如下內容
可以看到返回了默認的出錯頁麵,到目前為止,它除了這個什麼也做不了。接下來,我們就添加一些特性,比如:REST訪問,做一個helloworld式的應用。
後續實踐內容與原文有不同,在操作性上要比原文具備更好的實踐性。
你好,世界
現在我們擁有了一個可以運行的Spring Boot應用,讓我們為它添加一些簡單的功能。首先,我們想做的是,讓應用暴露一個位置是api/holaV1
HTTP/REST端點,訪問它將返回 Hola Spring Boot @ X,而其中的 X 是運行應用的本機IP。
在編寫代碼前,先將hola-springboot
導入到IDE中,在com.murdock.examples.holaspringboot
包下麵建立一個類,名稱為HolaRestControllerV1
。
public class HolaRestControllerV1 {
public String hola() throws UnknownHostException {
String hostname = null;
try {
hostname = InetAddress.getLocalHost()
.getHostAddress();
} catch (UnknownHostException e) {
hostname = "unknown";
}
return "Hola Spring Boot @ " + hostname;
}
}
可以看到方法hola()
返回了我們需要的內容,一個簡單的字符串。
添加HTTP端點
到現在為止,我們隻是創建了一個名為HolaRestControllerV1
的POJO
,你可以寫一些單元測試去做驗證,而讓它暴露HTTP端點,則需要增加一些內容。
@RestController
@RequestMapping("/api")
public class HolaRestControllerV1 {
@RequestMapping(method = RequestMethod.GET, value = "/holaV1", produces = "text/plain")
public String hola() throws UnknownHostException {
String hostname = null;
try {
hostname = InetAddress.getLocalHost()
.getHostAddress();
} catch (UnknownHostException e) {
hostname = "unknown";
}
return "Hola Spring Boot @ " + hostname;
}
}
- @RestController
這個注解告知Spring,該類是一個用於暴露HTTP端點的控製器(可以暴露GET、PUT和POST等基於HTTP協議的功能) - @RequestMapping
用於映射HTTP URI到對應的類或者方法
通過添加這兩個注解,我們就可以使得原有的類具備了暴露HTTP端點的能力。針對上麵的代碼,比如@RequestMapping("/api")
代表著HolaRestControllerV1
接受來自/api
路徑的請求,當添加@RequestMapping(method = RequestMethod.GET, value = "/holaV1", produces = "text/plain")
時,表示告知Spring在/holaV1(其實是/api/holaV1)暴露HTTP GET端點,該端點接受的類型是text/plain
。Spring Boot將會使用內置的Tomcat
運行,當然你也可以切換到Jetty
或者Undertow
。
我們在hola-springboot
目錄下,執行mvn clean package spring-boot:run
,然後使用瀏覽器訪問https://localhost:8080/api/holaV1
,如果一切正常,我們可以看到如下內容。
現在這些返回內容是寫死的,如果我們想個應用增加一些環境相關的配置,如何做呢?比如:可以替代 Hola 這個詞,比如使用 Guten Tag 德語,我們把這個應用部署給德國人用,我們需要一個將外部屬性注入給應用的途徑。
外部配置
Spring Boot可以很容易使用諸如:properties文件、命令行參數和係統環境變量等作為外部的配置來源。我們甚至可以將這些配置屬性通過Spring Context綁定到類型實例上,例如:如果想將helloapp.*
屬性綁定到HolaRestController
,可以在類型上聲明@ConfigurationProperties(prefix="helloapp")
,Spring Boot會自動嚐試將比如helloapp.foo
或者helloapp.bar
等這些屬性值綁定到類型實例的foo
、bar
等字段上。
在Spring Initializer CLI創建的工程中,已經有了一個application.properties
,我們就可以在這個文件中定義新屬性,比如:helloapp.saying
。
$ more src/main/resources/application.properties
helloapp.saying=Guten Tag aus
創建一個新的控製器HolaRestControllerV2
。
@RestController
@RequestMapping("/api")
@ConfigurationProperties(prefix = "helloapp")
public class HolaRestControllerV2 {
private String saying;
@RequestMapping(method = RequestMethod.GET, value = "/holaV2", produces = "text/plain")
public String hola() throws UnknownHostException {
String hostname = null;
try {
hostname = InetAddress.getLocalHost()
.getHostAddress();
} catch (UnknownHostException e) {
hostname = "unknown";
}
return saying + " @ " + hostname;
}
public String getSaying() {
return saying;
}
public void setSaying(String saying) {
this.saying = saying;
}
}
停止之前運行的應用,然後在hola-springboot
目錄下,繼續使用mvn clean package spring-boot:run
來編譯工程,運行這個應用,然後使用瀏覽器訪問https://localhost:8080/api/holaV2
,你會看到如下內容。
我們現在通過更改外部配置文件來使應用適應部署的環境,比如:調用服務的url、數據庫url和密碼以及消息隊列配置,這些都適合作為配置。但是也要把握度,不是所有的內容都適合放置在配置中,比如:應用在任何環境下都應該具備相同的超時、線程池、重試等配置。
暴露應用的Metrics和信息
一個Spring Boot應用搭建起來了,緊接著就是將其部署到生產環境,我們怎樣監控它呢?當我們想知道它運行的怎麼樣,我們該怎麼辦呢?除非我們讓應用向外暴露出Metrics,否則應用就會像黑盒子一樣。Spring Boot專門提供了一個starter -- actuator
來完成這個工作。
讓我們看看如何啟用actuator
,啟用的過程非常簡單。在hola-springboot/pom.xml
中依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然後在hola-springboot/src/main/resources/application.properties
中增加一個配置(安全原因):
$ more src/main/resources/application.properties
management.security.enabled=false
隨後,結束當前應用,在hola-springboot
下運行:mvn clean package spring-boot:run
重新編譯工程,啟動項目。
我們可以通過瀏覽器訪問幾次https://localhost:8080/api/holaV1
以及https://localhost:8080/api/holaV2
,然後訪問一下:https://localhost:8080/metrics
,可以看到如下內容。
類似這樣的URL還有許多:
- https://localhost:8080/beans
- https://localhost:8080/env
- https://localhost:8080/health
- https://localhost:8080/metrics
- https://localhost:8080/trace
- https://localhost:8080/mappings
暴露出這些運行時信息,使得開發人員在忙於業務開發的同時,更輕鬆獲取到係統信息。
怎樣在maven之外運行
到現在為止,我們還是以開發者視角使用maven來構建這個簡單的工程。如果我們需要將它部署到其他環境,比如:開發、測試或者生產環境,需要怎麼做呢?
幸運的是,使用Spring Boot,我們可以輕鬆的發布和構建,Spring Boot推薦單一、可執行的jar,而在這個jar中包括了所有的依賴,這些依賴的jar都會放置在應用的類路徑下。在hola-springboot
下,運行mvn clean package
,然後可以通過java -jar
來運行。
$ mvn clean package
$ java -jar target/hola-springboot-1.0.jar
就是這樣,我們可以啟動這個應用,後續接下來介紹的Dropwizard
和WildFly Swarm
都使用類似的方式進行。
調用其他服務
在微服務環境下,每個服務都有提供功能的義務並服務好它的調用者。就像我們在第一章中談的,因為網絡的不確定性,構建分布式係統十分的困難,本章主要討論一個服務怎樣調用到後台的服務。
在第五章中,將會討論服務的柔性、適應性交互和調用
接下來將擴展hola-springboot
項目,完成服務的調用,但在此之前,我們先要搭建一個後台服務,完成類似下圖的交互。
後台服務的構建,將采用forge + WildFly的方式進行,比原文中寫一個Servlet部署到Jetty顯得更好
關於forge的安裝,在mac下:brew install jboss-forge
通過以下方式,可以在microservices-camp
下創建一個具備持久化能力的REST服務,它可以自由的部署到WildFly
中。
使用forge
構建完成之後,可以將其導入到IDE中,如果觀察BookEndpoint
這個類型,你會發現涉及到CRUD
以及分頁查詢等邏輯已經完全具備了。
通過上述命令,我們可以構建出一個hola-backend.war
的應用,下麵我們將其部署到WildFly
中。WildFly
的使用可以通過下載到本地運行,但是由於涉及到兩個進程的交互,本文采用Docker
的方式進行部署,讀者可以自行準備環境。
筆者準備了
WildFly
鏡像,可以簡單的運行起來
執行:sudo docker run --name wildfly -it -p 9990:9990 -p 8080:8080 weipeng2k/wildfly-admin
,可以啟動一個WildFly
,HTTP端口在8080,應用管理端口在9990
管理員賬號筆者已經構建在鏡像中:admin/Admin#hello1234
登錄到WildFly
後台,通過管理界麵,部署hola-backend.war
。
可以看到後台的更新日誌,從中可以了解到應用部署正常。
使用這種方式的好處在於開發階段如果有新的包生成直接進行上傳就好,如果想整體銷毀,直接停止刪除容器即可,不會弄壞WildFly
。下麵使用chrome插件Postman
構建Book
數據,然後測試是否可用。
新增數據測試。
查詢數據測試。
看來後台服務應用hola-backend
工作正常,當然可以通過WildFly
的管理界麵查詢運行時信息,這點和Spring Boot的actuator很像,但是產品化的體驗做的更好些。
接下來在hola-springboot
項目中新建BookRestController
,使用RestTemplate
來完成後端服務的交互。
@RestController
@RequestMapping("/api")
@ConfigurationProperties(prefix = "books")
public class BookRestController {
private RestTemplate template = new RestTemplate();
private String backendHost;
private int backendPort;
@RequestMapping(value = "/books/{bookId}",
method = RequestMethod.GET, produces = "text/plain")
public String greeting(@PathVariable("bookId") Long bookId) {
String backendServiceUrl = String.format("https://%s:%d/hola-backend/rest/books/{bookId}", backendHost, backendPort);
Map object = template.getForObject(backendServiceUrl, Map.class, bookId);
return object.toString();
}
public String getBackendHost() {
return backendHost;
}
public void setBackendHost(String backendHost) {
this.backendHost = backendHost;
}
public int getBackendPort() {
return backendPort;
}
public void setBackendPort(int backendPort) {
this.backendPort = backendPort;
}
}
可以看到BookRestController
將後端的host與port放在了配置中,而前綴是books
,那麼也就需要在application.properties
中增加這些配置。
$ more src/main/resources/application.properties
books.backendHost=192.168.0.125
books.backendPort=8080
接下來,打開瀏覽器訪問:https://localhost:8080/api/books/1
,它將訪問hola-springboot
,而hola-springboot
將會調用hola-backend
,最終由hola-springboot
輸出結果。
小結
通過本章的內容,我們學習了Spring Boot的基本知識,了解它與傳統的WAR
和EAR
不同的部署方式,以及如何使用外部資源來完成配置,並通過actuator暴露了Metrics,使用RestTemplate
調用了另一個服務。如果你想了解跟多內容,可以參考下麵的鏈接。
最後更新:2017-11-03 15:04:48