天天看點

深入學習微架構:Spring Boot

及開發過程。該架構使用了特定的方式來進行配置,進而使開發人員不再需要定義樣闆化的配置。通過這種方式,boot緻力于在蓬勃發展的快速應用開發領域(rapid application development)成為上司者。

相關廠商内容

相關贊助商

深入學習微架構:Spring Boot

程式清單1

為了實作更為簡單的建構配置,開發人員可以使用gradle建構系統中簡潔的groovy dsl,如程式清單1.1所示。

程式清單1.1

spring boot在剛剛公開宣布之後就将一個樣例釋出到了twitter上,它目前成為了最流行的一個應用樣例。它的全部描述如程式清單1.2所示,一個非常簡單的groovy檔案可以生成功能強大的以spring為後端的web應用。

程式清單1.2

這個應用可以通過<code>spring run app.groovy</code>指令在spring boot cli中運作。boot會分析檔案并根據各種“編譯器自動配置(compiler auto-configuration)”标示符來确定其意圖是生成web應用。然後,它會在一個嵌入式的tomcat中啟動spring應用上下文,并且使用預設的8080端口。打開浏覽器并導航到給定的url,随後将會加載一個頁面并展現簡單的文本響應:“hello”。提供預設應用上下文以及嵌入式容器的這些過程,能夠讓開發人員更加關注于開發應用以及業務邏輯,進而不用再關心繁瑣的樣闆式配置。

為了闡述在java maven工程中,如何快速地使spring web工程準備就緒,考慮一下程式清單1.3中的應用程式代碼。

程式清單1.3

在<code>application</code>類上的<code>@enableautoconfiguration</code>注解會告知boot要采用一種特定的方式來對應用進行配置。這種方法會将其他樣闆式的配置均假設為架構預設的約定,是以能夠聚焦于如何盡快地使應用準備就緒以便運作起來。<code>application</code>類是可運作的,是以,當我們以java application的方式運作這個類時,就能啟動該應用及其嵌入式的容器,這樣也能實作即時地開發。

為了釋出版本而建構工程時,boot的maven和gradle插件可以嵌入(hook)到這些建構系統的打包過程中,以生成可執行的“胖jar包(fat jar)”,這種jar包含了工程的所有依賴并且能夠以可運作jar的方式執行。使用maven打包boot應用隻需運作<code>mvn package</code>指令,與之類似,使用gradle時,執行<code>gradle build</code>指令将會在建構的目标位址下生成可運作的jar。

boot對spring應用的開發進行了簡化,提供了子產品化方式導入依賴的能力,強調了開發restful web服務的功能并提供了生成可運作jar的能力,這一切都清晰地表明在開發可部署的微服務方面boot架構是一個強大的工具。正如前面的例子所示,借助于boot,讓一個restful web工程運作起來是一件很容易的事情;不過,為了了解boot所有潛在的功能,我們會闡述在開發完整功能的微服務時,會遇到的所有繁瑣的事情。在企業級基礎設施領域,微服務是一種越來越流行的應用架構,因為它能夠實作快速開發、更小的代碼庫、企業級內建以及子產品化部署。有衆多的架構緻力于該領域的開發,該章節将會讨論使用boot如何簡化這一過程。

我們可以基于各種目的來建構微服務,但有一點是肯定的,那就是大多數都需要讀取和寫入資料庫的能力。spring boot使資料庫內建變成了一項非常簡單的任務,因為它具有自動配置spring data以通路資料庫的能力。隻需在你的工程中将<code>spring-boot-starter-data-jpa</code>包含進來,boot的自動配置引擎就能探測到你的工程需要資料通路功能,并且會在spring應用上下文中建立必要的bean,這樣你就可以使用repository和服務了。為了更具體地闡述這一點,請參見程式清單1.4中的gradle建構檔案,它列出了一個基于groovy的微服務web應用的建構結構,該應用使用了spring data對jpa的支援來實作資料通路。

程式清單1.4

在這個配置中,boot的<code>actuator</code>子產品提供了對<code>hsqldb</code>的依賴,這會搭建所有必要的依賴——包括模式的建立——是以spring data可以使用這個記憶體資料庫作為資料源。這種簡便的方式能夠讓開發人員免于在開發期建立和管理複雜的xml配置,進而能夠快速地開發資料庫驅動的微服務。如果在classpath中有h2或derby資料庫的話,這種自動化配置也會生效。boot所提供的另一個便利之處就是能夠快速簡便地使用相關資料啟動應用的資料庫模式。這在開發期是非常有用的,此時資料庫可能是在記憶體中或者是不穩定的,開發人員需要保證的是在應用啟動的時候能夠通路到這些特定的資料。為了闡述這一點,考慮一下程式清單1.5中的示例jpa實體,它代表了微服務所提供的“user”資料結構。

程式清單1.5

為了啟用代表<code>user</code>對象的通用資料,我們隻需建立一個名為<code>schema.sql</code>或<code>data.sql</code>的檔案,并将其包含在classpath之中。這個檔案會在模式建立完成之後執行,是以基于程式清單1.5所給出的實體,我們可以使用sql語句啟用一個使用者賬号,如程式清單1.6所示。

程式清單1.6

在啟動的時候,我們所提供的sql代碼會執行,這樣就能確定有一個測試賬号可以使用。微服務此時已經具有了資料通路的起始點,程式清單1.7展現了如何按照spring data的開發模式建立<code>repository</code>接口,該接口會作為<code>user</code>實體的資料通路對象(data access object)。

程式清單1.7

程式清單1.8

在啟動的時候,應用将會輸出日志,表明hibernate按照<code>user</code>實體的定義建立資料庫結構,在應用初始化的最後,boot還會從<code>schema.sql</code>檔案中導入資料。

在開發微服務應用時,需要特别注意的一點是使用了<code>@requestmapping</code>注解。這不是boot特定的注解。不過,因為boot安裝了自己的端點以監控應用的性能、健康情況以及配置,是以需要確定應用的代碼不要與這些内置的提供詳情的路徑解析相沖突。鑒于此,如果有從請求路徑中解析屬性的需求(在我們的場景中,也就是user的<code>id</code>屬性),那麼我們需要仔細考慮這個動态的屬性解析會對微服務的其他行為産生什麼影響。在本例中,隻是簡單地将controller映射到<code>/user</code>端點而不是根上下文,就能允許boot的端點也可以進行通路。

微服務所提供的資料并不一定全部适合關系型結構,針對這一點spring boot也暴露了一些子產品,進而讓開發人員可以使用spring data的mongodb和redis項目,不過依然采取特定的方式來進行配置。spring data用來定義資料通路對象(data access object)的高層架構,這樣快速切換jpa與非jpa資料源會變得非常容易。參見程式清單1.9,它展現了一個重新定義的<code>userrepository</code>接口,這個接口設計為使用mongodb取代jpa。

程式清單1.9

<code>mongorepository</code>接口也擴充了<code>crudrepository</code>,是以微服務的controller代碼,也就是程式清單1.8所示并不需要修改。為了實作與mongodb的內建,工程唯一要做的就是在應用的classpath中包含<code>spring-boot-starter-data-mongodb</code>。程式清單1.4所示的gradle建構檔案需要稍微調整一下依賴的部分,如程式清單1.10所示。

程式清單1.10

mongodb依賴都置于classpath之中以後,boot将會自動配置spring data連接配接到localhost上的資料庫,并且預設的資料庫名為<code>test</code>。在這個庫中,将會自動建立<code>user</code>集合(按照mongodb的标準),微服務現在就能使用mongodb作為後端了。對非jpa的資料存儲來說,初始化資料比其他的方式更為簡單,這主要是因為它不能針對mongodb的文檔存儲和redis的鍵值存儲運作sql檔案。鑒于spring data會使用這些存儲的持久化執行個體,這就意味着開發期建立的資料需要在重新開機後保留。為了持久化資料,我們需要修改微服務的controller,這樣服務的使用者就能建立<code>user</code>執行個體了。我們也可以将微服務的<code>usercontroller</code>進行修改,使其符合通用的restful api結構,讓controller以不同的方式處理不同的http方法。程式清單1.11展現了為controller添加建立新<code>user</code>執行個體的功能。

程式清單1.11

當微服務的使用者往應用的端點上發送一個http post請求時,spring将會把請求體轉換為<code>user</code>執行個體。代碼接下來會使用<code>userrepository</code>将這個對象存儲到mongodb集合之中。使用curl建立<code>user</code>執行個體的樣例如程式清單1.12所示。

程式清單1.12

按照boot針對mongo資料源的特定配置,新的<code>user</code>執行個體預設會持久化到本地mongo執行個體的<code>test</code>資料庫的<code>user</code>集合之中。如果我們打開web浏覽器并對微服務發起一個http get請求,我們就能看到所建立的user存在于傳回的清單之中。

程式清單1.13

程式清單1.14

在一些場景下你可能需要更為靈活的配置,boot允許你通過java的系統屬性(system properties)重寫很多它的預設配置。例如,如果你的應用需要在部署到産品化環境中使用不同的資料庫使用者,那麼username配置指令可以通過标準的java系統屬性傳入到應用之中,而這需要切換到指令行中執行<code>-dspring.datasource.username=user</code>。關于這一點更為現實的場景是雲部署環境,如cloud foundry或heroku,這些平台需要應用啟動特定的http端口,這一點通過作業系統的環境變量可以實作。boot能夠從系統屬性繼承得到配置,這樣你的應用就可以在指令行中使用<code>-dserver.port=$port</code>來得到http端口。在開發微服務時,這是一種相當有用的特性,因為它可以讓微服務應用運作在各種環境配置之中。

微服務必須要支援的很重要的一點就是外部化配置。這種配置可以包含任何的内容,從占位符資訊到資料庫配置等等,在初始規劃和建構應用原型時,這是必須要考慮的架構内容。在spring io平台中,已經存在各種導入配置的政策,但是應用能夠以多種方式使用配置所造成的後果往往是産生冗長的編碼性耦合。

boot一個很棒的特性在于它能管理外部化的配置并将其轉換為對象結構,這個對象可以在整個應用上下文中使用。建立一個簡單老式的java/groovy對象(plain old java/groovy object),并使用<code>@configurationproperties</code>注解,那麼這個對象就能使用boot配置結構中預先定義的<code>name</code>名下的配置項。更具體一點來講,考慮一下程式清單1.15中的pogo,它能夠得到<code>application.</code>key下的配置指令。

程式清單1.15

當<code>applicationproperties</code>對象在spring上下文中建立完成之後,boot将會識别出它是一個配置對象,并且會按照運作時classpath之中<code>application.properties</code>或<code>application.yml</code>檔案中的配置指令填充它的屬性。是以,如果我們在微服務的<code>application.yml</code>檔案中添加<code>application</code>内容區的話,如程式清單1.16所示,那麼我們就可以在應用的其他部分以程式設計的方式通路這些配置指令。

程式清單1.16

這些配置指令可以有各種用途,要通路這些指令的唯一要求就是代表它們的pojo/pogo必須是spring應用上下文的成員。boot能夠将一個controller作為spring java配置對象,這樣就能很容易地管理配置bean與應用上下文的內建,如程式清單1.17所示。

程式清單1.17

程式清單1.17中的樣例代碼可能有些牽強,不過,即便是在更為複雜的場景下,如何使用boot來通路應用特定配置的原則是相同的。配置類也支援嵌套式的對象圖,這樣來自于配置中的深層資料就能更便利地進行通路,也有了更好的語義。例如,如果我們想要得到的配置指令是<code>application.</code>根下的那些metrics key,那麼可以在<code>applicationproperties</code> pogo中添加一個嵌套對象來表示這些值,如程式清單1.18所示。

程式清單1.18

現在,我們的<code>application.yml</code>檔案可以如程式清單1.19所示,它在<code>application.</code>代碼塊中包含了<code>metrics</code>配置。

程式清單1.19

當我們需要通路<code>application.metrics.dbexecutiontimekey</code>的值時,能夠以程式設計的方式通過<code>applicationproperties</code>對象來進行通路。

為了在整個應用之中使用<code>application.properties</code>或<code>application.yml</code>檔案中的這些配置指令,我們并不是必須要将其轉換為對象圖。boot也為spring應用上下文提供了<code>propertysourcesplaceholderconfiguration</code>,這樣的話,來自于<code>application.properties</code>或<code>application.yml</code>檔案的指令或者來自于java系統的重寫屬性都可以作為spring屬性占位符來使用。spring的這種機制能夠讓你以一種特定的文法來為屬性定義占位符值,如果spring發現了占位符配置的話,就會用這個配置來進行填充。作為示例,我們可以在controller中使用<code>@value</code>注解來直接通路<code>application.metrics.dbexecutiontimekey</code>,如程式清單1.20所示。

程式清單1.20

關于應用名額的報告,後面會有更為詳細的介紹,但現在重要的一點在于,了解<code>@value</code>注解如何與spring屬性占位符一起使用,使boot能夠自動注入值,進而滿足這個微服務的特定配置需求。

在微服務的開發中,對于完備安全場景的需求會持續增長。為了滿足這種需求,boot引入了強大完整的spring security,并且提供了自動配置的功能,以快速簡便地啟用安全層。隻需在應用的classpath中包含<code>spring-boot-starter-security</code>子產品就能使boot引入一些安全特性,如跨站腳本防護(cross-site scripting protection)并且會添加頭資訊以防止點選劫持(click-jacking)。除此之外,添加一條簡單的配置指令就能啟用基本認證來保護你的應用,如程式清單1.21所示。

程式清單1.21

boot會為你提供一個預設的使用者賬号<code>user</code>和預設角色<code>user</code>,并且會在應用啟動的時候在控制台上輸出随機生成的密碼。就像boot的其他功能那樣,對于内置的<code>user</code>賬号,我們可以很容易地指定不同的使用者名和密碼(分别為“secured”和“foo”),這需要通過明确定義的配置指令來實作,如程式清單1.22所示。

程式清單1.22

對于簡單的内部應用或開發原型來說,boot内置的基礎設施能夠快速地在微服務中啟用基本認證,這是非常有用的。随着需求的演化,你的應用毫無疑問會需要更細粒度的安全特性,如保護端點隻能由特定的角色通路。從這個角度來看,我們可能希望具有<code>user</code>角色的調用者隻能讀取資料(即get請求),而對具有<code>admin</code>角色的調用者可以讀取和寫入資料(即post請求)。為了做到這一點,我們需要在工程的<code>application.yml</code>檔案中禁用boot的基本認證自動配置功能,并且定義我們自己的<code>user</code>和<code>admin</code>賬号以及對應的角色。當你的需求超過boot所提供的預設功能時,它通常很快就能實作,這可以作為佐證這一點的又一個例子。為了更具體地闡述這一點,考慮一下程式清單1.23中的代碼。這個樣例可以闡述如何發揮spring security所有潛在的功能以及更為複雜的認證政策,如基于jdbc後端、openid或單點登入(single-sign on)。

程式清單1.23

在程式清單1.23的樣例之中,應用現在被明确地配置為要基于<code>user</code>和<code>admin</code>使用者賬号進行通路,它們的密碼都是<code>password</code>,具有的角色分别是<code>user</code>和<code>admin</code>。微服務的get和post端點分别通過<code>user</code>和<code>admin</code>角色進行保護,這就意味着普通使用者可以通路隻讀的資料,而執行讀取-寫入操作的話,需要<code>admin</code>使用者憑證。

對于微服務來說,基本認證是很好的一個選擇,因為它遵循了很實用且廣泛使用的認證協定。換句話說,很多的api調用者,包括移動應用,能夠很容易地使用這一點來通路你的微服務。當你的認證需求超過了基本認證的功能時(如openid或oauth),微服務可以使用spring security的全部功能來滿足你的需求。

在任何的應用中,消息(messaging)都是一種很強大的工具,在一點上,微服務當然也不能例外。使用消息驅動架構開發的應用能夠更好地支援可重用性和擴充性。spring boot能夠讓開發人員在編寫微服務時将消息作為架構的核心組成部分,它使用到了spring io平台的企業內建模式(enterprise integration patterns)實作,即spring integration。spring integration提供了開發消息驅動架構的基本結構,以及與分布式企業平台內建的子產品。這種能力使得微服務可以使用來自抽象消息源的業務對象,這些消息源可以在應用内部,也可能是組織機構内部的其他服務所提供的。

盡管boot并沒有提供明确的spring上下文自動化配置,但是它為spring integration提供了一個starter子產品,它會負責引入spring integration項目的一系列依賴。這些依賴包括spring integration的核心庫(core library)、http子產品(用來進行面向http的企業內建)、ip子產品(用來進行基于socket的內建操作)、file子產品(用于進行檔案系統內建)以及stream子產品(用于支援使用stream的操作,如stdin和stdout)。這個starter子產品為開發人員提供了健壯的消息功能的工具集,可以使已有的基礎設施适應微服務api。

除了starter子產品,boot也為通過cli建構的應用提供了編譯器自動配置的功能。對于需要快速建構微服務原型并驗證可行性的開發者來說,這種方式提供了一些捷徑。使用企業級平台的應用可以快速地進行開發,在轉移到正式的工程和建構系統之前,就能确認其價值。使用spring boot和spring integration使一個消息驅動的微服務運作起來非常簡單,如程式清單1.24的樣例代碼所示。

程式清單1.24

使用消息驅動的方式來進行微服務的開發能提供很大的代碼可重用性,并且能夠與底層的服務提供者實作相解耦。在更為正式的場景之中,程式清單1.24的代碼可能會負責組合資料,這些資料可能來自于資料庫調用和企業組織中某個外部的服務內建。spring integration具有内置的設施用來進行負載路由(payload routing)和處理器鍊(handler chaining),這對于組合不同的資料來說,是一個很有吸引力的方案,我們的微服務可以作為一個資料的提供者(provider)。

微服務最重要的一個特性可能就是為報表終端(reporting agent)提供度量名額。不像那些功能完備的web應用,微服務是輕量級的,設計時可能就不會規劃提供報表界面或完備的接口來分析服務的活動。這種類型的操作最好是留給專門進行資料聚合和分析的應用,這些資料能夠用來進行穩定性、性能以及商務智能的監控。基于這樣的前提,微服務應該為這些工具提供端點,進而更加容易地擷取有關該服務活動的資料。而報表工具負責将資料聚合到一個視圖或報告中,對于關心資料的人這才是有意義的。

微服務的一些名額如穩定性和性能,對所有的應用都是通用的,但是與業務操作相關的名額必須由應用本身來具體進行管理。針對這一點,spring boot的<code>actuator</code>子產品為開發人員提供了一種機制,允許開發人員通過<code>/metrics</code>端點以編碼的方式暴露微服務狀态的細節。boot将名額拆分為“counter”和“gauge”兩種類别:counter是所有以number類型來展現的名額,而gauge是衡量雙精度計算的名額。為了讓微服務的開發人員更加容易地使用名額,boot暴露了<code>counterservice</code>和<code>gaugeservice</code>,它們可以自動織入到應用上下文之中。請參見程式清單1.25的樣例,它闡述了如何通過<code>counterservice</code>對外暴露點選數。

程式清單1.25

在點選<code>/user</code>端點時,有可能提供id也有可能不提供id,<code>/metrics</code>端點都會在<code>counter.</code>父節點下記錄新的key。例如,如果我們隻是查詢<code>/user</code>端點而不帶有id的話,那麼就會注冊<code>counter.queries.without.id</code>名額。類似的,如果我們帶有id的話,那麼就會看到有一個<code>counter.queries.by.id.&lt;id&gt;</code>的key,它能用來标記對于給定的id已經進行了多少次查詢。這些名額可能會有助于掌握最經常通路的<code>user</code>對象并指導要采取的行為,如緩存或資料庫索引。類似于遞增名額的數值,<code>counterservice</code>也允許将名額的值将為零。這對于跟蹤打開的連接配接數或其他頻率分布(histographic)的測量都是很有用處的。

gauge是稍微有所不同的一種類型名額,它會進行探索性的計算或基于請求來确定值。如<code>gaugeservice</code>的javadocs所述,“gauge”可以測量任意的值,從方法執行的次數到會議室的溫度。當需要為報表工具暴露細節時,這種類型的測量尤其适合于使用<code>gaugeservice</code>。gauge的名額會在<code>/metrics</code>端點之下進行通路,并且帶有<code>gauge.</code>字首。它們的注冊方式與counter有些差别,如程式清單1.26所示。

程式清單1.26

預設情況下,名額會存儲在一個易失的記憶體資料庫之中,但boot同時也為應用上下文提供了<code>metricsrepository</code>實作,它能支援更為持久化的行為。boot自帶了一個<code>redismetricsrepository</code>,它能夠自動織入進來,進而将名額存儲到redis值存儲之中,另外,可以編寫自定義的實作将名額存儲到任意的資料存儲形式之中。

一旦微服務的名額在boot中進行了注冊,那麼報表工具就可以通過<code>/metrics</code>端點來檢索它們。已命名的名額可以通過<code>/metrics</code>端點擷取,隻需将名額的key名作為查詢字元串的一部分即可。例如,如果隻是通路gauge名額下的“user.get.db.time”,報表工具可以針對<code>/metrics/gauge.user.get.db.time</code>進行查詢。

正如前面所讨論的,boot提供了maven和gradle插件,它為建構系統的打包階段提供了一種鈎子(hook),以産生所謂的“胖jar”,在這種jar中包含了工程的所有依賴。當這個胖jar包執行時,應用将會運作在與工程開發期相同的嵌入式容器之中。這種簡便的方式能夠讓開發人員省去很多麻煩,因為他們的部署包在開發期和運作時環境之中具有相同的依賴結構。這也能夠緩解運維團隊的焦慮,他們不用擔心部署的場景,因為在部署時一個錯誤配置的運作時容器可能會帶有某個特定的依賴,而在項目的開發期所依賴的可能是另外一個。

為了在maven下執行打包,隻需執行<code>mvn package</code>指令。spring boot的插件會備份工程所建立的原始jar并且在檔案名上添加“.original”。在這裡,能夠得到可運作的jar,檔案符合maven artifact的命名約定,它可以按照工程最合适的方式進行部署。使用gradle建構boot工程同樣很簡單,隻需執行标準的<code>gradle build</code>指令即可。類似于maven,boot插件在原有的打包任務之後使用gradle安裝了一個生命周期事件,并且會在<code>build/libs</code>目錄下建立胖jar包。對所生成的胖jar包進行檢查的一種方式就是所有依賴的jar都會位于歸檔檔案的<code>lib/</code>目錄下。

打包完成之後,胖jar包就能夠像其他可運作的jar檔案那樣在指令行中執行了,也就是使用<code>$java_home/bin/java -jar path/to/myproject.jar</code>指令。啟動後,boot應用的日志将會顯示在控制台上。

對于需要部署到傳統servlet容器之中的應用,boot提供了一種方式以編碼的方式初始化web配置。為了使用這一點,boot提供了可選的<code>webapplicationinitializer</code>,它會使用servlet容器來注冊應用,這會通過servlet 3.0 api以編碼的方式注冊servlet并且會用到<code>servletcontext</code>。通過提供<code>springbootservletinitializer</code>的子類,boot應用能夠使用嵌入的spring上下文來注冊配置,這個spring上下文是在容器初始化的時候建立的。為了闡述這個功能,考慮程式清單1.27中的示例代碼。

程式清單1.27

<code>application</code>類中被重寫的<code>configure</code>方法就是使用嵌入式的spring上下文注冊應用的地方。在更為正式的場景之中,這個方法可能會用來注冊spring java配置類,它會定義應用中所有controller和服務的bean。

當将應用打包部署到servlet容器之中時,工程要建構為一個war檔案。在maven工程中,為了适應這一點,需要移除boot插件,并且packaging需要明确定義為“war”類型,如程式清單1.28所示。

程式清單1.28

對這個工程執行<code>mvn install</code>指令會在<code>target</code>目錄下生成<code>myproject-1.0.0-snapshot.war</code>檔案。使用gradle建構的工程可以使用gradle war plugin,它為建構war檔案暴露了一個<code>war</code>任務。類似于maven的配置,boot gradle工程也需要移除所包含的boot插件。産生war檔案的示例gradle建構腳本如程式清單1.29所示。

程式清單1.29

對于boot工程,使用這個建構腳本運作gradle的<code>war</code>任務将會在<code>build/libs</code>目錄下産生war檔案。

不管是maven還是gradle的配置,一旦war檔案産生,它就可以部署到任意相容servlet 3.0的應用伺服器之中。部分相容的容器包括tomcat 7+、jetty 8、glassfish 3.x、jboss as 6.x/7.x以及websphere 8.0。