OpenFeign响应统一解析:从响应(统一封装类型Result)中解析出核心数据(data字段)作为FeignClient接口返回值:自定义解码器(Decoder)

一、场景

SpringBoot项目中,使用 OpenFeign 调用远程服务的接口。接口返回的响应数据,是封装到数据类型(Result.java)中的,Result 对象中的data对象的数据,才是后续业务逻辑需要的核心数据。

在默认情况下,FeignClient 接口的返回类型应使用Result<T>,比如 Result<UserVo>

当有大量 FeignClient 接口都使用 Result 来接收对象的时候,会产生一系列问题:

  • 首先,FeignClient 接口定义的代码冗余,写了大量的 Result 来包装核心响应数据。
  • 其次,FeignClient 接口返回值处理逻辑,冗余更多,需要在每一处调用 FeignClient 接口的地方,解析 Result 状态码,判断接口请求是否成功(状态码是否为成功)。如果请求成功,则取出data字段的值;如果请求失败,则做业务失败逻辑处理(比如抛出业务异常)。

二、需求:FeignClient 接口响应,统一解析

此场景的核心需求:FeignClient 接口响应,统一解析。

详细需求包含以下三点:

  1. FeignClient 接口, 返回值类型 为核心数据类型(Result.data对象的类型),而不是使用统一封装类型Result
  2. 统一处理请求失败的情况,抛出业务异常,交给异常处理器统一处理。
  3. FeignClient 接口返回值为void时,使用解码器,解析Result,处理请求失败。默认情况,返回值为void(比如新增、修改、删除接口返回值,没有data数据),不会进入解码逻辑,业务始终识别为调用接口成功。

三、解决方案

自定义解码器 + Feign.builder().decodeVoid()

  1. FeignClient配置自定义解码器(Decoder),该解码器能够解析响应数据,转为Result对象,并从中提取data数据,作为接口的返回值。

  2. 对于 FeignClient 返回值为void,不会进入解码逻辑,无法正确处理业务请求失败的情况。
    解决方案:FeignClient 配置自定义Feign.Builder,调用decodeVoid()方法修改配置,当返回值为 void 时,仍然使用解码器。

本文的框架版本:

  • spring-boot-starter-parent-3.3.3
  • spring-cloud-starter-openfeign-4.1.3

四、核心代码

1. 创建自定义解码器(Decoder)

package com.example.hello_feign_client.feign.config;

import com.example.hello_common.exception.BusinessException;
import com.example.hello_common.response.Result;
import com.example.hello_common.response.ResultEnum;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.Util;
import feign.codec.Decoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.lang.reflect.Type;

@Slf4j
@RequiredArgsConstructor
public class ResultFeignDecoder implements Decoder {

    private final ObjectMapper objectMapper;

    @Override
    public Object decode(Response response, Type type) throws IOException {
        log.info("调用自定义Feign解码器");

        // 读取响应中数据,将响应体中数据转为字符串
        String bodyString = Util.toString(response.body().asReader(Util.UTF_8));

        // 解析响应体字符串到 Result 对象
        JavaType parameterType = objectMapper.getTypeFactory().constructType(type);
        JavaType resultType = objectMapper.getTypeFactory().constructParametricType(Result.class, parameterType);
        Result<?> result = objectMapper.readValue(bodyString, resultType);

        // 如果响应状态为失败,抛出业务异常
        if (ResultEnum.FAIL.getCode().equals(result.getCode())) {
            throw new BusinessException(result.getMessage());
        }

        // 从 Result 中提取 data 字段
        return result.getData();
    }

}

注意事项:确保 Result 类有一个无参构造函数,并且可以通过反射访问其 data 字段。

2. 创建自定义Feign.Builder

    @Bean
    public Feign.Builder decodeVoidFeignBuilder() {
        return Feign.builder().decodeVoid();
    }

创建自定义Feign.Builder,配置decodeVoid(),此配置的含义是当FeignClient接口返回值类型为void时,仍然使用解码器。

3. 创建Feign配置类

package com.example.hello_feign_client.feign.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Feign;
import org.springframework.context.annotation.Bean;

public class ResultFeignConfig {

    @Bean
    public ResultFeignDecoder resultFeignDecoder(ObjectMapper objectMapper) {
        return new ResultFeignDecoder(objectMapper);
    }

    @Bean
    public Feign.Builder decodeVoidFeignBuilder() {
        return Feign.builder().decodeVoid();
    }

}

注意:ResultFeignConfig 上不要添加 @Configuration 注解。

ResultFeignConfig 添加 @Configuration 注解,则解码器和Builder会全局生效,也就是对所有的FeignClient生效;如果不添加 @Configuration 注解,则仅对指定的FeignClient生效。

本文需要的效果是,解码器和Builder,仅对指定FeignClient生效。

4. 配置 FeignClient

package com.example.hello_feign_client.feign.client;

import com.example.hello_common.model.param.UserParam;
import com.example.hello_common.model.vo.UserVo;
import com.example.hello_feign_client.feign.config.ResultFeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@FeignClient(contextId = "userFeignClient",
        name = "hello-feign-server",
        url = "http://localhost:9001",
        path = "/users",
        configuration = ResultFeignConfig.class)
public interface UserFeignClient {

    @PostMapping
    void addUser(@RequestBody UserParam param);

    @DeleteMapping("/{id}")
    void deleteUser(@PathVariable String id);

    @PutMapping("/{id}")
    void editUser(@PathVariable String id, @RequestBody UserParam param);

    @GetMapping("/{id}")
    UserVo getUserById(@PathVariable String id);

    @GetMapping
    List<UserVo> listUsers();

}

@FeignClient 中配置 configuration = ResultFeignConfig.class

这样配置之后,当调用 Feign 客户端时,如果返回类型为 Result<T>,则会自动提取 T 类型的数据并返回。

五、代码分析:Feign解码器 - 反序列化

1. 错误代码

请注意:反序列化为 Result 对象时,需要使用指定泛型参数的类型;直接使用Result.class类型是无法正确解析的。

下面的代码,无法将响应正确解析Result对象。

Result<?> result = objectMapper.readValue(bodyString, Result.class); // 错误代码示例

2. 代码分析:JSON 字符串反序列化为 Result 对象

 // 解析响应体字符串到 Result 对象
 JavaType parameterType = objectMapper.getTypeFactory().constructType(type);
 JavaType resultType = objectMapper.getTypeFactory().constructParametricType(Result.class, parameterType);
 Result<?> result = objectMapper.readValue(bodyString, resultType);

这段代码的主要目的是构建正确的类型信息,并使用 Jackson 的 ObjectMapper 将 JSON 字符串反序列化为 Result 对象。下面逐行解释这段代码:

  1. 构建参数类型:

    JavaType parameterType = objectMapper.getTypeFactory().constructType(type);
    
    • objectMapper.getTypeFactory(): 获取 ObjectMapper 内置的 TypeFactory 实例。
    • constructType(type): 使用给定的 Type 参数构造一个 JavaType 实例。这里的 type 是解码的目标类型,通常是调用 Feign 接口的方法所指定的返回类型。
  2. 构建泛型类型:

    JavaType resultType = objectMapper.getTypeFactory().constructParametricType(Result.class, parameterType);
    
    • constructParametricType(Class<T> rawClass, Class<?>... parameterClasses): 创建一个具有指定泛型参数的类型。
    • Result.class: 表示 Result 类本身。
    • parameterType: 上一步中构造的参数类型,作为 Result 泛型参数。

    综合这两步,这段代码创建了一个 JavaType,表示 Result<参数类型> 的形式。例如,如果参数类型是 List<String>,那么最终构造的类型将是 Result<List<String>>

  3. 反序列化 JSON 字符串:

    Result<?> result = objectMapper.readValue(bodyString, resultType);
    
    • readValue(String content, JavaType valueType): 使用给定的 JavaType 将 JSON 字符串反序列化为相应的 Java 对象。
    • bodyString: 从 Feign Response 读取到的 JSON 字符串。
    • resultType: 第二步中创建的 JavaType,即 Result<参数类型>

总结:这段代码的关键在于构建正确的 JavaType 以确保反序列化操作正确无误。它首先创建了一个表示参数类型的 JavaType,然后基于这个类型创建了一个表示 Result<参数类型>JavaType。最后,使用这个 JavaType 将 JSON 字符串反序列化为 Result 对象。这样,无论参数类型是什么,都可以正确地解析出 Result 中的数据部分。

六、数据模型

Result(响应封装实体)

package com.example.hello_common.response;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 响应封装实体
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {

    private Integer code;

    private String message;

    private T data;

    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }

    public static <T> Result<T> fail(String message) {
        return new Result<>(ResultEnum.FAIL.getCode(), message, null);
    }

}

ResultEnum(响应状态码)

package com.example.hello_common.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 响应状态码
 */
@Getter
@AllArgsConstructor
public enum ResultEnum {

    SUCCESS(1, "操作成功"),
    FAIL(0, "操作失败");

    private final Integer code;
    private final String message;

}

BusinessException(业务异常)

package com.example.hello_common.exception;

/**
 * 业务异常
 */
public class BusinessException extends RuntimeException {

    public BusinessException(String message) {
        super(message);
    }

}

七、Result无参构造器

应确保 Result 类有一个无参构造函数,并且可以通过反射访问其 data 字段。

Result 类没有无参构造函数时,会出现如下错误:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.example.hello_common.response.Result (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

AI解析:

这个异常 com.fasterxml.jackson.databind.exc.InvalidDefinitionException 表明 Jackson 库在尝试将 JSON 数据反序列化为 Java 对象 (com.example.hello_common.response.Result) 时遇到了问题。具体来说,错误消息指出没有找到可以用来构造 Result 类实例的构造器(Creators)。

这个异常通常有以下几种原因:

  1. 缺少默认构造器:如果你的 Result 类定义了私有的默认构造器或者根本没有提供默认构造器(即一个无参的公共构造器),那么 Jackson 将无法创建这个类的新实例。

  2. 缺少必需的属性:如果 Result 类需要一些必须的字段来构造,并且这些字段没有对应的 setter 方法或者构造函数,Jackson 也可能无法正确地创建对象。

  3. 配置问题:如果你明确指定了 Jackson 应该使用某个构造器或属性来创建对象,并且这个构造器或属性不存在或不可访问,也会导致此异常。

要解决这个问题,你可以采取以下几个步骤:

  • 确保 Result 类有一个公共的无参构造器。
  • 如果 Result 类需要某些字段才能正确初始化,那么确保这些字段有相应的 setter 方法或者提供一个包含这些字段的构造器,并确保这些方法或构造器是公共可访问的。
  • 检查 Jackson 配置,确保没有错误地配置了对象的创建方式。
  • 如果你正在使用 Jackson 的注解来帮助序列化/反序列化,确保这些注解正确地应用到了类或其属性上。

检查并修正上述问题后,你应该能够成功地将 JSON 反序列化为 Result 对象。

八、参考文章

【微服务十二】SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)

openFeign增强解码器

SpringCloud OpenFeign 自定义响应解码器的问题记录

解决Feign的自定义解码器在接口返回值为void时不执行的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宋冠巡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值