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
,带入到方法中,最终出现在请求的头部,不影响目标项目中参数的获取。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。