SpringCloud Alibaba微服务实战三十六 - 使用Feign的一些问题以及如何解决?

简介: SpringCloud Alibaba微服务实战三十六 - 使用Feign的一些问题以及如何解决?

大家好,我是飘渺。

在SpringCloud架构体系中,微服务间的通信是基于Feign调用。而在实际使用Feign的过程中我们大概率会面临下面几个问题:

  • Feign客户端放在消费端还是独立一个api层?
  • Feign调用的接口如何要不要进行包装?
  • Feign如何抓取业务生产端的业务异常?

这篇文章我们就来一起探讨一下这几个问题,希望看完能对你有所帮助。

首先我们先看看Feign的调用方式如何抉择?


Feign的调用方式如何选择?


总体来说,Feign的调用方式分为两大类:


在生产端API中声明Feign客户端

如上,消费端服务直接依赖生产端提供的API包,然后通过@Autowired注解注入,就可以直接调用生产者提供的接口。

这样做的 好处 是:简单方便,消费端直接使用生产者提供的Feign接口即可。

这样做的 坏处 也很明显:消费端获取到的接口是生产者提供给所有服务的接口列表,当某一生产者接口很多时就会很混乱;而且熔断降级类也在生产端,消费端在调用时由于包路径可能与生产者不一样,必须要通过@SpringBootApplication(scanBasePackages = {"com.javadaily.feign"})扫描Feign的路径,当消费端需要引入很多生产者Feign时那就需要扫描很多个接口路径。

此调用方式在之前两篇文章中有详细说明,感兴趣的可以通过下方链接直达:

SpringCloud Alibaba微服务实战三 - 服务调用

SpringCloud Alibaba微服务实战二十 - 集成Feign的降级熔断

在消费端声明Feign客户端

还是需要独立一个公共的API接口层,生产端消费端都需要引入此jar包,同时在消费端按需编写Feign客户端及熔断类。

这样做的 好处 是:客户端可以按需编写自己需要的接口,熔断降级都由消费者控制;不需要在启动类上加入额外的扫描注解scanBasePackages

这样做的 坏处 是:消费端代码冗余,每个消费者都需要编写Feign客户端;服务间耦合比较紧,修改一处接口三处都要修改。

小结

那么问题来了:既然有两种调用方式,那那种才更合理呢?

我这里建议的是优先使用第二种方式,由客户端自己定制Feign客户端。

从职责来说只有消费端才能明确知道自己要调用哪个服务提供方,需要调用哪些接口。如果直接把@FeignClient写在服务提供方的API上,消费端就很难按需定制,而熔断处理逻辑也应该是由消费端自己定制熔断逻辑。虽然会导致代码冗余,但是职责很清晰,而且可以避免扫描不到接口路径的问题。

当然这里只是个人建议,如果你觉得我说的不对,你可以按照你自己的想法来。

接下来我们再来看看Feign接口要不要封装的问题。


Feign接口要不要包装?


现状分析

在前后端分离项目中,后端给前端返回接口数据时一般会统一返回格式,此时我们的Controller大概会这样写:

@RestController
@Log4j2
@Api(tags = "account接口")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AccountController implements AccountApi {
    private final AccountService accountService;
   ...
  public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){   
        AccountDTO accountDTO = accountService.selectByCode(accountCode);
        return ResultData.success(accountDTO);
    }
    ...
}

而Feign的接口定义需要跟实现类保持一致,那么现在order-service获取订单详情时也要返回用户信息,此时我们通过feign调用account-service的getByCode()接口会这样写:

/**
 * 获取Order详情
 * @param orderNo order编号
 * @return ResultData<OrderDTO>
*/
@GetMapping("/order/{orderNo}")
public ResultData<OrderDTO> getById(@PathVariable("orderNo") String orderNo){
  OrderDTO orderDTO = orderService.selectByNo(orderNo);
  return ResultData.success(orderDTO);
}
public OrderDTO selectByNo(String orderNo) {
    OrderDTO orderDTO = new OrderDTO();
    //1. 查询订单基础信息
    Order order = orderMapper.selectByNo(orderNo);
    BeanUtils.copyProperties(order,orderDTO);
    //2. 获取用户信息
    ResultData<AccountDTO> accountResult = accountClient.getByCode("javadaily");
   if(accountResult.isSuccess()){
     orderDTO.setAccountDTO(accountResult.getData());
   }
    return orderDTO;
}

这里要先获取到ResultData包装类,再通过判断返回结果解成具体的AccountDTO对象,很明显这段代码有两个问题:

  1. 每个Controller接口都需要手动使用ResultData.success对结果进行包装,Repeat Yourself !
  2. Feign调用时又需要从包装类解装成需要的实体对象,Repeat Yourself !

如果有很多这样的接口调用,那...


优化包装

这样丑陋的代码我们当然需要进行优化,优化的目标也很明确:当我们通过Feign调用时,直接获取到实体对象,不需要额外的解装。而前端通过网关直接调用时,返回统一的包装体。

这里我们可以借助ResponseBodyAdvice来实现,通过对Controller返回体进行增强,如果识别到是Feign的调用就直接返回对象,否则给我们加上统一包装结构。

至于为什么前后端需要统一返回格式以及如何实现,在我的老鸟系列文章 SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的! 有详细讲述,感兴趣的可以移步。

现在问题是:如何识别出是Feign的调用还是网关直接调用呢?

这又有两种方法,基于自定义注解实现和基于Feign拦截器实现。

基于自定义注解实现

自定义一个注解,比如Inner,给Feign的接口标注上此注解,这样在使用ResponseBodyAdvice匹配时可以通过此注解进行匹配。

不过这种方法有个弊端,就是前端和feign没法公用,如一个接口user/get/{id}既可以通过feign调用也可以通过网关直接调用,采用这种方法就需要写2个不同路径的接口。

基于Feign拦截器实现

对于Feign的调用,在Feign拦截器上加上特殊标识,在转换对象时如果发现是feign调用就直接返回对象。


具体实现过程

这里我们使用第二种方法来实现(第一种方法也很简单,大家可自行尝试)

  1. 在feign拦截器中给feign请求添加特定请求头T_REQUEST_ID
/**
 * 给Feign设置请求头
 */
@Bean
public RequestInterceptor requestInterceptor(){
  return requestTemplate -> {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if(null != attributes){
      HttpServletRequest request = attributes.getRequest();
      Map<String, String> headers = getRequestHeaders(request);
      // 传递所有请求头,防止部分丢失
      //此处也可以只传递认证的header
      //requestTemplate.header("Authorization", request.getHeader("Authorization"));
      for (Map.Entry<String, String> entry : headers.entrySet()) {
        requestTemplate.header(entry.getKey(), entry.getValue());
      }
      // 微服务之间传递的唯一标识,区分大小写所以通过httpServletRequest获取
      if (request.getHeader(T_REQUEST_ID)==null) {
        String sid = String.valueOf(UUID.randomUUID());
        requestTemplate.header(T_REQUEST_ID, sid);
      }
    }
  };
}


  1. 自定义BaseResponseAdvice并实现ResponseBodyAdvice
@RestControllerAdvice(basePackages = "com.javadaily")
@Slf4j
public class BaseResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //Feign请求时通过拦截器设置请求头,如果是Feign请求则直接返回实体对象
        boolean isFeign = serverHttpRequest.getHeaders().containsKey(OpenFeignConfig.T_REQUEST_ID);
        if(isFeign){
            return object;
        }
        if(object instanceof String){
            return objectMapper.writeValueAsString(ResultData.success(object));
        }
        if(object instanceof ResultData){
            return object;
        }
        return ResultData.success(object);
    }
}

如果为Feign请求,则不做转换,否则通过ResultData进行包装。

  1. 修改后端接口返回对象
@ApiOperation("select接口")
@GetMapping("/account/getByCode/{accountCode}")
@ResponseBody
public AccountDTO getByCode(@PathVariable(value = "accountCode") String accountCode){
  return accountService.selectByCode(accountCode);
}

不需要在接口上返回封装体ResultData,经由ResponseBodyAdvice实现自动增强。

  1. 修改feign调用逻辑
@Override
    public OrderDTO selectByNo(String orderNo) {
        OrderDTO orderDTO = new OrderDTO();
        //1. 查询订单基础信息
        Order order = orderMapper.selectByNo(orderNo);
        BeanUtils.copyProperties(order,orderDTO);
    //2. 远程获取用户信息
        AccountDTO accountResult = accountClient.getByCode(order.getAccountCode());
        orderDTO.setAccountDTO(accountResult);
        return orderDTO;
    }

经过上面四个步骤,在正常情况下 达到了我们优化目标,通过Feign调用直接返回实体对象,通过网关调用返回统一包装体。看上去很完美,但是实际很糟糕,这又导致了第三个问题,Feign如何处理异常?


Feign异常处理


现状分析

生产者对于提供的接口方法会进行业务规则校验,对于不符合业务规则的调用请求会抛出业务异常BizException,而正常情况下项目上会有个全局异常处理器,他会捕获业务异常BizException,并将其封装成统一包装体返回给调用方,现在让我们来模拟这种业务场景:

  1. 生产者抛出业务异常
public AccountDTO selectByCode(String accountCode) {
  if("javadaily".equals(accountCode)){
    throw new BizException(accountCode + "用户不存在");
  }
  AccountDTO accountDTO = new AccountDTO();
  Account account = accountMapper.selectByCode(accountCode);
  BeanUtils.copyProperties(account,accountDTO);
  return accountDTO;
}

当用户名为 “javadaily” 的时候直接抛出业务异常BizException

  1. 全局异常拦截器捕获业务异常
/**
 * 自定义业务异常处理
 * @param e the e
 * @return ResultData
 */
@ExceptionHandler(BaseException.class)
public ResultData<String> exception(BaseException e) {
  log.error("业务异常 ex={}", e.getMessage(), e);
  return ResultData.fail(e.getErrorCode(),e.getMessage());
}

捕获BaseException,BizException属于BaseException的子类,同样会被捕获。

  1. 调用方直接模拟异常数据调用
public OrderDTO selectByNo(String orderNo) {
  OrderDTO orderDTO = new OrderDTO();
  //1. 查询订单基础信息
  Order order = orderMapper.selectByNo(orderNo);
  BeanUtils.copyProperties(order,orderDTO);
  //2. 远程获取用户信息
  AccountDTO accountResult = accountClient.getByCode("javadaily");
  orderDTO.setAccountDTO(accountResult);
  return orderDTO;
}

调用getByCode()方法时传递 “javadaily” ,触发生产者的业务异常规则。


Feign捕获不到异常

此时我们调用selectByNo()方法,会发现调用方捕获不到异常,accountDTO全部被设置成为null,如下:

将Feign的日志级别设置为FULL查看返回结果:

通过日志可以看到Feign其实获取到了全局异常处理器转换后的统一对象ResultData,并且响应码为200,正常响应。而消费者接受对象为AccountDTO,属性无法转换,全部当作NULL值处理。

很显然,这不符合我们正常业务逻辑,我们应该要直接返回生产者抛出的异常,那如何处理呢?

很简单,我们只需要给全局异常拦截器中业务异常设置一个非200的响应码即可,如:

/**
 * 自定义业务异常处理。
 * @param e the e
 * @return ResultData
 */
@ExceptionHandler(BaseException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(BaseException e) {
  log.error("业务异常 ex={}", e.getMessage(), e);
  return ResultData.fail(e.getErrorCode(),e.getMessage());
}

这样消费者就可以正常捕获到生产者抛出的业务异常,如下图所示:

异常被额外封装

虽然能获取到异常,但是Feign捕获到异常后又在业务异常的基础上再进行了一次封装。

原因是当feign调用结果为非200的响应码时就触发了Feign的异常解析,Feign的异常解析器会将其包装成FeignException,即在我们业务异常的基础上再包装一次

可以在feign.codec.ErrorDecoder#decode()方法上打上断点观察执行结果,如下:

很显然,这个包装后的异常我们并不需要,我们应该直接将捕获到的生产者的业务异常直接抛给前端,那这又该如何解决呢?

很简单,我们只需要重写Feign的异常解析器,重新实现decode逻辑,返回正常的BizException即可,而后全局异常拦截器又会捕获BizException!(感觉有点无限套娃的感觉)

代码如下:

  1. 重写Feign异常解析器
/**
 * 解决Feign的异常包装,统一返回结果
 * @author 公众号:JAVA日知录
 */
@Slf4j
public class OpenFeignErrorDecoder implements ErrorDecoder {
    /**
     * Feign异常解析
     * @param methodKey 方法名
     * @param response 响应体
     * @return BizException
     */
    @Override
    public Exception decode(String methodKey, Response response) {
        log.error("feign client error,response is {}:",response);
        try {
            //获取数据
            String errorContent = IOUtils.toString(response.body().asInputStream());
            String body = Util.toString(response.body().asReader(Charset.defaultCharset()));
            ResultData<?> resultData = JSON.parseObject(body,ResultData.class);
            if(!resultData.isSuccess()){
                return new BizException(resultData.getStatus(),resultData.getMessage());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new BizException("Feign client 调用异常");
    }
}


  1. 在Feign配置文件中注入自定义的异常解码器
@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig {  
  /**
     * 自定义异常解码器
     * @return OpenFeignErrorDecoder
     */
    @Bean
    public ErrorDecoder errorDecoder(){
        return new OpenFeignErrorDecoder();
    }
}


  1. 再次调用,符合预期。

此时通过自定义Feign的异常解码器,直接抛出生产者的业务异常信息,完成目标。


总结


本文对Feign在使用过程中会遇到的问题做了个小小的总结,也提出了自己可能不太成熟的解决方案。当然,由于本人水平有限,提出的解决方案不一定是最好的,如果你有更好的解决方案,还请留言告诉我,谢谢!

目录
相关文章
|
6月前
|
NoSQL MongoDB 微服务
微服务——MongoDB实战演练——文章评论的基本增删改查
本节介绍了文章评论的基本增删改查功能实现。首先,在`cn.itcast.article.dao`包下创建数据访问接口`CommentRepository`,继承`MongoRepository`以支持MongoDB操作。接着,在`cn.itcast.article.service`包下创建业务逻辑类`CommentService`,通过注入`CommentRepository`实现保存、更新、删除及查询评论的功能。最后,新建Junit测试类`CommentServiceTest`,对保存和查询功能进行测试,并展示测试结果截图,验证功能的正确性。
107 2
|
6月前
|
NoSQL Java MongoDB
微服务——MongoDB实战演练——文章评论实体类的编写
本节主要介绍文章评论实体类的编写,创建了包`cn.itcast.article.po`用于存放实体类。具体实现中,`Comment`类通过`@Document`注解映射到MongoDB的`comment`集合,包含主键、内容、发布时间、用户ID、昵称等属性,并通过`@Indexed`和`@CompoundIndex`注解添加单字段及复合索引,以提升查询效率。同时提供了Mongo命令示例,便于理解和操作。
102 2
|
6月前
|
NoSQL 测试技术 MongoDB
微服务——MongoDB实战演练——MongoTemplate实现评论点赞
本节介绍如何使用MongoTemplate实现评论点赞功能。传统方法通过查询整个文档并更新所有字段,效率较低。为优化性能,采用MongoTemplate对特定字段直接操作。代码中展示了如何利用`Query`和`Update`对象构建更新逻辑,通过`update.inc(&quot;likenum&quot;)`实现点赞数递增。测试用例验证了功能的正确性,确保点赞数成功加1。
113 0
|
6月前
|
NoSQL 测试技术 MongoDB
微服务——MongoDB实战演练——根据上级ID查询文章评论的分页列表
本节介绍如何根据上级ID查询文章评论的分页列表,主要包括以下内容:(1)在CommentRepository中新增`findByParentid`方法,用于按父ID查询子评论分页列表;(2)在CommentService中新增`findCommentListPageByParentid`方法,封装分页逻辑;(3)提供JUnit测试用例,验证功能正确性;(4)使用Compass插入测试数据并执行测试,展示查询结果。通过这些步骤,实现对评论的高效分页查询。
92 0
|
6月前
|
NoSQL MongoDB 微服务
微服务——MongoDB实战演练——文章微服务模块搭建
本节介绍文章微服务模块的搭建过程,主要包括以下步骤:(1)创建项目工程 *article*,并在 *pom.xml* 中引入依赖;(2)配置 *application.yml* 文件;(3)创建启动类 *cn.itcast.article.ArticleApplication*;(4)启动项目,确保控制台无错误提示。通过以上步骤,完成文章微服务模块的基础构建与验证。
76 0
|
3月前
|
NoSQL Java 微服务
2025 年最新 Java 面试从基础到微服务实战指南全解析
《Java面试实战指南:高并发与微服务架构解析》 本文针对Java开发者提供2025版面试技术要点,涵盖高并发电商系统设计、微服务架构实现及性能优化方案。核心内容包括:1)基于Spring Cloud和云原生技术的系统架构设计;2)JWT认证、Seata分布式事务等核心模块代码实现;3)数据库查询优化与高并发处理方案,响应时间从500ms优化至80ms;4)微服务调用可靠性保障方案。文章通过实战案例展现Java最新技术栈(Java 17/Spring Boot 3.2)的应用.
207 9
|
3月前
|
缓存 负载均衡 监控
微服务架构下的电商API接口设计:策略、方法与实战案例
本文探讨了微服务架构下的电商API接口设计,旨在打造高效、灵活与可扩展的电商系统。通过服务拆分(如商品、订单、支付等模块)和标准化设计(RESTful或GraphQL风格),确保接口一致性与易用性。同时,采用缓存策略、负载均衡及限流技术优化性能,并借助Prometheus等工具实现监控与日志管理。微服务架构的优势在于支持敏捷开发、高并发处理和独立部署,满足电商业务快速迭代需求。未来,电商API设计将向智能化与安全化方向发展。
|
5月前
|
负载均衡 前端开发 Java
SpringCloud调用组件Feign
本文深入探讨微服务Spring体系中的Feign组件。Feign是一个声明式Web服务客户端,支持注解、编码器/解码器,与Spring MVC注解兼容,并集成Eureka、负载均衡等功能。文章详细介绍了SpringCloud整合Feign的步骤,包括依赖引入、客户端启用、接口创建及调用示例。同时,还涵盖了Feign的核心配置,如超时设置、拦截器实现(Basic认证与自定义)和日志级别调整。最后,总结了`@FeignClient`常用属性,帮助开发者更好地理解和使用Feign进行微服务间通信。
477 1
|
6月前
|
负载均衡 Dubbo Java
Spring Cloud Alibaba与Spring Cloud区别和联系?
Spring Cloud Alibaba与Spring Cloud区别和联系?
|
7月前
|
人工智能 SpringCloudAlibaba 自然语言处理
SpringCloud Alibaba AI整合DeepSeek落地AI项目实战
在现代软件开发领域,微服务架构因其灵活性、可扩展性和模块化特性而受到广泛欢迎。微服务架构通过将大型应用程序拆分为多个小型、独立的服务,每个服务运行在其独立的进程中,服务与服务间通过轻量级通信机制(通常是HTTP API)进行通信。这种架构模式有助于提升系统的可维护性、可扩展性和开发效率。
2258 2