1.背景
SpringCloud中,使用Eureka作爲服務的註冊中心,Feign作爲輕量級的Rest Client,使得編寫Web客戶端更容易,只需創建一個接口加上Feign相應的註解,即可完成接口在Eureka上的註冊。
在技術中臺目前的架構中,使用了Consul作爲服務治理方案中的註冊中心,應用系統通過部署組件(Sidecar),自動將服務註冊到Consul的註冊中心中,訪問授權組件(Gateway)根據該產品已經註冊到Consul註冊中心的可用實例進行轉發。
當使用了Eureka + Feign的工程,按照API集市的使用方式(Sidecar + Gateway)進行整合時,就會出現同一個項目要使用兩個註冊中心的情況。
我們希望能夠使用統一的註冊中心,結合API集市所提供的服務的註冊方式和內容,減少各項目使用Eureka所帶來的維護和資源的成本,即不需要額外部署及維護Eureka集羣。所以尋找 Consul + Feign 的方案,以替代 Eureka + Feign 的方案。
2.方案實踐
開發環境和工具:jdk1.8
, gradle-5.2.1
本文中的工程都是採用Springboot,SpringCloud版本使用Greenwich.SR2
,使用的Gradle沒有使用Maven作爲管理工具。
2.1. Eureka + Feign 方案
實現流程:
1.啓動Eureka服務端
2.部署以Eureka爲註冊中心的Provider
3.啓動以Eureka爲註冊中心的Consumer
4.接口服務驗證
2.1.1 Eureka服務註冊中心
工程名:eureka-server
引入Eureka服務端相關依賴
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Eureka服務application.yml配置
server:
# 註冊中心端口
port: 8761
eureka:
instance:
hostname: localhost
client:
# 是否註冊到註冊中心,該項目本身屬於註冊中心,所以爲false
register-with-eureka: false
# 是否從註冊中心拉取服務列表,false爲不拉取
fetch-registry: false
serviceUrl:
# 註冊服務器的地址,服務提供者和消費者使用這個地址對服務進行註冊和消費
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
創建SpringBoot啓動類Application,添加@SpringBootApplication和@EnableEurekaServer註解
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
啓動服務註冊中心,並訪問http://localhost:8761/
,界面如下圖所示:
2.1.2 Eureka服務提供者
工程名:eureka-provider
引入Eureka服務提供者相關依賴
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// 解析OpenAPI格式文檔
implementation 'io.swagger.parser.v3:swagger-parser:2.0.12'
// 生成OpenAPI文檔
implementation 'io.springfox:springfox-swagger2:2.9.2'
implementation 'io.springfox:springfox-swagger-ui:2.9.2'
// Getter/Setter生成
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Entity字段映射
implementation 'org.mapstruct:mapstruct:1.3.0.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.3.0.Final'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Eureka服務提供者application.yml配置
server:
port: ${CONSUL_FEIGN_PROVIDER_PORT:8808}
spring:
application:
# 定義在註冊中心的服務提供者的名稱
name: feign-provider
eureka:
client:
serviceUrl:
# 服務註冊中心的地址
defaultZone: http://localhost:8761/eureka/
創建SpringBoot啓動類Application,添加@SpringBootApplication和@EnableEurekaClient註解
@SpringBootApplication
@EnableEurekaClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
服務提供者Controller代碼如下
@RestController
@RequestMapping(value = "/api/feign")
@Slf4j
public class HelloWorldController {
@Autowired
private HelloWorldService helloWorldService;
@Autowired
private Environment environment;
/**
* HelloWorld實體映射
*/
@Mapper(componentModel = "spring")
public static abstract class HelloWorldMapping {
public final static HelloWorldMapping Instance = Mappers.getMapper(HelloWorldMapping.class);
/**
* 實體轉化成DTO
*
* @param entity
* @return
*/
public abstract HelloWorldDTO toDTO(HelloWorld entity);
/**
* dto轉化成entity
*
* @param dto
* @return
*/
@Mappings({
@Mapping(target = "id", ignore = true),
@Mapping(target = "speakContent", ignore = true)
})
public abstract HelloWorld toEntity(HelloWorldDTO dto);
/**
* dto中的數據更新到entity
* @param entity
* @param dto
*/
@InheritInverseConfiguration
@Mappings({
@Mapping(target = "id", ignore = true),
@Mapping(target = "speakContent", ignore = true)
})
public abstract void updateEntityFromDTO(@MappingTarget HelloWorld entity, HelloWorldDTO dto);
}
@ApiOperation(value = "根據Id獲取HelloWorld詳情")
@GetMapping(value = "/helloWorld/{id}")
public ReturnDataDTO get(@ApiParam(name = "id", value = "主鍵ID") @PathVariable(name = "id") Integer id) {
// 根據主鍵ID查詢
HelloWorld helloWorld = helloWorldService.findById(id);
return ReturnDataDTO.success(HelloWorldMapping.Instance.toDTO(helloWorld));
}
@ApiOperation(value = "根據Id獲取HelloWorld詳情")
@GetMapping(value = "/helloWorld")
public ReturnDataDTO getById(@ApiParam(name = "id", value = "主鍵ID") @RequestParam(name = "id") Integer id) {
// 根據主鍵ID查詢
HelloWorld helloWorld = helloWorldService.findById(id);
return ReturnDataDTO.success(HelloWorldMapping.Instance.toDTO(helloWorld));
}
@ApiOperation(value = "新增HelloWorld")
@PostMapping(value = "/helloWorld")
public ReturnDataDTO add(@ApiParam(name = "helloWorldDTO", value = "新增HelloWorld對象") @RequestBody HelloWorldDTO helloWorldDTO) {
// vm轉化成實體
HelloWorld helloWorld = HelloWorldMapping.Instance.toEntity(helloWorldDTO);
// 新增數據
helloWorld = helloWorldService.add(helloWorld);
log.info("新增實體:{}", helloWorld);
String serverPort = environment.getProperty("server.port");
helloWorld.setSpeakContent(String.format("hello %s, port:%s", helloWorld.getName(), serverPort));
// 實體轉化成DTO
helloWorldDTO = HelloWorldMapping.Instance.toDTO(helloWorld);
return ReturnDataDTO.success(helloWorldDTO);
}
@ApiOperation(value = "更新HelloWorld")
@PutMapping(value = "/helloWorld/{id}")
public ReturnDataDTO update(@ApiParam(name = "id", value = "主鍵ID") @PathVariable(name = "id") Integer id,
@ApiParam(name = "helloWorldDTO", value = "更新HelloWorld對象") @RequestBody HelloWorldDTO helloWorldDTO) {
// 根據主鍵查詢
HelloWorld helloWorld = helloWorldService.findById(id);
if(null == helloWorld){
return ReturnDataDTO.fail(String.format("ID:%s不存在"));
}
// vm中的數據更新到entity
HelloWorldMapping.Instance.updateEntityFromDTO(helloWorld, helloWorldDTO);
// 更新數據
helloWorld = helloWorldService.update(helloWorld);
return ReturnDataDTO.success(HelloWorldMapping.Instance.toDTO(helloWorld));
}
@ApiOperation(value = "根據ID刪除HelloWorld")
@DeleteMapping(value = "/helloWorld")
public ReturnDataDTO delete(@ApiParam(name = "id", value = "主鍵ID") @RequestParam(name = "id") Integer id) {
// 根據主鍵ID刪除
HelloWorld helloWorld = helloWorldService.deleteById(id);
return ReturnDataDTO.success(HelloWorldMapping.Instance.toDTO(helloWorld));
}
}
HelloWorldService相關類從git中查看
啓動兩個服務提供者,端口號分別是8808和8809,啓動成功並訪問http://localhost:8761/
,會發現服務FEIGN-PROVIDER
已經註冊,並且有兩個服務實例,如下圖:
2.1.3 Eureka服務消費者
工程名:eureka-feign-consumer
引入Eureka服務消費者相關依賴
// 使用feign定義的api sdk
implementation 'cn.yjyzsl:feign-helloworld-starter-api:1.0.0-SNAPSHOT'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// 解析OpenAPI格式文檔
implementation 'io.swagger.parser.v3:swagger-parser:2.0.12'
// 生成OpenAPI文檔
implementation 'io.springfox:springfox-swagger2:2.9.2'
implementation 'io.springfox:springfox-swagger-ui:2.9.2'
// Getter/Setter生成
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Entity字段映射
implementation 'org.mapstruct:mapstruct:1.3.0.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.3.0.Final'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Eureka服務消費者application.yml配置
spring:
application:
name: feign-consumer
eureka:
client:
# 是否註冊到註冊中心
register-with-eureka: false
serviceUrl:
# 服務註冊中心的地址
defaultZone: http://localhost:8761/eureka/
創建SpringBoot啓動類Application,添加@SpringBootApplication、@EnableFeignClients、@EnableEurekaClient註解
@SpringBootApplication
@EnableFeignClients
@EnableEurekaClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
使用feign定義helloworld的Rest Api,添加註解@Component和@FeignClient,其中@FeignClient註解中要指定服務提供者的服務名,代碼如下:
@Component //注入到spring中,否則無法注入的
@FeignClient(value = "feign-provider") //value表示調用的服務名
public interface HelloWorldFeignService {
@GetMapping("/api/feign/helloWorld")
ReturnDataDTO<HelloWorldDTO> getByRequestParam(@RequestParam(name = "id") Integer id);
@GetMapping("/api/feign/helloWorld/{id}")
ReturnDataDTO<HelloWorldDTO> getByPathVariable(@PathVariable(name = "id") Integer id);
@PostMapping(value = "/api/feign/helloWorld")
ReturnDataDTO<HelloWorldDTO> add(@RequestBody HelloWorldDTO helloWorldDTO);
@PutMapping(value = "/api/feign/helloWorld/{id}")
ReturnDataDTO<HelloWorldDTO> update(@PathVariable(name = "id") Integer id, @RequestBody HelloWorldDTO helloWorldDTO);
@DeleteMapping(value = "/api/feign/helloWorld")
ReturnDataDTO<HelloWorldDTO> delete(@RequestParam(name = "id") Integer id);
}
服務消費者Controller中引用HelloWorldFeignService
實現服務的發現和消費,代碼如下:
@Api(value = "HelloWorld服務API", tags = {"hello world"}, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
@RestController
public class HelloWorldController {
@Autowired
private HelloWorldFeignService helloWorldFeignService;
/**
* HelloWorld實體映射
*/
@Mapper(componentModel = "spring")
public static abstract class HelloWorldMapping {
public final static HelloWorldMapping Instance = Mappers.getMapper(HelloWorldMapping.class);
/**
* 實體轉化成DTO
*
* @param entity
* @return
*/
public abstract HelloWorldDTO toDTO(HelloWorld entity);
/**
* vm轉化成entity
*
* @param vm
* @return
*/
@Mappings({
@Mapping(target = "id", ignore = true),
@Mapping(target = "speakContent", ignore = true)
})
public abstract HelloWorld toEntity(HelloWorldDTO vm);
/**
* vm中的數據更新到entity
* @param entity
* @param vm
*/
@InheritInverseConfiguration
@Mappings({
@Mapping(target = "id", ignore = true),
@Mapping(target = "speakContent", ignore = true)
})
public abstract void updateEntityFromVM(@MappingTarget HelloWorld entity, HelloWorldDTO vm);
}
@GetMapping(value = "/helloWorld/{id}")
public ReturnDataDTO getByPathVariable(@ApiParam(name = "id", value = "主鍵ID") @PathVariable(name = "id") Integer id) {
// 根據主鍵ID查詢
ReturnDataDTO ReturnDataDTO = helloWorldFeignService.getByPathVariable(id);
return ReturnDataDTO.success(ReturnDataDTO);
}
@GetMapping(value = "/helloWorld")
public ReturnDataDTO getByRequestParam(@ApiParam(name = "id", value = "主鍵ID") @RequestParam(name = "id") Integer id) {
// 根據主鍵ID查詢
ReturnDataDTO ReturnDataDTO = helloWorldFeignService.getByRequestParam(id);
return ReturnDataDTO.success(ReturnDataDTO);
}
@PostMapping(value = "/helloWorld")
public ReturnDataDTO add(@ApiParam(name = "HelloWorldDTO", value = "新增HelloWorld對象") @RequestBody HelloWorldDTO helloWorldDTO) {
ReturnDataDTO ReturnDataDTO = helloWorldFeignService.add(helloWorldDTO);
return ReturnDataDTO;
}
@ApiOperation(value = "更新HelloWorld")
@PutMapping(value = "/helloWorld/{id}")
public ReturnDataDTO update(@ApiParam(name = "id", value = "主鍵ID") @PathVariable(name = "id") Integer id,
@ApiParam(name = "HelloWorldDTO", value = "更新HelloWorld對象") @RequestBody HelloWorldDTO helloWorldDTO) {
ReturnDataDTO ReturnDataDTO = helloWorldFeignService.update(id, helloWorldDTO);
return ReturnDataDTO;
}
@ApiOperation(value = "根據ID刪除HelloWorld")
@DeleteMapping(value = "/helloWorld")
public ReturnDataDTO delete(@ApiParam(name = "id", value = "主鍵ID") @RequestParam(name = "id") Integer id) {
ReturnDataDTO ReturnDataDTO = helloWorldFeignService.delete(id);
return ReturnDataDTO;
}
}
啓動服務消費者,端口號爲9000,通過Postman工具進行接口測試,先調用兩次是新增接口,然後調用get接口,發現兩個實例輪詢的調用,在一個服務多實例的情況下Feign實現了負載均衡。
調用如下圖所示:
工程Git鏈接:springboot-feign