使用SpringBoot開啓微服務之旅(詳細步驟)

https://www.tuicool.com/articles/YJZ3amq

本文要點

  • 微服務可以使你的代碼解耦
  • 微服務可以使不同的團隊專注於更小範圍的工作職責、使用獨立的技術、更安全更頻繁地部署
  • SpringBoot支持各種REST API的實現方式
  • 服務發現和服務調用是獨立於服務平臺的
  • Swagger生成穩健的API文檔和調用接口

如果還沒有準備好使用微服務,那你肯定落後於學習曲線中的早期接受者階段了,而且是時候開啓微服務之旅了。本文中,我們將演示創建REST風格微服務所必需的各種組件,使用Consul服務註冊中心和Spring Boot搭建各種腳手架、進行依賴注入和依賴管理,使用Maven進行構建,使用Spring REST和Jersey/JaxRS創建Java REST風格API。

在過去的二十年裏,企業使用SDLC流程變得非常敏捷,但是應用程序仍然相當龐大而且耦合在一起,包含大量支持各種版本的各種各樣API的jar包。但是,如今有一種趨勢朝着更精簡的DevOps範的流程推進,功能也變得“無服務器化”。進行微服務重構可以解耦代碼和資源,讓構建流程更小,讓發佈更安全,讓API更穩定。

本文中,我們將構建一個簡易的股票市場投資組合管理應用程序。在這個應用中,客戶可以通過服務調用,爲他們的股票投資組合(股票代碼和數量)進行定價。投資組合微服務將檢索用戶的投資組合,將它發送給定價微服務來應用最新的定價,然後返回完全定價和分類彙總過的投資組合,通過一個REST調用將所有這些信息展示給客戶。

在我們開始創建微服務之前,需要安裝Consul來準備我們的環境。

下載Consul服務註冊中心

我們將使用Hashicorp Consul來實現服務發現,所以請前往 https://www.consul.io/downloads.html 下載Consul,有Windows版、Linux版和Mac版等。這個鏈接將會提供一個可執行程序,你需要將這個程序添加到你的path環境變量中。

啓動Consul

從一個腳本彈出框以dev模式啓動Consul:

consul agent -dev

爲了驗證它確實已經在運行,可以打開瀏覽器,訪問consul UI http://localhost:8500 。如果一切正常,consul應該會報告它的運行狀態良好。點擊(在左邊的)consul服務,會(在右邊)提供更多信息。

如果這個地方有什麼問題,請確保你已經將consul添加到執行路徑中而且8500和8600端口是可用的。

創建SpringBoot應用程序

我們將使用集成在主流IDE中的 Spring Initializr ,來創建我們的SpringBoot應用程序的腳手架。下面的截屏使用的是IntelliJ IDEA。

選擇File/New Project,來打開新建項目模板彈出框,然後選擇Spring Initializr。

事實上,你可以無需IDE就安裝腳手架。通過SpringBoot Initializr網站 https://start.spring.io完成一個在線web表格,會產出一個可以下載的包含你的空項目的zip文件。

點擊“Next”按鈕,填寫所有的項目元數據。使用下面的配置:

點擊“Next”按鈕來選擇依賴,然後在依賴搜索欄輸入Jersey和Consul Discovery。添加那些依賴:

點擊“Next“按鈕來指定你的項目名字和存放位置。使用在web表單中配置的默認名字“portfolio”,指定你希望存放項目的地址,然後點擊“Finish”來生成並打開項目:

(點擊圖片放大)

你可以使用生成的application.properties文件,但是SpringBoot也接受YAML文件格式,YAML格式看起來更直觀,因此可以將這個文件重命名爲application.yml。

我們將這個微服務命名爲“portfolio-service”。我們可以指定一個端口或者使用端口0來讓應用程序使用一個可用的端口。在我們的例子中,我們使用端口57116。如果你將這個服務作爲一個Docker container部署,你可以將它映射到任何你選中的端口。讓我們通過添加如下配置到applicatin.yml文件,來爲應用程序命名並指定端口:

spring:
 application:
   name: portfolio-service
server:
 port: 57116

爲了讓我們的服務可以被發現,需要爲SpringBoot的application類添加註解。打開PortfolioApplication,在這個類聲明的上方添加@EnableDiscoveryClient。

接受imports。這個class看起來會是這樣:

package com.restms.demo.portfolio;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
. . .
@SpringBootApplication
@EnableDiscoveryClient
public class PortfolioApplication {

  public static void main(String[] args) {
     SpringApplication.run(PortfolioApplication.class, args);
  }
}

(爲了演示如何由各種獨立的平臺組合微服務,我們將爲這個服務使用Jersey,然後爲下一服務使用Spring REST)。

爲了安裝Jersey REST風格Web Service,我們需要指定一個ResourceConfig Configuration類。增加JerseyConfig類(本例中,我們會把它放在相同的package下作爲我們的application類。)它應該看起來像這樣,加上適當的package和imports:

@Configuration
@ApplicationPath("portfolios")
public class JerseyConfig extends ResourceConfig {
   public JerseyConfig()
   {
       register(PortfolioImpl.class);
   }
}

需要注意的是,它繼承了ResourceConfig來表明它是一個Jersey的配置類。@ApplicationPath("portfolios")屬性指定了調用的上下文,意味着調用路徑應該以“portfolios”開頭。(如果你沒有指定,上下文默認爲“/”。)

PortfolioImpl類將服務兩種請求,其中portfolios/customer/{customer-id}返回所有的portfolios,而portfolios/customer/{customer-id}/portfolio/{portfolio-id}返回一個portfolio。一個portfolio包括一組股票代碼和相應的持有份額。

(本例中,有3個客戶,id分別爲0、1、2,而且每一個客戶都有3個portfolio,id分別爲0、1、2)。

你的IDE會讓你創建PortfolioImpl,照着做就行了。本例中,將它添加在相同的package。輸入如下代碼並接受所有imports:

@Component
@Path("/portfolios")
public class PortfolioImpl implements InitializingBean {
   private Object[][][][] clientPortfolios;
   @GET
   @Path("customer/{customer-id}")
   @Produces(MediaType.APPLICATION_JSON)
   // a portfolio consists of an array of arrays, each containing an array of 
   // stock ticker and associated shares
   public Object[][][] getPortfolios(@PathParam("customer-id") int customerId)
   {
       return clientPortfolios[customerId];
   }

   @GET
   @Path("customer/{customer-id}/portfolio/{portfolio-id}")
   @Produces(MediaType.APPLICATION_JSON)
   public Object[][] getPortfolio(@PathParam("customer-id") int customerId, 
                           @PathParam("portfolio-id") int portfolioId) {
       return getPortfolios(customerId)[portfolioId];
   }

   @Override
   public void afterPropertiesSet() throws Exception {
       Object[][][][] clientPortfolios =
       {
         {
		// 3 customers, 3 portfolios each
           {new Object[]{"JPM", 10201}, new Object[]{"GE", 20400}, new Object[]{"UTX", 38892}},
           {new Object[]{"KO", 12449}, new Object[]{"JPM", 23454}, new Object[]{"MRK", 45344}},
           {new Object[]{"WMT", 39583}, new Object[]{"DIS", 95867}, new Object[]{"TRV", 384756}},
         }, {
           {new Object[]{"GE", 38475}, new Object[]{"MCD", 12395}, new Object[]{"IBM", 91234}},
           {new Object[]{"VZ", 22342}, new Object[]{"AXP", 385432}, new Object[]{"UTX", 23432}},
           {new Object[]{"IBM", 18343}, new Object[]{"DIS", 45673}, new Object[]{"AAPL", 23456}},
         }, {
           {new Object[]{"AXP", 34543}, new Object[]{"TRV", 55322}, new Object[]{"NKE", 45642}},
           {new Object[]{"CVX", 44332}, new Object[]{"JPM", 12453}, new Object[]{"JNJ", 45433}},
           {new Object[]{"MRK", 32346}, new Object[]{"UTX", 46532}, new Object[]{"TRV", 45663}},
         }
       };

       this.clientPortfolios = clientPortfolios;
   }
}

@Component註解表明這是一個Spring組件類,將它暴露爲一個端點。正如我們從方法的註解中看到的那樣,@Path註解聲明這個類可以通過“portfolios”路徑訪問到,兩個支持的api調用可以通過portfolios/customer/{customer-id}和portfolios/customer/{customer-id}/portfolio/{portfolio-id}。這些方法通過@GET註解表明它服務HTTP GET請求,這個方法聲明返回一個數組並註解爲返回Json,因此它會返回一個Json數組。注意如何在方法聲明中使用@PathParam註解來從request中提取映射的參數。

(本例中,我們返回硬編碼的值。當然,在實際應用中,實現的服務在這裏會查詢數據庫或其它一些服務或者數據源。)

現在構建這個項目,然後運行。如果你是在使用IntelliJ,它會創建一個默認的可運行程序,你只需點擊綠色的“運行”箭頭。你還可以使用

mvn spring-boot:run

或者,你可以運行一次maven install,然後使用java -jar並指定target目錄下生成的jar文件來運行這個應用程序:

java -jar target\portfolio-0.0.1-SNAPSHOT.jar

我們現在應該可以在Consul中查看這個服務,所以返回瀏覽器,打開 http://localhost:8500/ui/#/dc1/services (如果你已經打開了這個地址,刷新就可以了)。

我們看到我們的portfolio-service在那裏了,但是顯示爲failing(失敗)。那是因爲Consol在等待從我們的服務發送一個“健康”的心跳請求。

爲了生成心跳請求,我們在應用程序的pom文件中增加SpringBoot “Actuator”服務 的依賴。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在pom文件中,請注意,Jersey版本在consul-starter和jersey-starter中有一個版本衝突。爲了解決這個衝突,將jersey starter移爲第一個依賴。

你的pom文件現在應該包含如下依賴:

<dependencies>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-jersey</artifactId>
  </dependency>
  <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-consul-discovery</artifactId>
  </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>
</dependencies>

重啓Consul,然後portfolio-service會顯示正常:

現在在portfolio-service下有兩個通過的節點,其中一個是我們實現的portfolio服務,另外一個是心跳服務。

檢查分配的端口。你可以在應用程序輸出臺看到:

INFO 19792 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 57116 (http)

你也可以直接在consul UI中查看這個端口。點擊portfolio-service,然後選擇“Service 'portfolio-service'”鏈接,會顯示該服務的端口,本例中爲57116。

調用 http://localhost:57116/portfolios/customer/1/portfolio/2 ,然後你會看到json數組 [["IBM",18343],["DIS",45673],["AAPL",23456]]。

我們第一個微服務就正式開放了!

定價服務

接下來,我們會創建定價服務,這一次使用Spring RestController而不是Jersey。

定價服務會接受客戶端id和portfolio id作爲參數,然後會使用一個RestTemplate查詢portfolio服務來獲取股票代碼和份額,隨後返回當前的價格。(這些都是假數據,所以不要用這些數據來做交易決策!)

使用如下信息創建一個新項目:

這次選擇Web、Consul Discovery和Actuator依賴:

(點擊圖片放大)

將項目命名爲“pricing”,在你選中的目錄中生成項目。

這次我們會使用application.properties而不是application.yml。

在application.properties中設置名字和端口如下:

spring.application.name=pricing
server.port=57216

用@EnableDiscoveryClient給PricingApplication註解。這個類應該看起來像這樣,加上package和imports。

@SpringBootApplication
@EnableDiscoveryClient
public class PricingApplication {
  public static void main(String[] args) {
     SpringApplication.run(PricingApplication.class, args);
  }
}

接下來,我們會創建PricingEndpoint類。這個類有一點冗長,因爲它演示了一些重要的功能,包括服務發現(查找portfolio service)和使用RestTemplate來創建一個查詢:

@RestController
@RequestMapping("/pricing")
public class PricingEndpoint implements InitializingBean {
   @Autowired
   DiscoveryClient client;
   Map<String, Double> pricingMap = new HashMap<>();

   RestTemplate restTemplate = new RestTemplate();

   @GetMapping("/customer/{customer-id}/portfolio/{portfolio-id}")
   public List<String> getPricedPortfolio(
                           @PathVariable("customer-id") Integer customerId, 
                           @PathVariable("portfolio-id") Integer portfolioId)
   {
      List<ServiceInstance> instances 
                                  = client.getInstances("portfolio-service");
      ServiceInstance instance 
             = instances.stream()
                        .findFirst()
                        .orElseThrow(() -> new RuntimeException("not found"));
      String url = String.format("%s/portfolios/customer/%d/portfolio/%d", 
                                 instance.getUri(), customerId, portfolioId);
      // query for the portfolios, returned as an array of List 
      // of size 2, containing a ticker and a position (# of shares)
      Object[] portfolio = restTemplate.getForObject(url, Object[].class);
      // Look up the share prices, and return a list of Strings, formatted as
      // ticker, shares, price, total
      List<String> collect = Arrays.stream(portfolio).map(position -> {
          String ticker = ((List<String>) position).get(0);
          int shares = ((List<Integer>) position).get(1);
          double price = getPrice(ticker);
          double total = shares * price;
          return String.format("%s %d %f %f", ticker, shares, price, total);
      }).collect(Collectors.toList());
      return collect;
   }

   private double getPrice(String ticker)
   {
      return pricingMap.get(ticker);
   }

   @Override
   public void afterPropertiesSet() throws Exception {
       pricingMap.put("MMM",201.81);
       pricingMap.put("AXP",85.11);
       pricingMap.put("AAPL",161.04);
       pricingMap.put("BA",236.32);
       pricingMap.put("CAT",118.02);
       pricingMap.put("CVX",111.31);
       pricingMap.put("CSCO",31.7);
       pricingMap.put("KO",46.00);
       pricingMap.put("DIS",101.92);
       pricingMap.put("XOM",78.7);
       pricingMap.put("GE",24.9);
       pricingMap.put("GS",217.62);
       pricingMap.put("HD",155.82);
       pricingMap.put("IBM",144.29);
       pricingMap.put("INTC",35.66);
       pricingMap.put("JNJ",130.8);
       pricingMap.put("JPM",89.75);
       pricingMap.put("MCD",159.81);
       pricingMap.put("MRK",63.89);
       pricingMap.put("MSFT",73.65);
       pricingMap.put("NKE",52.78);
       pricingMap.put("PFE",33.92);
       pricingMap.put("PG",92.79);
       pricingMap.put("TRV",117.00);
       pricingMap.put("UTX",110.12);
       pricingMap.put("UNH",198.00);
       pricingMap.put("VZ",47.05);
       pricingMap.put("V",103.34);
       pricingMap.put("WMT", 80.05);

   }
}

爲了發現portfolio服務,我們需要訪問一個DiscoveryClient。這可以通過Spring的@Autowired註解輕鬆實現

@Autowired
   DiscoveryClient client;

然後在服務調用中,用這個DiscoveryClient實例來尋址我們的服務:

List<ServiceInstance> instances = client.getInstances("portfolio-service");
ServiceInstance instance = instances.stream().findFirst().orElseThrow(() -> new RuntimeException("not found"));

一旦尋址到這個服務,我們可以用它來執行我們的請求。這個請求是我們根據在portflo-service中創建的api調用組合而成的。

String url = String.format("%s/portfolios/customer/%d/portfolio/%d", instance.getUri(), customerId, portfolioId);

最終,我們使用一個RestTemplate來執行我們的GET請求。

Object[] portfolio = restTemplate.getForObject(url, Object[].class);

需要注意的是,對於RestControllers(和SpringMVC RequestController一樣),路徑變量可以從@PathVariable註解中提取,而不像Jersey那樣從@PathParam中提取。

這裏使用一個Spring RestController來將定價服務發佈出去。

文檔

我們已經克服所有困難創建了我們的微服務,但是如果不讓世界知道如何使用它們,它們就不會產生任何價值。

爲此,我們使用了一個稱作 Swagger 的工具。Swagger是一個簡單易用的工具,不僅爲我們的API調用生成文檔,還提供了一個可以援引這些文檔的易用的web客戶端。

首先,讓我們在pom文件中指定Swagger:

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>2.7.0</version>
</dependency>
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>2.7.0</version>
</dependency>

接下來,我們需要告訴Swagger想要爲哪些類生成文檔。我們需要引入一個稱爲SwaggerConfig的新類,它包含Swagger的各種配置。

@Configuration
@EnableSwagger2
public class SwaggerConfig {
   @Bean
   public Docket api() {
       return new Docket(DocumentationType.SWAGGER_2)
               .select()
               .apis(RequestHandlerSelectors.any())
               .paths(PathSelectors.regex("/pricing.*"))
               .build();
   }
}

我們可以看下這個類做了什麼。首先,我們用@EnableSwagger2註解表明它是一個Swagger配置。

接下來,我們創建了一個Docket bean,告訴Swagger要暴露哪些API。在上面的例子中,我們告訴Swagger暴露所有以“/pricing”開頭的路徑。還可以選擇指定class文件而不是路徑來生成文檔:

.apis(RequestHandlerSelectors.basePackage("com.restms.demo"))
.paths(PathSelectors.any())

重啓定價微服務,然後在瀏覽器上調用 http://localhost:57216/swagger-ui.html 。

點擊“List Operations”按鈕來查看詳細的服務操作。

點擊“Expand Opeartions”來創建一個基於form的查詢調用。提供一些參數,點擊“Try it out!”,然後等待響應結果:

(點擊圖片放大)

你可以通過給方法增加Swagger註解來增加更多的顏色。

例如,使用@ApiOperation註解來裝飾已有的方法PricingImpl.getPricedPortfolio:

@ApiOperation(value = "Retrieves a fully priced portfolio",
       notes = "Retrieves fully priced portfolio given customer id and portfolio id")
@GetMapping("/customer/{customer-id}/portfolio/{portfolio-id}")
public List<String> getPricedPortfolio(@PathVariable("customer-id") Integer customerId, @PathVariable("portfolio-id") Integer portfolioId)

重啓並刷新swagger-ui,查看新創建的文檔:

你還可以用Swagger做許多事情,更多詳情請查看它的文檔。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章