8

Feign Api返回值为void时获取 Response信息

 4 years ago
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.
neoserver,ios ssh client
Feign Api返回值为void时获取 Response信息

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK