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

本文包含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;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值