一、场景
在SpringBoot
项目中,使用 OpenFeign
调用远程服务的接口。接口返回的响应数据,是封装到数据类型(Result.java)中的,Result
对象中的data
对象的数据,才是后续业务逻辑需要的核心数据。
在默认情况下,FeignClient
接口的返回类型应使用Result<T>
,比如 Result<UserVo>
。
当有大量 FeignClient 接口都使用 Result 来接收对象的时候,会产生一系列问题:
- 首先,FeignClient 接口定义的代码冗余,写了大量的 Result 来包装核心响应数据。
- 其次,FeignClient 接口返回值处理逻辑,冗余更多,需要在每一处调用 FeignClient 接口的地方,解析
Result 状态码
,判断接口请求是否成功(状态码是否为成功)。如果请求成功,则取出data字段的值;如果请求失败,则做业务失败逻辑处理(比如抛出业务异常)。
二、需求:FeignClient 接口响应,统一解析
此场景的核心需求:FeignClient 接口响应,统一解析。
详细需求包含以下三点:
- FeignClient 接口,
返回值类型
为核心数据类型(Result.data对象的类型
),而不是使用统一封装类型Result
。 - 统一处理请求失败的情况,抛出业务异常,交给异常处理器统一处理。
- FeignClient 接口返回值为
void
时,使用解码器,解析Result,处理请求失败。默认情况,返回值为void(比如新增、修改、删除接口返回值,没有data数据),不会进入解码逻辑,业务始终识别为调用接口成功。
三、解决方案
自定义解码器 + Feign.builder().decodeVoid()
-
FeignClient配置自定义解码器(Decoder),该解码器能够解析响应数据,转为Result对象,并从中提取data数据,作为接口的返回值。
-
对于 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
对象。下面逐行解释这段代码:
-
构建参数类型:
JavaType parameterType = objectMapper.getTypeFactory().constructType(type);
objectMapper.getTypeFactory()
: 获取ObjectMapper
内置的TypeFactory
实例。constructType(type)
: 使用给定的Type
参数构造一个JavaType
实例。这里的type
是解码的目标类型,通常是调用 Feign 接口的方法所指定的返回类型。
-
构建泛型类型:
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>>
。 -
反序列化 JSON 字符串:
Result<?> result = objectMapper.readValue(bodyString, resultType);
readValue(String content, JavaType valueType)
: 使用给定的JavaType
将 JSON 字符串反序列化为相应的 Java 对象。bodyString
: 从 FeignResponse
读取到的 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)。
这个异常通常有以下几种原因:
-
缺少默认构造器:如果你的
Result
类定义了私有的默认构造器或者根本没有提供默认构造器(即一个无参的公共构造器),那么 Jackson 将无法创建这个类的新实例。 -
缺少必需的属性:如果
Result
类需要一些必须的字段来构造,并且这些字段没有对应的 setter 方法或者构造函数,Jackson 也可能无法正确地创建对象。 -
配置问题:如果你明确指定了 Jackson 应该使用某个构造器或属性来创建对象,并且这个构造器或属性不存在或不可访问,也会导致此异常。
要解决这个问题,你可以采取以下几个步骤:
- 确保
Result
类有一个公共的无参构造器。 - 如果
Result
类需要某些字段才能正确初始化,那么确保这些字段有相应的 setter 方法或者提供一个包含这些字段的构造器,并确保这些方法或构造器是公共可访问的。 - 检查 Jackson 配置,确保没有错误地配置了对象的创建方式。
- 如果你正在使用 Jackson 的注解来帮助序列化/反序列化,确保这些注解正确地应用到了类或其属性上。
检查并修正上述问题后,你应该能够成功地将 JSON 反序列化为 Result
对象。
八、参考文章
【微服务十二】SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)