SpringBoot與OpenFeign整合,實現(xiàn)微服務之間的聲明式API調(diào)用系統(tǒng)
SpringBoot與OpenFeign的整合為構建微服務架構提供了一種強大且靈活的方式。通過聲明式的API調(diào)用,開發(fā)者可以專注于業(yè)務邏輯的實現(xiàn),而不必擔心底層的網(wǎng)絡通信細節(jié)。結合Spring Cloud生態(tài)中的其他組件,可以進一步增強系統(tǒng)的可伸縮性、可靠性和安全性。
與OpenFeign的整合的好處
簡化HTTP客戶端開發(fā)
- 聲明式編程:通過注解的方式定義HTTP客戶端接口,使得代碼更加簡潔和易于理解。
- 減少樣板代碼:無需手動編寫底層的HTTP請求代碼,減少了重復的工作量。
集成Spring Cloud生態(tài)
- 服務發(fā)現(xiàn):結合Spring Cloud Eureka或Consul等服務注冊中心,可以通過服務名稱自動發(fā)現(xiàn)并調(diào)用相應的服務實例。
- 負載均衡:內(nèi)置支持Ribbon或其他負載均衡策略,確保請求均勻分布到各個服務實例。
- 熔斷機制:結合Hystrix或Resilience4j等庫,可以輕松實現(xiàn)服務間的熔斷保護。
強大的配置能力
- 全局配置:可以通過配置文件統(tǒng)一管理Feign客戶端的行為,如超時設置、重試機制等。
- 自定義配置:可以通過
configuration
屬性指定自定義的配置類,覆蓋默認行為。
易于測試
- Mocking:可以很容易地使用Mockito等工具對Feign客戶端進行單元測試,提高代碼的健壯性。
- 集成測試:通過SpringBootTest框架,可以方便地進行集成測試,驗證服務間的通信是否正常。
Feign客戶端的注冊與初始化
當 SpringBoot應用啟動時,
@EnableFeignClients
注解會觸發(fā)Feign客戶端的掃描和初始化過程。
1. 掃描Feign客戶端接口
- SpringBoot在啟動過程中會掃描帶有
@FeignClient
注解的接口,并將其注冊到 Spring 上下文中。
2. 創(chuàng)建FeignContext
- 每個Feign客戶端都有一個對應的
FeignContext
,用于存儲相關的配置信息,如 Encoder、Decoder、Interceptor 等。
3. 解析接口注解
- Feign使用
Contract
接口來解析接口上的注解(如@GetMapping
、@PostMapping
等),并生成元數(shù)據(jù)。
4. 創(chuàng)建 Target
- Feign使用
Target
接口來表示遠程服務的目標。Target
包含了服務名稱、URL 和類型等信息。
5. 創(chuàng)建Feign.Builder
- Feign使用
Builder
類來構建Feign客戶端實例。Builder
可以配置各種選項,如 Encoder、Decoder、Interceptor 等。
6. 創(chuàng)建Feign.Client
- Feign使用
Client
接口來執(zhí)行實際的 HTTP 請求。默認情況下,F(xiàn)eign使用 JDK 的 HttpURLConnection,但也可以配置為使用 Apache HttpClient 或 OkHttp。
7. 創(chuàng)建 InvocationHandlerFactory
- Feign使用
InvocationHandlerFactory
來創(chuàng)建動態(tài)代理對象的InvocationHandler
,從而實現(xiàn)在方法調(diào)用時自動生成 HTTP 請求。
8. 創(chuàng)建動態(tài)代理對象
- 最后,F(xiàn)eign使用 Java 動態(tài)代理機制,根據(jù)
InvocationHandler
創(chuàng)建具體的Feign客戶端實例。
關鍵組件說明
- @EnableFeignClients:啟用 Feign客戶端掃描。
- @FeignClient:定義 Feign客戶端接口。
- FeignContext:存儲 Feign客戶端的相關配置。
- Targeter:負責創(chuàng)建 Feign客戶端實例。
- Contract:解析接口上的注解,生成元數(shù)據(jù)。
- Encoder/Decoder:處理請求體和響應體的序列化和反序列化。
- Interceptor:攔截 HTTP 請求和響應,進行額外的處理。
- Client:實際執(zhí)行 HTTP 請求的客戶端,如 Apache HttpClient 或 OkHttp。
代碼實操
創(chuàng)建Maven多模塊項目,父pom.xml文件
<?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.example</groupId>
<artifactId>microservices-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>provider-service</module>
<module>consumer-service</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
創(chuàng)建Provider Service
<?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>
<parent>
<groupId>com.example</groupId>
<artifactId>microservices-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>provider-service</artifactId>
</project>
Provider Service - application.properties
# 配置服務器端口
server.port=8081
# 日志級別設置為INFO
logging.level.root=INFO
Provider Service - Application
package com.example.providerservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 主應用程序類,用于啟動Provider Service
*/
@SpringBootApplication
public class ProviderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderServiceApplication.class, args);
}
}
Provider Service - 數(shù)據(jù)傳輸對象(DTO)
package com.example.providerservice.dto;
import lombok.Data;
/**
* 數(shù)據(jù)傳輸對象,用于接收和返回請求信息
*/
@Data
public class RequestDto {
private String name;
private int age;
}
/**
* 響應數(shù)據(jù)傳輸對象,用于返回處理結果
*/
@Data
public class ResponseDto {
private String message;
private String name;
private int age;
}
Provider Service - 隨便搞一個Controller給別人調(diào)用
package com.example.providerservice.controller;
import com.example.providerservice.dto.RequestDto;
import com.example.providerservice.dto.ResponseDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 提供者服務的REST控制器
*/
@RestController
@RequestMapping("/api/provider")
@Slf4j
public class ProviderController {
/**
* 處理POST請求的方法
*
* @param requestDto 請求體中的數(shù)據(jù)
* @return 處理后的響應對象
*/
@PostMapping("/process")
public ResponseDto processRequest(@RequestBody RequestDto requestDto) {
log.info("Received request with name: {} and age: {}", requestDto.getName(), requestDto.getAge());
// 構建響應對象
ResponseDto responseDto = new ResponseDto();
responseDto.setMessage("Processed request for " + requestDto.getName() + " who is " + requestDto.getAge() + " years old.");
responseDto.setName(requestDto.getName());
responseDto.setAge(requestDto.getAge());
return responseDto;
}
}
創(chuàng)建Consumer Service
上面的Provider Service代碼都不是重點,隨便寫一下就可以了。
下面的代碼就要認真看了!
Consumer Service - 引入OpenFeign依賴
<?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>
<parent>
<groupId>com.example</groupId>
<artifactId>microservices-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>consumer-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Consumer Service - application.properties
# 配置服務器端口
server.port=8082
# 日志級別設置為INFO
logging.level.root=INFO
Consumer Service - Application
加上@EnableFeignClients
package com.example.consumerservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 消費者服務的主應用程序類
*/
@SpringBootApplication
@EnableFeignClients
public class ConsumerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class, args);
}
}
Consumer Service - 重點來了
調(diào)用API就是這么簡單!
package com.example.consumerservice.client;
import com.example.consumerservice.dto.RequestDto;
import com.example.consumerservice.dto.ResponseDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* Feign客戶端接口,用于調(diào)用Provider Service的API
*/
@FeignClient(name = "providerService", url = "http://localhost:8081")
public interface ProviderClient {
/**
* 調(diào)用Provider Service的/process接口
*
* @param requestDto 請求體中的數(shù)據(jù)
* @return 處理后的響應對象
*/
@PostMapping("/api/provider/process")
ResponseDto processRequest(@RequestBody RequestDto requestDto);
}
Consumer Service - 數(shù)據(jù)傳輸對象(DTO)
package com.example.consumerservice.dto;
import lombok.Data;
/**
* 數(shù)據(jù)傳輸對象,用于發(fā)送請求信息
*/
@Data
public class RequestDto {
private String name;
private int age;
}
/**
* 響應數(shù)據(jù)傳輸對象,用于接收處理結果
*/
@Data
public class ResponseDto {
private String message;
private String name;
private int age;
}
Consumer Service - 用于測試的Controller
package com.example.consumerservice.controller;
import com.example.consumerservice.client.ProviderClient;
import com.example.consumerservice.dto.RequestDto;
import com.example.consumerservice.dto.ResponseDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 消費者服務的REST控制器
*/
@RestController
@RequestMapping("/api/consumer")
@Slf4j
public class ConsumerController {
private final ProviderClient providerClient;
@Autowired
public ConsumerController(ProviderClient providerClient) {
this.providerClient = providerClient;
}
/**
* 處理POST請求的方法
*
* @param requestDto 請求體中的數(shù)據(jù)
* @return 處理后的響應對象
*/
@PostMapping("/process")
public ResponseDto processRequest(@RequestBody RequestDto requestDto) {
ResponseDto responseDto = providerClient.processRequest(requestDto);
log.info("Received response from Provider Service: {}", responseDto.getMessage());
return responseDto;
}
}
測試
curl -X POST http://localhost:8082/api/consumer/process \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "age": 30}'
Respons:
{
"message": "Processed request for John Doe who is 30 years old.",
"name": "John Doe",
"age": 30
}