Feign將註冊中心從Eureka切換到Consul驗證(一)

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

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