javascript
使用 Spring HATEOAS 开发 REST 服务--转
原文地址:https://www.ibm.com/developerworks/cn/java/j-lo-SpringHATEOAS/index.html?ca=drs-&utm_source=tuicool&utm_medium=referral
絕大多數開發人員對于 REST 這個詞都并不陌生。自從 2000 年 Roy Fielding 在其博士論文中創造出來這個詞之后,REST 架構風格就很快地流行起來,已經成為了構建 Web 服務時應該遵循的事實標準。很多 Web 服務和 API 都宣稱滿足了 REST 架構風格的要求,即所謂的“RESTful”服務。不過就如同其他很多流行的概念一樣,不少人對于 REST 的含義還是存在或多或少的種種誤解。REST 在某些時候被當成了一種營銷的手段。不少所謂的“RESTful” Web 服務或 API 實際上并不滿足 REST 架構風格的要求。這其中的部分原因在于 REST 的含義比較復雜,包含很多不同方面的內容。本文首先對 REST 架構做一個簡單的說明以澄清某些誤解。
REST 架構
REST 是 Representational state transfer 的縮寫,翻譯過來的意思是表達性狀態轉換。REST 是一種架構風格,它包含了一個分布式超文本系統中對于組件、連接器和數據的約束。REST 是作為互聯網自身架構的抽象而出現的,其關鍵在于所定義的架構上的各種約束。只有滿足這些約束,才能稱之為符合 REST 架構風格。REST 的約束包括:
- 客戶端-服務器結構。通過一個統一的接口來分開客戶端和服務器,使得兩者可以獨立開發和演化??蛻舳说膶崿F可以簡化,而服務器可以更容易的滿足可伸縮性的要求。
- 無狀態。在不同的客戶端請求之間,服務器并不保存客戶端相關的上下文狀態信息。任何客戶端發出的每個請求都包含了服務器處理該請求所需的全部信息。
- 可緩存。客戶端可以緩存服務器返回的響應結果。服務器可以定義響應結果的緩存設置。
- 分層的系統。在分層的系統中,可能有中間服務器來處理安全策略和緩存等相關問題,以提高系統的可伸縮性??蛻舳瞬⒉恍枰私庵虚g的這些層次的細節。
- 按需代碼(可選)。服務器可以通過傳輸可執行代碼的方式來擴展或自定義客戶端的行為。這是一個可選的約束。
- 統一接口。該約束是 REST 服務的基礎,是客戶端和服務器之間的橋梁。該約束又包含下面 4 個子約束。
- 資源標識符。每個資源都有各自的標識符。客戶端在請求時需要指定該標識符。在 REST 服務中,該標識符通常是 URI??蛻舳怂@取的是資源的表達(representation),通常使用 XML 或 JSON 格式。
- 通過資源的表達來操縱資源??蛻舳烁鶕玫降馁Y源的表達中包含的信息來了解如何操縱資源,比如對資源進行修改或刪除。
- 自描述的消息。每條消息都包含足夠的信息來描述如何處理該消息。
- 超媒體作為應用狀態的引擎(HATEOAS)??蛻舳送ㄟ^服務器提供的超媒體內容中動態提供的動作來進行狀態轉換。這也是本文所要介紹的內容。
在了解 REST 的這些約束之后,就可以對“表達性狀態轉換”的含義有更加清晰的了解。“表達性”的含義是指對于資源的操縱都是通過服務器提供的資源的表達來進行的??蛻舳嗽诟鶕Y源的標識符獲取到資源的表達之后,從資源的表達中可以發現其可以使用的動作。使用這些動作會發出新的請求,從而觸發狀態轉換。
HATEOAS 約束
HATEOAS(Hypermedia as the engine of application state)是 REST 架構風格中最復雜的約束,也是構建成熟 REST 服務的核心。它的重要性在于打破了客戶端和服務器之間嚴格的契約,使得客戶端可以更加智能和自適應,而 REST 服務本身的演化和更新也變得更加容易。
在介紹 HATEOAS 之前,先介紹一下 Richardson 提出的 REST 成熟度模型。該模型把 REST 服務按照成熟度劃分成 4 個層次:
- 第一個層次(Level 0)的 Web 服務只是使用 HTTP 作為傳輸方式,實際上只是遠程方法調用(RPC)的一種具體形式。SOAP 和 XML-RPC 都屬于此類。
- 第二個層次(Level 1)的 Web 服務引入了資源的概念。每個資源有對應的標識符和表達。
- 第三個層次(Level 2)的 Web 服務使用不同的 HTTP 方法來進行不同的操作,并且使用 HTTP 狀態碼來表示不同的結果。如 HTTP GET 方法來獲取資源,HTTP DELETE 方法來刪除資源。
- 第四個層次(Level 3)的 Web 服務使用 HATEOAS。在資源的表達中包含了鏈接信息??蛻舳丝梢愿鶕溄觼戆l現可以執行的動作。
從上述 REST 成熟度模型中可以看到,使用 HATEOAS 的 REST 服務是成熟度最高的,也是推薦的做法。對于不使用 HATEOAS 的 REST 服務,客戶端和服務器的實現之間是緊密耦合的??蛻舳诵枰鶕掌魈峁┑南嚓P文檔來了解所暴露的資源和對應的操作。當服務器發生了變化時,如修改了資源的 URI,客戶端也需要進行相應的修改。而使用 HATEOAS 的 REST 服務中,客戶端可以通過服務器提供的資源的表達來智能地發現可以執行的操作。當服務器發生了變化時,客戶端并不需要做出修改,因為資源的 URI 和其他信息都是動態發現的。
回頁首
示例
本文將通過一個完整的示例來說明 HATEOAS。該示例是一個常見的待辦事項的服務,用戶可以創建新的待辦事項、進行編輯或標記為已完成。該示例中包含的資源如下:
- 用戶:應用中的用戶。
- 列表:待辦事項的列表,屬于某個用戶。
- 事項:具體的待辦事項,屬于某個列表。
應用提供相關的 REST 服務來完成對于列表和事項兩個資源的 CRUD 操作。
回頁首
Spring HATEOAS
如果 Web 應用基于 Spring 框架開發,那么可以直接使用 Spring 框架的子項目 HATEOAS 來開發滿足 HATEOAS 約束的 Web 服務。本文的示例應用基于 Java 8 和使用 Spring Boot 1.1.9 來創建,Spring HATEOAS 的版本是 0.16.0.RELEASE。
基本配置
滿足 HATEOAS 約束的 REST 服務最大的特點在于服務器提供給客戶端的表達中包含了動態的鏈接信息,客戶端通過這些鏈接來發現可以觸發狀態轉換的動作。Spring HATEOAS 的主要功能在于提供了簡單的機制來創建這些鏈接,并與 Spring MVC 框架有很好的集成。對于已有的 Spring MVC 應用,只需要一些簡單的改動就可以滿足 HATEOAS 約束。對于一個 Maven 項目來說,只需要添加代碼清單 1中的依賴即可。
清單 1. Spring HATEOAS 的 Maven 依賴聲明
<dependency><groupId>org.springframework.hateoas</groupId><artifactId>spring-hateoas</artifactId><version>0.16.0.RELEASE</version> </dependency>資源
REST 架構中的核心概念之一是資源。服務器提供的是資源的表達,通常使用 JSON 或 XML 格式。在一般的 Web 應用中,服務器端代碼會對所使用的資源建模,提供相應的模型層 Java 類,這些模型層 Java 類通常包含 JPA 相關的注解來完成持久化。在客戶端請求時,服務器端代碼通過 Jackson 或 JAXB 把模型對象轉換成 JSON 或 XML 格式。代碼清單 2給出了示例應用中表示列表的模型類 List 的聲明。
清單 2. 表示列表的模型類 List 的聲明
@Entity public class List extends AbstractEntity {private String name;@ManyToOne@JsonIgnoreprivate User user;@OneToMany(mappedBy = "list", fetch = FetchType.LAZY)@JsonIgnoreprivate Set<Item> items = new HashSet<>();protected List() {}public List(String name, User user) {this.name = name;this.user = user;}public String getName() {return name;}public User getUser() {return user;}public Set<Item> getItems() {return items;} }當客戶端請求某個具體的 List 類的對象時,服務器端返回如代碼清單 3所示的 JSON 格式的表達。
清單 3. List 類的對象的 JSON 格式的表達
{"id": 1,"name": "Default" }在代碼清單 3中,服務器端返回的只是模型類對象本身的內容,并沒有提供相關的鏈接信息。為了把模型對象類轉換成滿足 HATEOAS 要求的資源,需要添加鏈接信息。Spring HATEOAS 使用 org.springframework.hateoas.Link 類來表示鏈接。Link 類遵循 Atom 規范中對于鏈接的定義,包含 rel 和 href 兩個屬性。屬性 rel 表示的是鏈接所表示的關系(relationship),href 表示的是鏈接指向的資源標識符,一般是 URI。資源通常都包含一個屬性 rel 值為 self 的鏈接,用來指向該資源本身。
在創建資源類時,可以繼承自 Spring HATEOAS 提供的 org.springframework.hateoas.Resource 類,Resource 類提供了簡單的方式來創建鏈接。代碼清單 4給出了與模型類 List 對應的資源類 ListResource 的聲明。
清單 4. 模型類 List 對應的資源類 ListResource 的聲明
public class ListResource extends Resource {private final List list;public ListResource(List list) {super(list);this.list = list;add(new Link("http://localhost:8080/lists/1"));add(new Link("http://localhost:8080/lists/1/items", "items"));}public List getList() {return list;} }如代碼清單 4所示,ListResource 類繼承自 Resource 類并對 List 類的對象進行了封裝,添加了兩個鏈接。在使用 ListResource 類之后,服務器端返回的表達格式如代碼清單 5所示。
清單 5. 使用 ListResource 類之后的 JSON 格式的表達
{"list": {"id": 1,"name": "Default"},"links": [{"rel": "self","href": "http://localhost:8080/lists/1"},{"rel": "items","href": "http://localhost:8080/lists/1/items"}] }代碼清單 5的 JSON 內容中添加了額外的 links 屬性,并包含了兩個鏈接。不過模型類對象的內容被封裝在屬性 list 中。這是因為 ListResource 類直接封裝了整個的 List 類的對象,而不是把 List 類的屬性提取到 ListResource 類中。如果需要改變輸出的 JSON 表達的格式,可以使用另外一種封裝方式的 ListResource 類,如代碼清單 6所示。
清單 6. 不同封裝格式的 ListResource 類
public class ListResource extends Resource {private final Long id;private final String name;public ListResource(List list) {super(list);this.id = list.getId();this.name = list.getName();add(new Link("http://localhost:8080/lists/1"));add(new Link("http://localhost:8080/lists/1/items", "items"));}public Long getId() {return id;}public String getName() {return name;} }對應的資源的表達如代碼清單 7所示。
清單 7. 使用不同封裝方式的 JSON 格式的表達
{"id": 1,"name": "Default","links": [{"rel": "self","href": "http://localhost:8080/lists/1"},{"rel": "items","href": "http://localhost:8080/lists/1/items"}] }這兩種不同的封裝方式各有優缺點。第一種方式的優點是實現起來很簡單,只需要把模型層的對象直接包裝即可;第二種方式雖然實現起來相對比較復雜,但是可以對資源的表達格式進行定制,使得資源的表達格式更直接。
在代碼實現中經常會需要把模型類對象轉換成對應的資源對象,如把 List 類的對象轉換成 ListResource 類的對象。一般的做法是通過“new ListResource(list)”這樣的方式來進行轉換。可以使用 Spring HATEOAS 提供的資源組裝器把轉換的邏輯封裝起來。資源組裝器還可以自動創建 rel 屬性為 self 的鏈接。代碼清單 8中給出了組裝資源類 ListResource 的 ListResourceAssembler 類的實現。
清單 8. 組裝資源類 ListResource 的 ListResourceAssembler 類的實現
public class ListResourceAssembler extends ResourceAssemblerSupport<List, ListResource> {public ListResourceAssembler() {super(ListRestController.class, ListResource.class);}@Overridepublic ListResource toResource(List list) {ListResource resource = createResourceWithId(list.getId(), list);return resource;}@Overrideprotected ListResource instantiateResource(List entity) {return new ListResource(entity);} }在創建 ListResourceAssembler 類的對象時需要指定使用資源的 Spring MVC 控制器 Java 類和資源 Java 類。對于 ListResourceAssembler 類來說分別是 ListRestController 和 ListResource。ListRestController 類在下一節中會具體介紹,其作用是用來創建 rel 屬性為 self 的鏈接。ListResourceAssembler 類的 instantiateResource 方法用來根據一個模型類 List 的對象創建出 ListResource 對象。ResourceAssemblerSupport 類的默認實現是通過反射來創建資源對象的。toResource 方法用來完成實際的轉換。此處使用了 ResourceAssemblerSupport 類的 createResourceWithId 方法來創建一個包含 self 鏈接的資源對象。
在代碼中需要創建 ListResource 的地方,都可以換成使用 ListResourceAssembler,如代碼清單 9所示。
清單 9. 使用 ListResourceAssembler 的示例
//組裝單個資源對象 new ListResourceAssembler().toResource(list);//組裝資源對象的集合 new ListResourceAssembler().toResources(lists);代碼清單 9中的 toResources 方法是 ResourceAssemblerSupport 類提供的。當需要轉換一個集合的資源對象時,這個方法非常實用。
鏈接
HATEOAS 的核心是鏈接。鏈接的存在使得客戶端可以動態發現其所能執行的動作。在上一節中介紹過鏈接由 rel 和 href 兩個屬性組成。其中屬性 rel 表明了該鏈接所代表的關系含義。應用可以根據需要為鏈接選擇最適合的 rel 屬性值。由于每個應用的情況并不相同,對于應用相關的 rel 屬性值并沒有統一的規范。不過對于很多常見的鏈接關系,IANA 定義了規范的 rel 屬性值。在開發中可能使用的常見 rel 屬性值如表1所示。
表 1. 常用的 rel 屬性
| self | 指向當前資源本身的鏈接的 rel 屬性。每個資源的表達中都應該包含此關系的鏈接。 |
| edit | 指向一個可以編輯當前資源的鏈接。 |
| item | 如果當前資源表示的是一個集合,則用來指向該集合中的單個資源。 |
| collection | 如果當前資源包含在某個集合中,則用來指向包含該資源的集合。 |
| related | 指向一個與當前資源相關的資源。 |
| search | 指向一個可以搜索當前資源及其相關資源的鏈接。 |
| first、last、previous、next | 這幾個 rel 屬性值都有集合中的遍歷相關,分別用來指向集合中的第一個、最后一個、上一個和下一個資源。 |
如果在應用中使用自定義 rel 屬性值,一般的做法是屬性值全部為小寫,中間使用“-”分隔。
鏈接中另外一個重要屬性 href 表示的是資源的標識符。對于 Web 應用來說,通常是一個 URL。URL 必須指向的是一個絕對的地址。在應用中創建鏈接時,在 URL 中使用硬編碼的主機名和端口號顯然不是好的選擇。Spring MVC 提供了相關的工具類可以獲取 Web 應用啟動時的主機名和端口號,不過創建動態的鏈接 URL 還需要可以獲取資源的訪問路徑。對于一個典型的 Spring MVC 控制器來說,其聲明如代碼清單 10所示。
清單 10. Spring MVC 控制器 ListRestController 類的實現
@RestController @RequestMapping("/lists") public class ListRestController {@Autowiredprivate ListService listService;@RequestMapping(method = RequestMethod.GET)public Resources<ListResource> readLists(Principal principal) {String username = principal.getName();return new Resources<ListResource>(new ListResourceAssembler().toResources(listService.findByUserUsername(username)));@RequestMapping(value = "/{listId}", method = RequestMethod.GET)public ListResource readList(@PathVariable Long listId) {return new ListResourceAssembler().toResource(listService.findOne(listId));} }從代碼清單 10中可以看到,Spring MVC 控制器 ListRestController 類通過“@RequestMapping”注解聲明了其訪問路徑是“/lists”,而訪問單個資源的路徑是類似“/lists/1”這樣的形式。在創建資源的鏈接時,指向單個資源的鏈接的 href 屬性值是類似“http://localhost:8080/lists/1”這樣的格式。而其中的“/lists”不應該是硬編碼的,否則當修改了 ListRestController 類的“@RequestMapping”時,所有相關的生成鏈接的代碼都需要進行修改。Spring HATEOAS 提供了 org.springframework.hateoas.mvc.ControllerLinkBuilder 來解決這個問題,用來根據 Spring MVC 控制器動態生成鏈接。代碼清單 11給出了創建單個資源的鏈接的方式。
清單 11. 使用 ControllerLinkBuilder 類創建鏈接
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;Link link = linkTo(ListRestController.class).slash(listId).withSelfRel();通過 ControllerLinkBuilder 類的 linkTo 方法,先指定 Spring MVC 控制器的 Java 類,再通過 slash 方法來找到下一級的路徑,最后生成屬性值為 self 的鏈接。在使用 ControllerLinkBuilder 生成鏈接時,除了可以使用控制器的 Java 類之外,還可以使用控制器 Java 類中包含的方法。如代碼清單 12所示。
清單 12. 通過控制器 Java 類中的方法生成鏈接
Link link = linkTo(methodOn(ItemRestController.class).readItems(listId)).withRel("items");代碼清單 12中的鏈接使用的是 ItemRestController 類中的 readItems 方法。參數 listId 是組成 URI 的一部分,在調用 readItems 方法時需要提供。
上面介紹的是通過 Spring MVC 控制器來創建鏈接,另外一種做法是從模型類中創建。這是因為控制器通常用來暴露某個模型類。如 ListRestController 類直接暴露模型類 List,并提供了訪問 List 資源集合和單個 List 資源的接口。對于這樣的情況,并不需要通過控制器來創建相關的鏈接,而可以使用 EntityLinks。
首先需要在控制器類中通過“@ExposesResourceFor”注解聲明其所暴露的模型類,如代碼清單 13中的 ListRestController 類的聲明。
清單 13. “@ExposesResourceFor”注解的使用
@RestController @ExposesResourceFor(List.class) @RequestMapping("/lists") public class ListRestController {}另外在 Spring 應用的配置類中需要通過“@EnableEntityLinks”注解來啟用 EntityLinks 功能。此外還需要添加代碼清單 14中給出的 Maven 依賴。
清單 14. EntityLinks 功能所需的 Maven 依賴
<dependency><groupId>org.springframework.plugin</groupId><artifactId>spring-plugin-core</artifactId><version>1.1.0.RELEASE</version> </dependency>在需要創建鏈接的代碼中,只需要通過依賴注入的方式添加對 EntityLinks 的引用,就可以使用 linkForSingleResource 方法來創建指向單個資源的鏈接,如代碼清單 15所示。
清單 15. 使用 EntityLinks 創建鏈接
@Autowired private EntityLinks entityLinks;entityLinks.linkForSingleResource(List.class, 1) ?需要注意的是,為了 linkForSingleResource 方法可以正常工作,控制器類中需要包含訪問單個資源的方法,而且其“@RequestMapping”是類似“/{id}”這樣的形式。
回頁首
超媒體控制與 HAL
在添加了鏈接之后,服務器端提供的表達可以幫助客戶端更好的發現服務器端所支持的動作。在具體的表達中,應用雖然可以根據需要選擇最適合的格式,但是在表達的基本結構上應該遵循一定的規范,這樣可以保證最大程度的適用性。這個基本結構主要是整體的組織方式和鏈接的格式。HAL(Hypertxt Application Language)是一個被廣泛采用的超文本表達的規范。應用可以考慮遵循該規范,Spring HATEOAS 提供了對 HAL 的支持。
HAL 規范
HAL 規范本身是很簡單的,代碼清單 16給出了示例的 JSON 格式的表達。
清單 16. HAL 規范的示例 JSON 格式的表達
{"_links": {"self": {"href": "http://localhost:8080/lists"}},"_embedded": {"lists": [{"id": 1,"name": "Default","_links": {"todo:items": {"href": "http://localhost:8080/lists/1/items"},"self": {"href": "http://localhost:8080/lists/1"},"curies": [{"href": "http://www.midgetontoes.com/todolist/rels/{rel}","name": "todo","templated": true}]}}]} }HAL 規范圍繞資源和鏈接這兩個簡單的概念展開。資源的表達中包含鏈接、嵌套的資源和狀態。資源的狀態是該資源本身所包含的數據。鏈接則包含其指向的目標(URI)、所表示的關系和其他可選的相關屬性。對應到 JSON 格式中,資源的鏈接包含在_links 屬性對應的哈希對象中。該_links 哈希對象中的鍵(key)是鏈接的關系,而值(value)則是另外一個包含了 href 等其他鏈接屬性的對象或對象數組。當前資源中所包含的嵌套資源由_embeded 屬性來表示,其值是一個包含了其他資源的哈希對象。
鏈接的關系不僅是區分不同鏈接的標識符,同樣也是指向相關文檔的 URL。文檔用來告訴客戶端如何對該鏈接所指向的資源進行操作。當開發人員獲取到了資源的表達之后,可以通過查看鏈接指向的文檔來了解如何操作該資源。
使用 URL 作為鏈接的關系帶來的問題是 URL 作為屬性名稱來說顯得過長,而且不同關系的 URL 的大部分內容是重復的。為了解決這個問題,可以使用 Curie。簡單來說,Curie 可以作為鏈接關系 URL 的模板。鏈接的關系聲明時使用 Curie 的名稱作為前綴,不用提供完整的 URL。應用中聲明的 Curie 出現在_links 屬性中。代碼中定義了 URI 模板為“http://www.midgetontoes.com/todolist/rels/{rel}”的名為 todo 的 Curie。在使用了 Curie 之后,名為 items 的鏈接關系變成了包含前綴的“todo:items”的形式。這就表示該鏈接的關系實際上是“http://www.midgetontoes.com/todolist/rels/items”。
Spring HATEOAS 的 HAL 支持
目前 Spring HATEOAS 僅支持 HAL 一種超媒體表達格式,只需要在應用的配置類上添加“@EnableHypermediaSupport(type= {HypermediaType.HAL})”注解就可以啟用該超媒體支持。在啟用了超媒體支持之后,服務器端輸出的表達格式會遵循 HAL 規范。另外,啟用超媒體支持會默認啟用“@EnableEntityLinks”。在啟用超媒體支持之后,應用需要進行相關的定制使得生成的 HAL 表達更加友好。
首先是內嵌資源在_embedded 對應的哈希對象中的屬性值,該屬性值是由 org.springframework.hateoas.RelProvider 接口的實現來提供的。對于應用來說,只需要在內嵌資源對應的模型類中添加 org.springframework.hateoas.core.Relation 注解即可,如代碼清單 17所示。
清單 17. 在模型類中添加 @Relation 注解
@Relation(value = "list", collectionRelation = "lists") public class List extends AbstractEntity { }代碼清單 17中聲明了當模型類 List 的對象作為內嵌資源時,單個資源使用 list 作為屬性值,多個資源使用 lists 作為屬性值。
如果需要添加 Curie,則提供 org.springframework.hateoas.hal.CurieProvider 接口的實現,如代碼清單 18所示。利用已有的 org.springframework.hateoas.hal.DefaultCurieProvider 類并提供 Curie 的前綴和 URI 模板即可。
清單 18. 添加 CurieProvider 接口的實現
@Bean public CurieProvider curieProvider() {return new DefaultCurieProvider("todo",new UriTemplate("http://www.midgetontoes.com/todolist/rels/{rel}")); }回頁首
結束語
在開發一個新的 Web 服務或 API 時,REST 架構風格已經成為事實上的標準。在開發時需要明白 REST 架構風格中所包含的約束的含義。HATEOAS 作為 REST 服務約束中最復雜的一個,目前還沒有得到廣泛的使用。但是采用 HATEOAS 所帶來的好處是很大的,可以幫助客戶端和服務器更好的解耦,可以減少很多潛在的問題。Spring HATEOAS 在 Spring MVC 框架的基礎上,允許開發人員通過簡單的配置來添加 HATEOAS 約束。如果應用本身已經使用了 Spring MVC,則同時啟用 HATEOAS 是一個很好的選擇。本文對 REST 和 HATEOAS 的相關概念以及 Spring HATEOAS 框架的使用做了詳細的介紹。
回頁首
下載
| sample_code.zip | 26k |
參考資料
學習
- 參考REST和HATEOAS的維基百科。
- 了解 Richardson 提出的REST 成熟度模型和 Martin Fowler 對此的相關介紹。
- 查看 Spring HATEOAS 的官方網站。
- 了解 IANA 定義的鏈接關系。
- 了解HAL 規范的具體內容。
轉載于:https://www.cnblogs.com/davidwang456/p/6673105.html
總結
以上是生活随笔為你收集整理的使用 Spring HATEOAS 开发 REST 服务--转的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Auto-Publishing and
- 下一篇: Spring Boot: Tuning