

Feign Api返回值为void时获取 Response信息
source link: https://www.techstack.tech/post/Feign%20Api%E8%BF%94%E5%9B%9E%E5%80%BC%E4%B8%BAvoid%E6%97%B6%E8%8E%B7%E5%8F%96%20Response%E4%BF%A1%E6%81%AF/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

macOS 10.15.6 jdk1.8 Springboot2.3.1RELEASE
spring-cloud-starter-openfeign2.2.3.RELEASE
A、B 两个服务之间通过 Feign Api 进行通信, A 提供的 Api 包中包含一些返回值为 void 的方法。
@RequestMapping("/provider/product")
@CompileStatic
interface IProductService {
/**
* 新增产品
* @param ageConfigDTOList
* @return
*/
@PostMapping('/add')
void save(@Validated @RequestBody ProductAO productAO)
」
A 服务进行了异常统一处理,接口出现异常时统一封装错误信息进行返回:
@ExceptionHandler(BizException.class)
ResponseEntity<ResponseBean> handleBizException(BizException bizException, HandlerMethod handlerMethod) {
log.warn("BizException caught of method with name [${handlerMethod.method.name}]. Message: [${bizException.message}]")
return ResponseEntity.ok(ResponseBean.fail(ResponseCodeEnum.ERROR, bizException.message))
}
@Override
protected ResponseEntity<ResponseBean> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("MethodArgumentNotValidException caught of request [${request.getDescription(false)}]. Message: [${ex.message}]")
List<FieldError> errors = ex.bindingResult.fieldErrors
List<String> messageList = errors.collect { it.defaultMessage }
return ResponseEntity.ok(ResponseBean.fail(ResponseCodeEnum.INVALID_INPUT, messageList.join(",")))
}
当 A 服务 save 方法出现业务异常时,会将业务异常信息统一封装之后放入 ResponseBody 里面返回给 B 服务。但是由于提供的 Api 包 save 方法的返回值为 void,导致 B 服务获取不到错误信息。无法对信息进行处理(打印日志、返回前端、封装错误信息提示等)。
- 修改 A 服务提供的 Api 包,void 方法可改为
ResponseEntity<Void> save(@Validated @RequestBody ProductAO productAO)
或者其它自定义数据结构。 - 修改 A 服务统一异常处理方法,将
ResponseEntity.ok(ResponseBean.fail(ResponseCodeEnum.ERROR, bizException.message))
改为return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(ResponseBean.fail(ResponseCodeEnum.ERROR, bizException.message))
Feign 是什么
spring-cloud-openfeign 在 Github 描述了其特性:
Declarative REST Client: Feign creates a dynamic implementation of an interface decorated with JAX-RS or Spring MVC annotations
Feign 利用注解来描述接口,简化了 Java HTTP Client 的调用过程,隐藏了实现细节。在应用启动时,Feign 会自动为@FeignClient标记的接口动态创建实现类。在调用接口时,会根据接口上的注解信息来创建RequestTemplate,结合实际调用时的参数来创建Request,最后完成调用。具体的Http Client可以自由选择,如:Apache Http Client、OkHttp等都可以。
EnableFeignClients
对于 Springboot 继承的这些第三方插件来说,搞明白其原理或者查看源码的第一步就是通过@Enable*
注解找到程序入口。对于 Feign 来说,第一步就是找到注解@EnableFeignClients
。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients
它以@Import的方式将FeignClientsRegistrar实例注入到Spring Ioc 容器中。
FeignClientsRegistrar
FeignClientsRegistrar 用于处理 FeignClient 的全局配置和被@FeignClient标记的接口,为接口动态创建实现类并添加到Ioc容器。
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 处理默认配置类
registerDefaultConfiguration(metadata, registry);
// 注册被 @FeignClient 标记的接口
registerFeignClients(metadata, registry);
}
}
在处理@FeignClient
处理的接口时调用了registerFeignClients(metadata, registry)
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
...
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
...
// 注册 FeignClient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
registerFeignClients(metadata, registry)
内部又调用了registerFeignClient(registry, annotationMetadata, attributes)
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
// 拿到FeignClientFactoryBean的BeanDefinitionBuilder
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
...
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
从上述代码可以看出registerFeignClients()用来处理@FeignClient标记的接口。首先扫描了classpath中@FeignClient标记的接口,然后注册。由于@FeignClient标记的是接口,不是普通对象,因此 Feign 利用了 FeignClientFactoryBean 来特殊处理BeanDefinitionBuilderdefinition=BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class)
。接着看 registerFeignClient(),最重要的是FeignClientFactoryBean.
也就是说,FeignClient 标记的接口实例会由 FeignClientFactoryBean.getObject() 来搞定。调试时在 getObject() 加个断点,在创建具体对象时会进入该方法。getObject()时会根据@FeignClient注解的一些属性信息来创建bean。
@Override
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
// 如果FeignClient没有指定URL(配置的是service)
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
跟进(HystrixTargeter)targeter.target(),调用了feign.target(target)
最后会发现调用了SynchronousMethodHandler.create()方法。也就是说,FeignClientFactoryBean.getObject() 返回的是一个SynchronousMethodHandler对象。
public MethodHandler create(Target<?> target, MethodMetadata md,
RequestTemplate.Factory buildTemplateFromArgs,
Options options, Decoder decoder, ErrorDecoder errorDecoder) {
return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
logLevel, md, buildTemplateFromArgs, options, decoder,
errorDecoder, decode404);
}
SynchronousMethodHandler是核心类,负责根据参数创建RequestTemplate,然后使用具体的http client执行请求。看一下SynchronousMethodHandler.invoke()方法。
@Override
public Object invoke(Object[] argv) throws Throwable {
// 利用参数构建请求模板, argv 就是被MVC注解描述的各种参数
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
// 执行请求
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
具体执行由executeAndDecode()搞定,targetRequest()就是应用Feign的拦截器,decode()用于处理response,可以自定义Decoder.
Object executeAndDecode(RequestTemplate template) throws Throwable {
// 应用Feign 的拦截器
Request request = targetRequest(template);
Response response;
long start = System.nanoTime();
try {
// 真正发起请求
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 10
response.toBuilder().request(request).build();
} catch (IOException e) {
...
}
try {
// response 处理机制,可以自定义Decoder来处理response
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
return decode(response);
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
return decode(response);
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
} ...
}
至此FeignClient 执行的主要流程就清楚了,回归到遇到的问题 Api void 方法无法获取到响应的 Response 信息,可以清楚的由上面 executeAndDecode
方法中的 if (response.status() >= 200 && response.status() < 300)
看出,对于 Api 中定义为 void 的方法执行了下面的代码:
if (void.class == metadata.returnType()) {
return null;
}
对于 Response code 在[200,300)范围内的 void 方法直接return null
程序没有进行 decode。至此解决方案就能很显然的的出来了。
tips:
对于一些有返回值的 Api 接口,如果程序出现了业务异常而并不想使用 http code 为错误码返回,可以在调用者服务中编写自定义 decode 类进行 decode 处理。
@Bean Decoder feignDecoder(ObjectFactory<HttpMessageConverters> messageConverters) { return new OptionalDecoder( new ResponseEntityDecoder(new BizExceptionDecoder(messageConverters))); } class BizExceptionDecoder extends SpringDecoder { BizExceptionDecoder(ObjectFactory<HttpMessageConverters> messageConverters) { super(messageConverters) } @Override Object decode(Response response, Type type) throws IOException, DecodeException, FeignException { Response.Body body = response.body() if (body != null) { String result = Util.toString(body.asReader(Util.UTF_8)); try { ResponseBean responseBean = JsonUtil.parse(result, ResponseBean.class) if (!responseBean.success) { throw new BizException(responseBean.message) } } catch (Exception ignore) { } } return super.decode(response, type) } }
Recommend
-
39
打造适用于c#的feign 之前因为工作原因使用sp...
-
31
feign简介: feign是一种声明式,模板化的HTTP客户端,spring cloud对feign进行了增强,使其支持SpringMvc的相关注解,并整合了ribbon做负载均衡。在spring cloud中使用feign做HTTP远程服务请求,可以做到就像调用本地方法一样...
-
9
iframe无刷新跨域上传文件并获取返回值 2011-05-06 通常我们会有一个统一的上传接口,这个接口会被其他的服务调用。如果出现不同域,还需要无刷新上传文件,并且获取返回值,这就有点麻烦了。比如,新浪微博启用了...
-
8
HTTP Cookies是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求里被携带并发送到服务器上。 Cookie 主要用于以下三个方面: 会话状态管理(如用户登录状态、购物车、游戏分数或其它...
-
5
如果response返回InputStreamResource什么时候关闭流 ...
-
10
V2EX › 程序员 后端 response code 该怎样返回? hlayk · 43 分钟前 · 224 次点击 ...
-
5
iframe无刷新跨域上传文件并获取返回值 2011-05-06 通常我们会有一个统一的上传接口,这个接口会被其他的服务调用。如果出现不同域,还需要无刷新...
-
3
后端response返回一张图片,前端如何下载? 2022年8月29日 52次浏览 之前文章关于文件下载介绍过很多,当然也有图片下载的,例如跨域...
-
3
Feign Reactive:访问REST API的首选 解道Jdon ...
-
9
在Spring MVC中,我们有时需要记录一下请求和返回的内容,方便出现问题时排查。比较Header、Request Body等。这些在Controller也可以记录,但在Filter中会更方便。而我们使用的是OncePerRequestFilter。
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK