本文包含springboot2.x版本和3.x版本的解决方案
项目的接口有一个全局的响应包装器,将接口的所有返回,包括各种类型如List、Entity,或者void,以及抛出的异常,封装成统一的结构给到前端,所以在使用Feign发起远程调用的时候,需要一个自定义的解码器,来对正常的返回值做一个拆包处理,同时对可能返回的异常重新抛出
public class FeignDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException {
String result = Util.toString(response.body().asReader(Util.UTF_8));
CommonResult commonResult = JSON.parseObject(result, CommonResult.class);
if (commonResult.getCode() != ResultCode.SUCCESS.getCode()) {
throw new RuntimeException(commonResult.getMessage());
}
return JSON.parseObject(JSON.toJSONString(commonResult.getData()), type);
}
}
实际运行过程中,发现对于有返回值的方法,不管是正常响应,还是异常都可以正常处理,但是对于@FeiginClient中,返回值是void的方法,即使对方接口抛出了异常,客户端依然会收到success,经过排查发现,调用无返回值方法时,Feign的自定义解码器没有执行
通过debug查看接口的调用栈,发现接口在执行的时候,会经过一个AsyncResponseHandler的类处理,然后执行一个判断是否为空的方法isVoidType,目测跟我们遇到的问题有关联
进入AsyncResponseHandler,找到这个方法
@Experimental
class AsyncResponseHandler {
......
......
void handleResponse(CompletableFuture<Object> resultFuture, String configKey, Response response, Type returnType, long elapsedTime) {
......
......
if (response.status() >= 200 && response.status() < 300) {
if (this.isVoidType(returnType)) {
resultFuture.complete((Object)null);
} else {
result = this.decode(response, returnType);
shouldClose = this.closeAfterDecode;
resultFuture.complete(result);
}
}
......
......
}
}
如果返回类型是void,就直接跳过了,没有走解码流程
初步确定了问题点,继续在debug的调用栈中回溯,找这个方法调用的地方,在一个SynchronousMethodHandler的类中
final class SynchronousMethodHandler implements MethodHandler {
......
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
......
this.asyncResponseHandler.handleResponse(resultFuture, this.metadata.configKey(), response, this.metadata.returnType(), elapsedTime);
......
}
......
}
通过观察这个类,在它的构造方法中,发现了一个forceDecoding变量,顾名思义“强制解码”,散发着非常可疑的气息
private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors, Logger logger, Level logLevel, MethodMetadata metadata, feign.RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder, boolean decode404, boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy, boolean forceDecoding) {
this.target = (Target)Util.checkNotNull(target, "target", new Object[0]);
this.client = (Client)Util.checkNotNull(client, "client for %s", new Object[]{target});
this.retryer = (Retryer)Util.checkNotNull(retryer, "retryer for %s", new Object[]{target});
this.requestInterceptors = (List)Util.checkNotNull(requestInterceptors, "requestInterceptors for %s", new Object[]{target});
this.logger = (Logger)Util.checkNotNull(logger, "logger for %s", new Object[]{target});
this.logLevel = (Level)Util.checkNotNull(logLevel, "logLevel for %s", new Object[]{target});
this.metadata = (MethodMetadata)Util.checkNotNull(metadata, "metadata for %s", new Object[]{target});
this.buildTemplateFromArgs = (feign.RequestTemplate.Factory)Util.checkNotNull(buildTemplateFromArgs, "metadata for %s", new Object[]{target});
this.options = (Options)Util.checkNotNull(options, "options for %s", new Object[]{target});
this.propagationPolicy = propagationPolicy;
if (forceDecoding) {
this.decoder = decoder;
this.asyncResponseHandler = null;
} else {
this.decoder = null;
this.asyncResponseHandler = new AsyncResponseHandler(logLevel, logger, decoder, errorDecoder, decode404, closeAfterDecode);
}
}
这个forceDecoding默认是false,所以走了else分支,创建了一个AsyncResponseHandler
而这个AsyncResponseHandler就是导致接口返回void时,自定义解码器不执行的罪魁祸首
所以猜测如果把forceDecoding改为true,应该是能解决这个问题的
经过一番回溯,最终在Feign的内部类Builder里找到了这个属性
public abstract class Feign {
......
......
public static class Builder {
private final List<RequestInterceptor> requestInterceptors = new ArrayList();
private Level logLevel;
private Contract contract;
private Client client;
private Retryer retryer;
private Logger logger;
private Encoder encoder;
private Decoder decoder;
private QueryMapEncoder queryMapEncoder;
......
......
private boolean forceDecoding;
......
......
}
}
这个属性在它的构造方法里默认设置为false
public Builder() {
this.logLevel = Level.NONE;
this.contract = new Default();
this.client = new feign.Client.Default((SSLSocketFactory)null, (HostnameVerifier)null);
this.retryer = new feign.Retryer.Default();
this.logger = new NoOpLogger();
this.encoder = new feign.codec.Encoder.Default();
this.decoder = new feign.codec.Decoder.Default();
this.queryMapEncoder = new FieldQueryMapEncoder();
this.errorDecoder = new feign.codec.ErrorDecoder.Default();
this.options = new Options();
this.invocationHandlerFactory = new feign.InvocationHandlerFactory.Default();
this.closeAfterDecode = true;
this.propagationPolicy = ExceptionPropagationPolicy.NONE;
this.forceDecoding = false;
this.capabilities = new ArrayList();
}
然后找了下,只有一个非public方法forceDecoding可以将它改为true
按理说这个属性应该是可以通过配置文件传入的,不过并没有找到相关的读取入口,而且这个方法有意地没有像其他方法一样设置为public,不再纠结,看到这里便已经有可行的解决办法了
构建一个Feign.Builder,通过反射将该属性设置为true
@Configuration
public class FeignConfig {
......
......
@Bean
public FeignDecoder feignDecoder() {
return new FeignDecoder();
}
@Bean
public Feign.Builder feignBuilder(FeignDecoder decoder) throws Exception {
Feign.Builder builder = Feign.builder().decoder(decoder);
Field forceDecoding = builder.getClass().getDeclaredField("forceDecoding");
forceDecoding.setAccessible(true);
forceDecoding.set(builder, true);
return builder;
}
}
再次测试,@FeignClient里返回void的方法也可以进入解码器了
最后对解码器做一些调整,返回值是void就只判断响应码是否有异常,不做进一步的拆包处理
public class FeignDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException {
String result = Util.toString(response.body().asReader(Util.UTF_8));
CommonResult commonResult = JSON.parseObject(result, CommonResult.class);
if (commonResult.getCode() != ResultCode.SUCCESS.getCode()) {
throw new AicRuntimeException(ResultCode.RPC_FAILED, commonResult.getMessage());
}
if (Void.class == type || Void.TYPE == type) {
return null;
}
return JSON.parseObject(JSON.toJSONString(commonResult.getData()), type);
}
}
问题解决
后记
后来公司项目做了升级,从springboot2.x升级到3.x,顺着上面的思路,发现之前的forceDecoding变量不在了,取而代之的是一个decodeVoid
这个属性虽然不在Feign.Builder里,但它提供了一个public方法decodeVoid
public Builder decodeVoid() {
return (Builder)super.decodeVoid();
}
然后在父类中设置decodeVoid为true
public B decodeVoid() {
this.decodeVoid = true;
return this.thisB;
}
所以我们不需要再通过反射去修改,直接调用这个方法就行了,在原来FeignConfig里做一个小调整
@Bean
public Feign.Builder feignBuilder(FeignDecoder decoder) throws Exception {
Feign.Builder builder = Feign.builder().decoder(decoder);
builder.decodeVoid();
return builder;
}