Springcloud Openfegion Usage

2023-02-01
4 min read

本文档描述 Spring Boot 项目如何集成 Nacos 注册中心、使用 OpenFeign 实现服务间调用,以及如何编写共享 API 模块供其他服务接入。

角色说明

在微服务架构中,一个服务可以同时扮演两种角色:

  1. 服务提供方(Provider)

    • 将自身注册到 Nacos
    • 暴露 HTTP 接口供其他服务调用
    • 需要依赖: spring-cloud-starter-alibaba-nacos-discovery
  2. 服务调用方(Consumer)

    • 从 Nacos 发现其他服务
    • 通过 Feign 接口调用远程服务
    • 需要依赖: spring-cloud-starter-alibaba-nacos-discovery + spring-cloud-starter-openfeign + wvp-api

项目结构

以 wvp 项目为例,推荐的结构如下:

wvp-GB28181-pro/
├── pom.xml                   # 主项目 POM,即服务提供方
├── src/                      # 主项目源码,含 Controller 等
└── wvp-api/                  # 共享 API 模块(独立 jar)
    ├── pom.xml
    └── src/main/java/
        └── com/genersoft/iot/vmp/
            ├── api/feign/
            │   └── ChannelGroupFeignClient.java   # Feign 接口
            ├── storager/dao/dto/
            │   └── ChannelGroup.java              # 响应 DTO
            └── gb28181/bean/
                └── DeviceChannel.java             # 嵌套 DTO

一个标准的pom框架是:把qo、vo、dto对象单独放到一个依赖中,后端服务与Api的模块都依赖它

  1. 共享 API 模块

    • 独立的 Maven jar 模块,不包含业务逻辑
    • 仅包含 Feign 接口定义和 DTO 类
    • 依赖全部为 provided scope,由调用方提供实际实现
    • 服务提供方和调用方各自维护一份 DTO 副本(或通过 API 模块共享)

服务提供方集成

  1. pom.xml 依赖

    在主项目 pom.xml 中添加:

    <!-- 依赖管理 -->
    <properties>
        <spring-cloud.version>2021.0.5</spring-cloud.version>
    </properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <!-- 服务注册发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>2021.0.5.0</version>
    </dependency>
    
    <!-- OpenFeign(仅当服务提供方也需要调用其他服务时需要) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  2. 启动类注解

    @SpringBootApplication
    @EnableDiscoveryClient                // 注册到 Nacos
    @EnableFeignClients                    // 启用 Feign 客户端(调用其他服务时需要)
    public class VManageBootstrap {
        public static void main(String[] args) {
            SpringApplication.run(VManageBootstrap.class, args);
        }
    }
    
  3. application.yml 配置

    spring:
      application:
        name: wvp                    
      cloud:
        nacos:
          discovery:
            server-addr: 10.100.10.104:8848   
            namespace: public             
            group: DEFAULT_GROUP           
            username: username
            password: password
    
  4. Controller 接口

    服务提供方正常编写 Controller 即可,不需要额外改动:

    @RestController
    @RequestMapping("/api/channel/groups")
    public class GroupController {
    
        // 注意:wvp 返回有另外的封装,统一返回了 WVPResult
        @GetMapping("/getTreeDataForAi")
        public List<ChannelGroup> getTreeDataForAi(
                @RequestParam(required = true) String groupName,
                @RequestParam(required = false) String deviceId,
                @RequestParam(required = false) String channelName) {
            return groupChannelService.getTreeData(null, groupName, deviceId, channelName);
        }
    }
    
  5. 共享 API 模块编写

    1. pom.xml

      API 模块依赖全部为 provided scope:

      <?xml version="1.0"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" ...>
          <modelVersion>4.0.0</modelVersion>
      
          <groupId>com.genersoft</groupId>
          <artifactId>wvp-api</artifactId>
          <version>1.9.3</version>
          <packaging>jar</packaging>
      
          <properties>
              <maven.compiler.source>1.8</maven.compiler.source>
              <maven.compiler.target>1.8</maven.compiler.target>
              <spring-cloud.version>2021.0.5</spring-cloud.version>
              <lombok.version>1.18.22</lombok.version>
          </properties>
      
          <dependencyManagement>
              <dependencies>
                  <dependency>
                      <groupId>org.springframework.cloud</groupId>
                      <artifactId>spring-cloud-dependencies</artifactId>
                      <version>${spring-cloud.version}</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
              </dependencies>
          </dependencyManagement>
      
          <dependencies>
              <!-- @FeignClient, @GetMapping 等注解 -->
              <dependency>
                  <groupId>org.springframework.cloud</groupId>
                  <artifactId>spring-cloud-starter-openfeign</artifactId>
                  <scope>provided</scope>
              </dependency>
              <!-- @Data 注解 -->
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
                  <version>${lombok.version}</version>
                  <scope>provided</scope>
              </dependency>
          </dependencies>
      </project>
      

      关键说明:

      • provided scope:依赖仅用于编译期,运行时由调用方提供,不会被打包到 API jar 中
      • 版本由 spring-cloud-dependencies BOM 管理,确保 Feign 版本与调用方一致
    2. Feign 接口定义

      package com.genersoft.iot.vmp.api.feign;
      
      import com.genersoft.iot.vmp.storager.dao.dto.ChannelGroup;
      import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestParam;
      
      import java.util.List;
      
      @FeignClient(name = "wvp", path = "/api/channel/groups", fallback = ChannelGroupFeignClientFallback.class)
      public interface ChannelGroupFeignClient {
      
          @GetMapping("/getTreeDataForAi")
          WVPResult<List<ChannelGroup>> getTreeDataForAi(
                  @RequestParam("groupName") String groupName,
                  @RequestParam(value = "deviceId", required = false) String deviceId,
                  @RequestParam(value = "channelName", required = false) String channelName);
      }
      

      参数说明:

      • name :被调用方注册到 Nacos 的服务名( spring.application.name
      • path :Controller 上的 @RequestMapping 前缀
      • 方法签名必须与 Controller 接口完全一致(路径、参数名、参数类型)
  6. 编写 Fallback 实现类

    在 API 模块中(或调用方项目中)创建 Fallback 类,实现 Feign 接口并返回兜底数据。以下情况会触发降级,走 Fallback 逻辑:

    1. *服务不可用*:被调用方宕机或未注册到 Nacos

    2. *响应超时*:Feign 调用超过设定的超时时间

      package com.genersoft.iot.vmp.api.feign;

      import com.genersoft.iot.vmp.storager.dao.dto.ChannelGroup; import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; import com.genersoft.iot.vmp.vmanager.bean.WVPResult; import org.springframework.stereotype.Component;

      import java.util.List;

      @Component public class ChannelGroupFeignClientFallback implements ChannelGroupFeignClient {

      @Override
      public WVPResult<List<ChannelGroup>> getTreeDataForAi(String groupName,
                                                            String deviceId,
                                                            String channelName) {
          return WVPResult.fail(ErrorCode.ERROR500.getCode(),
                                "channel group 服务暂不可用,请稍后重试");
      }
      

      }

    • Fallback 类必须实现 Feign 接口,且需标注 @Component
    • 降级逻辑应尽量简单,避免依赖外部服务
    • 返回统一错误码和提示信息,便于前端统一处理
  7. Feign 超时与重试配置

    每个 FeignClient 可以通过 configuration 参数指定独立的配置类,实现超时时间、重试策略等自定义。

    1. 配置类

      在 API 模块中创建 FeignClientConfig

      package com.genersoft.iot.vmp.api.feign;
      
      import feign.Request;
      import feign.Retryer;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      import java.util.concurrent.TimeUnit;
      
      @Configuration
      public class FeignClientConfig {
      
          @Value("${feign.client.wvp.connect-timeout:2000}")
          private int connectTimeout;
      
          @Value("${feign.client.wvp.read-timeout:3000}")
          private int readTimeout;
      
          @Value("${feign.client.wvp.retryer.period:500}")
          private long retryPeriod;
      
          @Value("${feign.client.wvp.retryer.max-period:2000}")
          private long retryMaxPeriod;
      
          @Value("${feign.client.wvp.retryer.max-attempts:3}")
          private int retryMaxAttempts;
      
          @Bean
          public Request.Options requestOptions() {
              return new Request.Options(connectTimeout, TimeUnit.MILLISECONDS,
                                         readTimeout, TimeUnit.MILLISECONDS, true);
          }
      
          @Bean
          public Retryer retryer() {
              return new Retryer.Default(retryPeriod, retryMaxPeriod, retryMaxAttempts);
          }
      }
      
    2. 在 FeignClient 中引用

      @FeignClient(name = "wvp", path = "/api/channel/groups",
                   configuration = FeignClientConfig.class,
                   fallback = ChannelGroupFeignClientFallback.class)
      public interface ChannelGroupFeignClient {
          // ...
      }
      
    3. YAML 覆盖示例

      feign:
        client:
          wvp:
            connect-timeout: 5000      # 连接超时 5s
            read-timeout: 10000        # 读取超时 10s
            retryer:
              period: 1000             # 初始重试间隔 1s
              max-period: 5000         # 最大重试间隔 5s
              max-attempts: 5          # 最多重试 5 次
      
  8. 发布 API 模块到远程仓库

    # 在 wvp 项目根目录执行
    cd wvp-api
    mvn deploy 
    

服务调用方集成

  1. 步骤一:调用方 pom.xml 添加依赖

    <!-- 服务注册发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>2021.0.5.0</version>
    </dependency>
    
    <!-- OpenFeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!-- 共享 API 模块 -->
    <dependency>
        <groupId>com.genersoft</groupId>
        <artifactId>wvp-api</artifactId>
        <version>1.9.3</version>
    </dependency>
    
  2. 步骤二:增加登录信息传递

    服务之间通过 fegionclient 调用,浏览器请求的 cookie 和 header 并不会传递过去,所幸可以使用 RequestInterceptor 来实现。
    RequestInterceptor 是 Feign 原生提供的一个函数式接口,用于在 HTTP 请求发出前对请求模板进行定制修改。 在 Spring Cloud 环境中,你可以通过将它声明为 Bean 来全局生效,或放在 @FeignClient 的 configuration 中局部生效。

    package com.skytech.infrastructure.config;
    
    import feign.RequestInterceptor;
    import feign.RequestTemplate;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import java.util.Enumeration;
    
    import javax.servlet.http.HttpServletRequest;
    
    @Configuration
    public class FeignHeaderInterceptor implements RequestInterceptor {
    
        @Override
        public void apply(RequestTemplate template) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                Enumeration<String> headerNames = request.getHeaderNames();
                while (headerNames.hasMoreElements()) {
                    String headerName = headerNames.nextElement();
                    String headerValue = request.getHeader(headerName);
                    template.header(headerName, headerValue);
                }
            }
        }
    }
    
  3. 步骤三:调用方 Nacos 配置

    spring:
      application:
        name: your-service-name
      cloud:
        nacos:
          discovery:
            server-addr: 10.100.10.104:8848
            namespace: public
            group: DEFAULT_GROUP
            username: username
            password: password
    
  4. 步骤四:启动类添加注解

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients(basePackages = "com.genersoft.iot.vmp.api.feign")
    public class YourApplication {
        public static void main(String[] args) {
            SpringApplication.run(YourApplication.class, args);
        }
    }
    
  5. 步骤五:注入并使用

    @Service
    public class YourService {
    
        @Autowired
        private ChannelGroupFeignClient channelGroupFeignClient;
    
        public void queryGroupTree() {
            WVPResult<List<ChannelGroup>> groups = channelGroupFeignClient
                    .getTreeDataForAi("分组名称", null, null);
            // 处理返回数据
        }
    }