在展開 Spring Cloud 的微服務架構部署之前, 我們先了解一下用于建構微服務的基礎架構-Spring Boot。 由于 Spring Cloud 的建構基于 Spring Boot 實作, 在後續的示例中我 們将大量使用 Spring Boot 來建構微服務架構中的基礎設施以及一些試驗中使用的微服務。 為了能夠輔助後續内容的介紹,確定讀者有一定的Spring Boot基礎,在這裡先對Spring Boot 做一個簡單的介紹, 以保證讀者能夠有一定的基礎去了解後續介紹的内容并順利完成後續 的一些示例試驗。
在這裡介紹 Spring Boot 的目的除了它是 Spring Cloud 的基礎之外, 也由于其自身的各 項優點, 如自動化配置、 快速開發、 輕松部署等, 非常适合用作微服務架構中各項具體微 服務的開發架構。是以我們強烈推薦使用 Spring Boot 來建構微服務, 它不僅可以幫助我們 快速地建構微服務, 還可以輕松簡單地整合 Spring Cloud 實作系統服務化, 而如果使用了 傳統的 Spring 建構方式的話, 在整合過程中我們還需要做更多的依賴管理工作才能讓它們 完好地運作起來。
在本文中我們将介紹下面這些與後續介紹有密切聯系的内容:
• 如何建構 Spring Boot 項目
• 如何實作 RESTfulAPI 接口
• 如何實作多環境的 Spring Boot 應用配置
• 深入了解 Spring Boot 配置的啟動機制
• Spring Boot 應用的監控與管理
1、架構介紹
對于很多Spring架構的初學者來說, 經常會因為其繁雜的配置檔案而卻步。 而對于很 多老手來說, 每次新建構項目總是會重複複制粘貼一 些差不多的配置檔案這樣枯燥乏味的事。
Spring Boot的出現 可以有效改善這類問題,SpringBoot的宗旨并非要重寫Spring或是 替代Spring, 而是希望通過設計大量的自動化配置等方式來簡化Spring原有樣闆化的配置, 使得開發者可以快速建構應用。
除了解決配置問題之外, Spring Boot還通過一系列StaiterPOMs的定義, 讓我們整合 各項功能的時候, 不需要在 Maven的pom.xml中維護那些錯綜複雜的依賴關系, 而是通 過類似子產品化的Starter子產品定義來引用, 使得依賴管理工作變得更為簡單。
在如今容器化大行其道的時代,Spring Boot除了可以很好融入Docker之外, 其自身就 支援嵌入式的 Tomcat、 Jetty 等容器。 是以, 通過Spring Boot 建構的應用不再需要安裝 Tomcat, 将應用打包成war, 再部署到Tomcat 這樣複雜的建構與部署動作, 隻需将Spring Boot應用打成jar包, 并通過java -jar指令直接運作就能啟動一個标準化的Web應用, 這使得Spring Boot應用變得非常輕便。
2、快速搭建SpringBoot項目
在本文中, 我們将逐漸指引讀者建立一個Spring Boot的基礎項目, 并且實作 一個簡單 的RESTfulAPL 通過這個例子對Spring Boot有一個初步的了解, 并體驗其結構簡單、 開 發迅速的特性。
項目建構與解析
系統及工具版本要求
• Java 7及以上版本
• Spring Framework 4.2.7及以上版本
• Maven 3.2及以上版本/Gradle 1.12及以上版本
本文内容均采用Java 1.7、 Spring Boot 1.5.10調試通過。
工程結構解析
• src/main/java: 主程式入口 DmsApplication, 可以通過直接運作該類來 啟動Spring Boot應用。
• src/main/resources: 配置目錄, 該目錄用來存放應用的一些配置資訊, 比如 應用名、服務端口、資料庫連結等。由千我們引入了Web子產品,是以産生了static 目錄與templates目錄, 前者用于存放靜态資源, 如圖檔、 css、JavaScript等; 後者用千存放Web頁面的模闆檔案, 這裡我們主要示範提供RESTful APL是以這 兩個目錄并不會用到。
• src/test/: 單元測試目錄, 生成的DmsApplicationTests通過JUnit 4實 現, 可以直接用運作Spring Boot應用的測試。 後文中, 我們會示範如何在該類中測 試RESTfulAPI。
Maven配置分析 打開目前工程下的pom.xml檔案, 看看生成的項目都引入了哪些依賴來建構Spring Boot工程, 内容大緻如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.baihe.dms</groupId>
<artifactId>dms</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>dms</name>
<description>Deduct Money System</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.14</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在基礎資訊部分, groupid和 artifactId 對應生成項目時頁面上輸入的内容。 另 外, 我們還可以注意到, 打包形式為 jar, 正如我們 之前所介紹的,Spring Boot預設将該Web應用打包為jar 的形式, 而非war 的形式, 因為 預設 的Web子產品依賴 會包含嵌入式的Tomcat , 這樣使得我們的應用jar自身就具備了提供 Web服務的能力, 後續我們會示範如何啟動它。
父項目parent配置指定為 spring-boot-starter-parent的1. 5.10 版本, 該父項 目中定義了Spring Boot版本的基礎依賴以及 一 些預設配置内容 , 比如,配置檔案application.properties的位置等。 在項目依賴 dependencies配置中, 包含了下面兩項。
• spring-boot-starter-web: 全棧Web開發子產品, 包含嵌入式Tomcat、 Spring MVC。
• spring-boot-starter-test: 通用測試子產品, 包含JUnit、 Hamcrest、 Mockito 。
這裡所引用的web和test 子產品,在SpringBoot 生态中被稱為Starter POMs。Starter POMs 是一系列輕便的依賴 包, 是一套一站式的Spring相關技術的解決方案。 開發者在使用和整 合子產品時, 不必再去搜尋樣例代碼中的依賴配置來複制使用, 隻需要引入對應的子產品包即 可 。
比如, 開發Web應用的時候, 就引入spring-boot-starter-web, 希望應用具備 通路資料庫能力的時候, 那就再引入 spring-boot-starter-jdbc 或是更好用的 spring-boot-starter-data-jpa。 在使用SpringBoot建構應用的時候, 各項功能模 塊的整合不再像傳統Spring應用的開發方式那樣,需要在 pom.xml中做大量的依賴配置, 而是通過使用StarterPOMs定義的依賴包,使得功能子產品整合變得非常輕巧, 易于了解與使用。
3、實作RESTfulAPI
在Spring Boot中建立一個RESTfulAPI的實作代碼同SpringMVC應用一樣, 隻是不 需要像SpringMVC那樣先做很多配置, 而是像下面這樣直接開始編寫Controller内容:
• 建立package, 命名為com.baihe.dms.controller.CmsController, 可根據實際的建構情況修改成自 己的路徑。
• 建立CmsController類,内容如下所示。
package com.baihe.dms.controller;
import com.baihe.dms.entity.common.CmsException;
import com.baihe.dms.entity.common.ResponseData;
import com.baihe.dms.service.WithholdService;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller("cmsController")
@RequestMapping(value = "/")
public class CmsController {
private Logger logger = Logger.getLogger(this.getClass());
private WithholdService withholdService;
CmsController(@Autowired WithholdService withholdService) {
this.withholdService = withholdService;
}
@RequestMapping(value = "/withhold" , method = RequestMethod.POST)
@ResponseBody
public ResponseData withhold(String contractNo, Long requestNo, Long userId, Long projectId,
Integer planStep, String bankCode, Double totalAmount, Integer overdueFlag, Integer incomeFree) {
try {
return withholdService.withhold(contractNo, requestNo, userId, projectId, planStep, bankCode, totalAmount, overdueFlag, incomeFree);
} catch (CmsException e) {
return ResponseData.no(e.getErrCode());
} catch (Exception e) {
logger.debug("withhold", e);
return ResponseData.no(ResponseData.INTERNAL_ERROR);
}
}
@GetMapping("/withholdCancelOrRecover")
@ResponseBody
public ResponseData withholdCancelOrRecover(Long requestNo, int status) {
try {
return withholdService.withholdCancelOrRecover(requestNo, status);
} catch (CmsException e) {
return ResponseData.no(e.getErrCode());
} catch (Exception e) {
logger.debug("withholdCancelOrRecover", e);
return ResponseData.no(ResponseData.INTERNAL_ERROR);
}
}
/*
* 由于豁免流程的存在
* 會出現立即還款情況
* 參數 requestNo 還款計劃流水号
* channelType 扣款管道 寶付還是易寶
* */
@RequestMapping("/atOnceWithhold")
@ResponseBody
public ResponseData atOnceWithhold(String requestNo, String channelType) {
try {
return withholdService.atOnceWithhold(Long.valueOf(requestNo), channelType);
} catch (CmsException e) {
return ResponseData.no(e.getErrCode());
} catch (Exception e) {
logger.debug("atOnceWithhold", e);
return ResponseData.no(ResponseData.INTERNAL_ERROR);
}
}
}
ResponseData類:
package com.baihe.dms.entity.common;
import java.io.Serializable;
/**
* 接口傳回的資料
*/
public class ResponseData implements Serializable {
private static final long serialVersionUID = 2047667816784695690L;
public static final int OK = 200; // 非 OK 的都是失敗
public static final int NO = -1;
public static final int INTERNAL_ERROR = -99;
public static final int INVALID_PARAMETER = -100;
public static final int NULL_PARAMETER = -101;
public static final int INVALID_AMOUNT = -102;
public static final int INVALID_BANKCODE = -103;
public static final int HAS_UNFINISHED_REQUEST = -104;
public static final int INVALID_SPLIT_CONFIG = -105;
public static final int SPLIT_TOO_MANY = -106;
public static final int NOT_NEED_SPLIT = -107;
private Integer code = NO;
private String message = "";
private Object data;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
private void generateMessage(int errCode) {
String message = "未知錯誤";
switch (errCode) {
case INTERNAL_ERROR:
message = "内部錯誤";
break;
case INVALID_PARAMETER:
message = "無效的參數";
break;
case NULL_PARAMETER:
message = "參數不能為空";
break;
case INVALID_AMOUNT:
message = "無效的金額";
break;
case INVALID_BANKCODE:
message = "無效的銀行編碼";
break;
case HAS_UNFINISHED_REQUEST:
message = "使用者尚有未完成的交易";
break;
case INVALID_SPLIT_CONFIG:
message = "拆配置設定置無效";
break;
case SPLIT_TOO_MANY:
message = "拆分數目過多";
break;
case NOT_NEED_SPLIT:
message = "金額沒有變化,無需拆分";
break;
case NO:
message = "内部錯誤";
break;
case OK:
message = "成功";
break;
}
this.message = message;
}
public static ResponseData create(int code, Object data) {
ResponseData responseData = new ResponseData();
responseData.code = code;
responseData.generateMessage(code);
responseData.data = data;
return responseData;
}
public static ResponseData create(int code) {
return create(code, null);
}
public static ResponseData ok(Object data) {
return ResponseData.create(ResponseData.OK, data);
}
public static ResponseData ok() {
return ResponseData.create(ResponseData.OK, null);
}
public static ResponseData no(int code, Object data) {
return ResponseData.create(code, data);
}
public static ResponseData no(int code) {
return ResponseData.create(code);
}
}
啟動Spring Boot應用的方式有很多種:
• 作為一個 Java 應用程式, 可以直接通過運作擁有 main 函數的類來啟動。
• 在 Maven 配置中, 之前提到了 spring-boot 插件, 可以使用它來啟動, 比如執行 mvn spring-boot: run 指令。
• 在伺服器上部署運作時, 通常先使用 mvn install 将應用打包成 jar 包, 再通過 java -jar xxx. jar 來啟動應用。
配置詳解
在面我們輕松地實作了一個簡單的RESTfulAPI應用, 體驗了SpringBoot的 諸多優點。我們用非常少的代碼就成功實作了一個Web應用, 這是傳統Spring應用無法辦到的。雖然在實作Controller時用到的代碼是一樣的,但是在配置方面,相信大家也注意到了, 在上面的例子中, 除了Maven的配置之外, 沒有引入任何其他配置。
這就是之前我們提到的,SpringBoot針對常用的開發場景提供了一系列自動化配置來 減少原本複雜而又幾乎很少改動的模闆化配置内容。但是,我們還是需要 了解如何在Spring Boot中修改這些自動化的配置内容, 以應對一些 特殊的場景需求, 比如, 我們在同一台主 機上需要啟動多個基千Spring Boot的Web應用, 若不為每個應用指定特别的端口号, 那 麼預設的8080 端口必将導緻沖突。 後續我們在使用SpringCloud的各個元件的時候, 其實有大量的工作都 會是針對配置 檔案的。是以我們有必要深入了解一些關于SpringBoot中的配置檔案的知識, 比如配置方 式、 如何實作多環境配置、 配置資訊的加載順序等。
- 配置檔案
在快速入門示例中, 我們介紹Spring Boot 的工程結構時, 提到過 src/rnain/ resources 目錄是Spring Boot的配置目錄, 是以當要為應用建立個性化配置時, 應在該 目錄下進行。
Spring Boot 的預設配置檔案位置為 src/main/resources/application. properties 。關于SpringBoot應用的配置内容都可以集中在該檔案中, 根據我們引入的 不 同Starter子產品,可以在這裡定義容器端口号、 資料庫連接配接資訊、 日志級别等各種配置信 息。比如, 我們需要自定義Web子產品的服務端口号,可以在application.properties 中添加 server.port=8888 來指定服務端口為 8888 , 也可 以通過 spring.appliction.name =hello 來指定應用名(該名字在後續SpringCloud中會被 注冊為服務名)。
Spring Boot的配置檔案除了可以使用傳統的 properties檔案之外,還支援現在被廣泛推 薦使用的YAML檔案。
YAML 采用的配置格式不像 properties 的配置那樣以單純的鍵值對形式來表示,而是以 類似大綱的縮進形式來表示。 下面是一段 YAML 配置資訊:
server:
port: 8081
spring:
profiles:
active: prod
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.baihe.dms.entity.common
configLocation: classpath:mybatis-config.xml
logging:
level:
com.baihe.dms.mapper: debug
endpoints:
shutdown:
enabled: true
sensitive:
false
-
自定義參數
除了可以在 Spring Boot 的配置檔案中設定各個 Starter 子產品中預定義的配置屬性, 也可 以在配置檔案中定義一些我們需要的自定義屬性。 比如在 application.properties 中 添加: book.name=SpringCloudinAction
book.author=ZhaiYongchao
然後, 在應用中可以通過@Value 注解來加載這些自定義的參數,
比如:
@Component
public class Book {
@Value("${book.name}")
private String name;
@Value("${book.author}")
private String author;
//省略getter和setter @Value 注解加載屬性值的時候可以支援兩種表達式來進行配置,
如下所示。
• 一種是上面介紹的 PlaceHolder 方式, 格式為${...}, 大括号内為 PlaceHolder。
• 另一種是使用SpEL 表達式 (Spring Expression Language), 格式為#{...}, 大括号 内為 SpEL 表達式。
-
使用随機數
在 一些特殊情況下, 我們希望有些參數每次被加載的時候不是 一個固定的值, 比如密 鑰、 服務端口等。 在 SpringBoot的屬性配置檔案中, 可以 通過 使用${random}配置來産 生随機的int值、long值或者string字元串,這樣我們就可以容易地通過 配置随機生成屬性, 而不是在程式中通過編碼來實作這些邏輯。
#随機字元串 com.didispace.blog.value=${random.value}
#随機int com.didispace.blog.number=${random.int}
#随機long com.didispace.blog.bignumber=${random.long}
# 10以内的随機數 com.didispace.blog.test1=${random.int(l0)}
# 10-20的随機數 com.didispace.blog.test2=${random.int[l0,20]}
-
指令行參數
在用指令行方式 啟 動 Spring Boot 應用時, 連續的兩個減号--就 是對 application.properties 中的屬性值進行指派 的辨別。 是以 , java -jar xxx.jar--server.port=8888指令, 等價千在 application.properties 中添加 屬性server.port= 8888。 通過指令行來修改屬性值是 SpringBoot非常重要的一個特性。 通過此特性, 理論上已經使得應用的屬性在啟動前是可變的, 是以其中的端口号也好、 資料庫連接配接也好, 都是可 以在應用啟動時發生改變的, 而不同于以往的Spring應用通過Maven的Profile在編譯器 中進行不同環境的建構。 SpringBoot的這種方式, 可以讓應用程式的打包内容貫穿開發、 測試以及線上部署, 而Maven不同Profile的方案為每個環境所建構的包,其内容本質上是 不同的。 但是, 如果 每個參數都需要通過指令行來指定, 這顯然也不是 一個好的方案, 所 以下面我們看看如何在SpringBoot中實作多環境的配置。
-
多環境配置
我們在開發應用的時候, 通常同一套程式會被應用和安裝到幾個不同的環境中, 比如 開發 、 測試、 生産等。 其中 每個環境的資料庫位址、 伺服器端口等配置都不同, 如果在為 不同環境打包時都要頻繁修改配置檔案的話, 那必将是個非常煩瑣且容易發生錯誤的事。 對于多環境的配置,各種項目建構工具或是架構的基本思路是 一緻的, 通過配置多份 不同環境的配置檔案,再通過打包指令指定需要打包的内容之後進行區分打包,SpringBoot 也不 例外, 或者說實作起來更加簡單。
在 Spring Boot 中, 多環境配置的檔案名需要滿足 application-{profile}. proper巨es的格式, 其中{profile}對應你的環境辨別,
• applicaction-dev.properties: 開發環境。
• applicaction-test.properties: 測試環境。
• application-prod.properties: 生産環境。
至于具體哪個配置檔案會被加載, 需要在 application.properties 檔案中通過 spring.profiles.active 屬性來設定, 其 值 對應配置檔案中的{profile}值。 如 spring.profiles.active= test就會加載 application-test.properties配置 檔案内容。
-
加載順序
為了能夠更合理地重寫各屬性的值,SpringBoot使用了下面這種較為特别的屬性加載 順序:
1 在指令行中傳入的參數。
2. SPRING APPLICATION JSON中的屬性。 SPRING_APPLICATION—JSON是以 JSON格式配置在系統環境變量中的内容。
3. java:comp/env中的JNDI 屬性。
4. Java的系統屬性, 可以通過System.getProperties()獲得的内容。
5 作業系統的環境變量 。
6 通過random.*配置的随機屬性。
7 位于目前應用 jar 包之外,針對不同{profile}環境的配置檔案内容,例如 application-{profile}.properties或是YAML定義的配置檔案。
8 位于目前應用 jar 包之内 ,針對不同{profile}環境的配置檔案内容,例如 application-{profile}.properties或是YAML定義的配置檔案。
9 位于目前應用jar包之外的application.properties和YAML配置内容。
10位于目前應用jar包之内的application.properties和YAML配置内容。
11在@Configura巨on注解修改的類中,通過@PropertySource注解定義的屬性。
12應用預設屬性,使用SpringApplication.setDefaultProperties 定義的 内容。
優先級按上面的順序由高到低,數字越小優先級越高。