Spring Cloud OpenFeign 初体验以及返回结果为 null 排查过程
前言
OpenFeign 也是属于那种看上去就比较神奇的组件,Spring Cloud 前缀说明与微服务相关。它在声明了另一个服务里的请求地址和方法之后,就可以直接注入到控制器中,像 service 一样方便的调用。
之前接触过 RestTemplete、HttpClient,还有若依框架里面,直接用 URLConnection 手搓一个客户端。感觉上都大同小异,可论代码整洁度,那还是 OpenFeign 更甚一筹。
OpenFeign 对每一个资源服务创建一个客户端 Client,指明客户端名称,或者定义 url 访问服务。看着没几行代码,很简单,写起来也简单,但第一次试水就出现了难搞的问题。
解决过程
定义 OpenFeign 方法
项目框架: OpenFeign 2.2.2.RELEASE
按照需求定义了三个接口方法,第一个是操作记录,分页查询;其他两个是操作接口。
@FeignClient(name = ServiceNameConstants.SETTLEMENT_BACKEND,
fallbackFactory = SettlementClientFactory.class)
public interface SettlementClient {
@RequestMapping(value = "/xx/xx/list")
@ResponseBody
Result<Page<PayListEntityVo>> list(@RequestBody BaseQueryFormSettlement<PayListQueryForm> queryForm) queryForm) throws Exception;
...
@Slf4j
@Component
public class SettlementClientFactory implements FallbackFactory<SettlementClient> {
@Override
public SettlementClient create(Throwable throwable) {
return new SettlementClient() {
@Override
public Result<Page<PayListEntityVo>> list(BaseQueryFormSettlement<PayListQueryForm> queryForm) {
log.info("扣款日志接口异常");
log.info("queryForm -> {}", queryForm);
return null;
}
...
};
}
}完全按照项目中定义过的 Feign 形式来添加客户端接口和回退工厂处理类。包括客户端中的几个方法定义都照搬的调用项目里方法的定义,方法名,参数,返回类型全都一致,甚至抛出的异常也给加上去了。最后还需要复制原来的输入输出 VO。
在一切都准备好之后,就开始了第一次测试,然后控制台就收到了 扣款日志接口异常,检查输出的查询参数也没有问题。
通过项目配置文件以及 nacos 确认服务名称没有问题。
使用 postman 构建请求,直接调用报错:401 UnAuthorized,返回内容提示需要登录。所以接口头还是需要添加 Authorization,即 token 参数。
通过登录接口拿到了 token,填充到 postman 的请求中,依然报错。但使用 token 调试当前项目请求正常。也就是说当前项目中的 token 与调用接口项目的 token 并不能互通。
实现 Spring Cloud 资源共享,即 token 令牌互通
通过同事了解到 Spring Cloud 使用了 oauth2 授权中心,对应的数据库中有三张主要作用的表:oauth_access_token、oauth_refresh_token、oauth_client_details。前面两张表存储授权中心登录返回的访问令牌和刷新令牌,后面一张表是控制各个服务与资源的访问权限关系,需要关注的也是最后一张表 oauth_client_details。
oauth_client_details 表主要关注前面两个字段 client_id、resource_ids。
client_id用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成).resource_ids是客户端所能访问的资源id集合,多个资源时用逗号(,)分隔。
一开始理解的时候,服务名和这里的客户端名、资源名还会混淆。
- 服务名,一般就是项目名称
spring.application.name,目前我观察到是这样的,nacos 是否有其他可以修改的方法暂时不清楚。 - 客户端名
security.oauth2.client.client-id,虽然 Feign 中定义的也叫客户端,但里面用的是服务名。 - 资源名
security.oauth2.resource.id
按照 REST 编码规范理解,所有提供服务的项目都可以看作是一种资源。所以每个服务都会有一个客户端名和资源名。上面的 oauth_client_details 表里,一个客户端最少要对应自己的资源,然后就是其他需要访问的资源。
按照这样的关系添加资源到当前客户端之后,token 可以使用,请求正常返回了分页信息。
添加 token 到 feign 请求的 headers 中
按照其他的接口里携带 token 的写法,只需要添加一个 @RequestHeader("Authorization") String token 参数即可。但这样就破坏了与原来定义方法的一致性。因为调用 feign 方法是需要传入 token 参数的,这样方法就是两个参数,而目标方法只有一个参数,不确定是否有问题。
在添加 @RequestHeader("Authorization") String token 之后,再次调试依然提示 扣款日志接口异常,feign 方法调用返回还是 null。postman 中请求只是添加了 token 之后就可以访问到了,而代码添加 token 之后却没有起作用。这有两种情况,一种是 token 的添加方式不对,另外一种则是接受返回数据进行解析时失败了。
在一顿 “盲人摸象” 版的探索研究之后,发现百度到的方法 【feign】OpenFeign设置header的5种方式,里面本来就有这个 @RequestHeader 方式。其他第 5 条配置拦截器,添加 token 方法(在下面,具体可以参考微服务:OpenFeign进行服务通讯时携带Token数据)不管是全局的还是单独的配置都获取不到 token,不知道什么原因。
@Configuration
@Slf4j
public class FeignConfiguration implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("请求token为=============={}", request.getHeader("token"));
requestTemplate.header("token", request.getHeader("token"));
}
}并且这种全局的添加方式肯定是不友好的,因为项目中可能会有两种及以上的 token 类型会被使用。
打开 feign 日志
现在迫切的需求就是,可以看到整个 feign 的日志,比如请求参数、返回参数,还有错误日志之类的。之前看到的是别人写的关于 feign 日志的启用方法,之后也找到了官方给出的文档:1.12. Feign logging。
第一步添加配置,启用 debug 日志配置
logging.level.project.user.UserClient: DEBUG
# 或者
logging:
level:
# feign日志以什么级别监控哪个接口
com.xx.client.SettlementClient: debug第二步就是添加一个配置
@Configuration
public class FeignTokenConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}默认为 NONE,就是什么都没有,直接返回 FULL 全部显示。
然后再测试控制台就多出来下面的内容:
可以看到 token 是有正常的添加了,响应部分返回数据也都有。但最后还是走到了回退工厂方法里,控制台依然提示 扣款日志接口异常。
查看回退方法中抛出的异常
日志有了,也起到了一定的作用,但还需要返回错误的信息,比如到底是反序列化失败了,字段类型不正确了,还是什么问题。
百度 feign 请求返回 null 如何获取报错信息,看到一条:
log.error("userInfoById error", cause); // cause 就是抛出的异常把这这个输出日志加入到每一个回退工厂方法里,输出错误轨迹:
@Slf4j
@Component
public class SettlementClientFactory implements FallbackFactory<SettlementClient> {
@Override
public SettlementClient create(Throwable throwable) {
return new SettlementClient() {
@Override
public Result<Page<PayListEntityVo>> list(BaseQueryFormSettlement<PayListQueryForm> queryForm, String token) {
log.info("扣款日志接口异常");
log.error("list error", throwable);
log.info("queryForm -> {}", queryForm);
return null;
}
...
};
}
}再次测试,终于拿到了错误内容:
feign.codec.DecodeException: Type definition error: [simple type, class org.springframework.data.domain.Page]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of org.springframework.data.domain.Page (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information解决 org.springframework.data.domain.Page 类型解析报错
看内容就是 org.springframework.data.domain.Page 类型解析报错了。百度了一下,得到一个 Cannot construct instance of org.springframework.data.domain.Page。
org.springframework.data.domain.Page是一个接口,没有默认的构造方法。
当jackson 反序列化Page对象的时候找不到默认的构造方法,从而导致反序列化失败,因此抛出异常。
解决方案也给了,需要添加一个回退工厂配置类,说是 OpenFeign 2.2.5.RELEASE 以上版本有效。项目中的版本是 2.2.2.RELEASE,但好歹先试一下:
import com.fasterxml.jackson.databind.Module;
import org.springframework.cloud.openfeign.support.PageJacksonModule;
import org.springframework.cloud.openfeign.support.SortJacksonModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfigurationFactory {
@Bean
public Module pageJacksonModule() {
return new PageJacksonModule();
}
@Bean
public Module sortJacksonModule() {
return new SortJacksonModule();
}
}结果报错,说是 SortJacksonModule 类型找不到。去除这个 Bean 实例注入之后,再试,可以正常返回了:
@Configuration
public class FeignConfigurationFactory {
@Bean
public Module pageJacksonModule() {
return new PageJacksonModule();
}
}最后经过测试的到一些结论:feign 方法名称、方法输入参数顺序、个数不必与原方法完全一致,可以增加自定义;类似 token 之类的参数,可以通过 @RequestHeader("Authorization") String token,带入到方法中,最终出现在请求的头部,不影响目标项目中参数的获取。