catface,使用Interface定义Controller,实现基于Http协议的RPC调用
catface
- 前言
- cat-client 模块
- @EnableCatClient
- @CatClient
- @CatMethod
- @CatNote
- @CatResponesWrapper
- CatClientConfiguration
- CatClientProvider
- CatClientFactory
- CatSendInterceptor
- CatHttp
- CatPayloadResolver
- CatObjectResolver
- CatLoggerProcessor
- CatResultProcessor
- CatSendProcessor
- AbstractResponesWrapper
- CatClientBuilders
- CatHttpRetryConfigurer
- 其他说明
- 1. 方法入参注解
- 2. 方法响应类
- 3. 示例
- cat-server 模块
- @EnableCatServer
- @CatServer
- @CatBefore
- CatServerConfiguration
- CatParameterResolver
- CatResultHandler
- CatServerInterceptor
- CatInterceptorGroup
- 其他说明
- cat-face 模块
- @Catface
- @CatNotes
- 其他说明
前言
关键词:Interface定义Controller;feign服务端;feign interface;Http RPC;cat-client;cat-server;catface;
概 要:catface
,使用类似FeignClient
的Interface作为客户端发起Http请求,然后在服务端使用实现了这些Interface的类作为Controller
角色,将客户端、服务端通过Interface耦合在一起,实现无感知调用的轻量级组件。其底层通讯协议依旧是Http,支持Http的所有特性:证书、负载均衡、熔断、路由转发、报文日志、swagger等;
如果使用过dubbo
就比较好理解,和dubbo
调用非常类似。只不过catface
只支持Http协议,并且目前只有spring项目可以使用。
catface
的灵感来于FeignClient
,想必其他同学在看见feignClient的Interface的时候,应该都有一个想法:feign interface使用的注解,和Controller有很是多共用,那么能不能直接用使用这些Interface来定义Controller呢?
举个例子
先粗略看一下feign的接口IDemoService
:
为了后续避免歧义,此处我们将类似于IDemoService的Interface,称呼为feign-interface
public interface IDemoService {@RequestMapping(value = "/server/demo41", method = RequestMethod.POST)Demo demo1(@RequestBody Demo req);@RequestMapping(value = "/server/demo43", method = RequestMethod.GET)List<Demo> demo3(@ModelAttribute Demo req);@RequestMapping(value = "/server/demo44", method = RequestMethod.POST)ResponseEntity<Demo> demo4(@RequestParam("userName") String name, @RequestParam("userMark") String mark);@RequestMapping(value = "/server/demo46/{uid}", method = RequestMethod.GET)Void demo6(@PathVariable("uid") Long userId, @RequestParam("userName") String name);@RequestMapping(value = "/server/demo47", method = RequestMethod.PUT)Demo demo7(@RequestHeader("token") String token);
}
FeignClient客户端,在spring项目中,只需要把IDemoService
作为一个普通的Service类使用,自动注入到其他Bean中,就可以直接执行IDemoService类中的方法,FeignClient便会自动根据方法上的注解信息,发起Http请求;
FeignClient服务端:没有严格意义上的FeignClient服务端!在spring项目中一般是Controller
提供的接口,返回相应的报文;
如果是公司内部系统相互调用,例如:clientA调用serverB:
在clientA中定义若干feign-interface作为客户端,并定义接口方法的入参、响应类;
在serverB中定义Controller、以及对应DTO、VO模型;
可以发现:
:: serverB中的DTO、VO,其数据结构,和feign-interface方法上入参、响应类是一致的;
:: clientA与serverB,在代码层面上没有任何联系。无论是feign-interface的方法入参、响应类增减字段,都不会影响到DTO、VO模型;反之也是一样;
如果存在feign-interface可以直接定义Controller这个功能,那么上述一般流程,可以修改成如下模式:
新增一个接口层:serverB-facede,表示由serverB模块对外提供服务;
其中:
:: serverB-facede中包含:feign-interface、方法的入参、响应类;
:: clientA依赖serverB-facede模块,在clientA中依旧可以使用feign-interface客户端;
:: serverB同样依赖serverB-facede模块,在serverB中实现feign-interface接口,将实现类注册成Controller;
如此以来,clientA和serverB,便通过serverB-facede耦合在一起。看上去就像是在clientA模块中,自动注入了一个feign-interface类,执行其中的方法,就可以得到serverB模块中实现类返回的数据!Http调用对于消费者、提供者,都是无感知的;
clientA 调用 serverB 服务消费者 ===================================> 服务提供者 │ │ │ 注入 │ │ │ └────────────── feign-interface │ │ │ └───────────────────────┤ │实现 ││feign-interface 实现类
如果了解spring扫描Bean的原理,在服务端根据feign-interface上的注解,手动注册Controller不难;而且在熟悉动态代理的情况下,自己根据feign-interface写一个http RPC也不是不可能;
于是在技术实现都不难的情况下,catface
就出现辣~:
项目地址:https://github.com/bugCats/cat-client-all
catface
基于spring框架开发的 (不会吧不会吧,在如今spring大有一统江湖的趋势,还会有新项目没有使用spring全家桶的吧?),通过Interface、以及其方法的输入返回模型,耦合客户端与服务端,实现无感知Http调用。
对于开发者而言,眼前没有客户端、服务端,只有Interface调用者和Interface的实现类:
-
精细化到每个接口的输入输出报文记录方案;
比如某些API是核心流程,需要记录详细的输入输出报文,以便如果后续出现问题,可以查阅日志内容;而有些API不是那么重要,但调用又非常频繁,此时我们不希望打印它们,以免浪费系统性能、浪费存储空间;对于一般的API,平时的时候就记录一下调用记录,如果发生Http异常、或返回了业务异常,就记录详细的输入输出报文; -
精细化到每个接口的Http等待时间:
对于中台而言,调用后台接口,不可能无限等待后台接口响应;和日志一样的逻辑,有的API等待时间可以长点,有的又不行; -
自动加、拆响应包装器类:
当服务端的所有响应,为统一的一个数据模型、具体的业务数据,是该模型通过泛型确认
的属性时,例如:HttpEntity<User>、ResponesDTO<User>,称这种似于HttpEntity、ResponesDTO的数据结构为响应包装器类
;public class ResponesDTO<T> {private String code; // code为1表示成功,非1表示失败private String message; //异常原因说明private T data; //业务数据 }
当客户端获取到响应对象ResponesDTO<User>之后:
1. 需要先判断对象是否为null;
2. 再判断code是否为1,不为1需要进入异常流程;
3. 最后才能获取到业务数据User;catface
支持自动拆响应包装器类。当启用后,客户端feign-interface的方法返回数据类型:
:: 可以是ResponesDTO<User>,保持原流程不变;
:: 直接为User,而服务端无需做任何修改。当feign-interface的方法成功执行完并返回了User对象,表示Http调用成功,并且响应的code一定是1;如果调用失败、或者code不为1,会自动进入预先设置好的异常流程;加响应包装器类,针对服务端而言,是拆包装器类的逆操作。当服务端启用之后,feign-interface和与之对应的Controller方法响应的数据类型,可以直接是User,
catface
会自动在最外层加上ResponesDTO;另外,对于使用继承实现公共属性code、message和业务数据并列的情况,也同样适用于此功能;
-
自定义标记功能:
在feign-interface上添加自定义标记、还可以为单个API接口添加标记。结合拦截器、springEL动态解析入参,便可以灵活实现各种各样的业务逻辑; -
在服务端,通过继承实现API接口升级:
在Java中,继承本身就可以增强父类功能。此特性也适用于catface
,通过继承特性对服务端API接口升级增强,而客户端无需任何修改; -
通过feign-interface生成的Controller,依旧支持swagger;
-
其他比较一般的功能:拦截器、修改http Jar包、负载均衡、熔断、异常重试、Mock测试等等;
cat-client 模块
此模块可以单独使用,使用方式和feignclient
非常类似;以feign-interface为模板动态生成Http调用实现类,在应用层自动注入feign-interface对象,执行feign-interface中的方法,即可发起http请求,并最终将Http响应结果,转换成方法返回对象数据类型,返回给应用层;
@EnableCatClient
这个注解表示启用CatClient客户端。该注解依赖于spring容器
,置于任何spring bean之上,例如:springboot项目启动类上、或者任意包含@Component
注解的类上;
- value: 扫描的包路径,处于这个目录中的feign-interface都会被注册成客户端。默认值是被标记类的同级目录;
- classes: 指定客户端class进行注册,优先度高于包路径。class值分为2大类:
:: 普通的class: 当类上包含@CatClient注解时,将该class注册成客户端;
:: CatClientProvider的子类: 解析该子类中包含@CatClient注解的方法,将方法返回对象的class注册成客户端; - configuration: 生成客户端的一些配置项和默认值;
@CatClient
该注解用于定义某个feign-interface为客户端,包含:客户端别名、远程服务端host、拦截器、异常回调、http等待时间、默认的API日志记录方案;定义CatClient客户端有2种方式:
方式1: 在feign-interface上添加@CatClient
;系统启动时,会根据@EnableCatClient的配置,自动扫描并注册成客户端:
@CatClient(host = "${userService.remoteApi}", connect = 3000, logs = RequestLogs.All2)
public interface IUserService {ResponseEntity<PageInfo<UserInfo>> userPage(@RequestHeader("token") String token, @ModelAttribute UserPageVi vi);UserInfo userInfo(@PathVariable("uid") String uid, @RequestParam("type") String type);ResponseEntity<Void> userSave(@RequestBody UserSaveVi vi);
}
方式2: 通过CatClientProvider的子类,集中批量定义。将包含@CatClient
注解的方法其返回对象的class注册成客户端;
public interface RemoteProvider extends CatClientProvider {@CatClient(host = "${userService.remoteApi}", connect = 3000, socket = 3000)IUserService userService(); //实际上将IUserService注册成客户端@CatClient(host = "${orderService.remoteApi}")IOrderService orderService(); //将IOrderService注册成客户端
}
- value: 客户端组件的别名,默认首字母小写。最终客户端会注册到spring容器中,可以通过
@Autowired
实现自动注入; - host: http请求的主机地址。可以是 IP+端口,也可以是域名、cloud服务名:
:: 字面量: https://www.bugcat.cc
:: 配置文件值: ${xxx.xxx}
:: 服务名,配合注册中心: http://myserver-name/ctx - interceptor: http请求拦截器;用来修改入参、请求url、修改参数签名、添加token等处理;默认值受CatClientConfiguration控制;
- factory: 负责创建发送Http请求相关对象的工厂类;一般如果仅需修改入参、或者添加签名等,可以使用拦截器修改。如果有比较大的Http流程调整,才考虑修改CatClientFactory,例如:添加负载均衡;默认值受CatClientConfiguration控制;
- fallback: 异常处理类;当接口发生http异常(40x、50x),执行的回调方法。如果在回调方法中,继续抛出异常,或者关闭回调模式,则会执行CatResultProcessor#onHttpError进行最终兜底处理;其值可以取以下类型:
:: Object.class: 尝试使用feign-interface默认方法,如果feign-interface没有默认实现,再执行兜底方法;
:: Void.class: 关闭回调模式,直接执行兜底方法;
:: 其他class值: 必须实现该feign-interface。当发生异常之后,执行实现类的对应方法; - socket: http读值超时毫秒,-1 代表不限制;默认值受CatClientConfiguration控制;
- connect: http链接超时毫秒,-1 代表不限制;默认值受CatClientConfiguration控制;
- logsMod: 记录日志方案;默认值受CatClientConfiguration控制;
- tags: 分组标记,给客户端添加自定义标记;详细使用见CatNote;
@CatMethod
定义客户端feign-interface中的方法,为API接口;
- value: 具体的url;
:: 字面量: /qq/972245132
:: 配置文件值: ${xxx.xxx}
:: uri类型参数: /qq/{pathVariable} - method: Http请求方式,默认使用POST发送表单;
- notes: 自定义参数、或标记;当方法上还存在@CatNotes注解时,会忽略这个属性值!详细使用见CatNotes;
- socket: http读值超时毫秒;-1 不限;0 同当前feign-interface配置;其他数值,超时的毫秒数;
- connect: http链接超时毫秒;-1 不限;0 同当前feign-interface配置;其他数值,超时的毫秒数;
- logsMod: 日志记录方案;Def 同当前feign-interface配置;
对于常用的GET、POST请求方式,还有@CatGet
、@CatPost
2个便捷组合注解;
@CatNote
自定义标记注解;有2大类使用场景:
- 为feign-interface、方法、参数,添加标记;
- @CatNote(key=“name”, value=“bugcat”): 字面量;创建了一个标记,标记名=name,标记值=bugcat;
- @CatNote(“bugcat”): 字面量;省略key属性,最终key与value值相同,即标记名=标记值=bugcat;
- @CatNote(key=“host”, value=“${orderhost}”): 取配置文件值;创建了一个标记,标记名=host,标记值从配置中获取orderhost对应的值;
- @CatNote(key=“userId”, value=“#{req.userId}”): 取方法入参值;使用springEL表达式,从方法入参对象中获取标记值。此种方法,必须要为入参取别名;
- 为方法入参取别名;一般配合
@RequestBody
使用,可以实现 #{参数别名.属性} 动态获取入参的属性值;可为入参取别名注解有:- @ModelAttribute(“paramName”): 为GET、POST表单对象取别名;
- @PathVariable(“paramName”): PathVariable类型参数别名;
- @RequestParam(“paramName”): 键值对参数别名;
- @RequestHeader(“paramName”): 请求头参数别名;
- @CatNote(“paramName”): 通用类型参数别名,一般结合
@RequestBody
使用;
@CatResponesWrapper
为当前feign-interface,配置加、拆响应包装器类;由于这个注解是标记在feign-interface接口上,因此如果feign-interface是作为客户端,那么@CatResponesWrapper
便是启用拆响应包装器类功能;如果是作为服务端Controller,则是加响应包装器类;
- value: Http响应包装器类处理类;
CatClientConfiguration
生成客户端的一些配置项和默认值。配合@EnableCatClient使用,可用于修改@CatClient
、@CatResponesWrapper
注解的实际默认值;
例如:@CatClient#socket的默认值为1000。如果需要统一修改成3000,而不想在每个feign-interface客户端上修改socket=3000,可以重写CatClientConfiguration#getSocket方法,使其返回3000即可;
- getSocket: http读值超时,默认1000毫秒;对应@CatClient#socket;
- getConnect: http链接超时,默认1000毫秒;对应@CatClient#connect;
- getLogsMod: API接口输入输出报文记录方案,默认是当发生Http异常时记录;对应@CatClient#logsMod;
- getWrapper: 拆包装器类处理类,对应@CatResponesWrapper#value;
- getMethodInterceptor: http请求拦截器,对应@CatClient#interceptor;
- getClientFactory: 创建客户端发送Http请求相关对象的工厂类,对应@CatClient#factory;
- getCatHttp: Http请求发送工具类,默认使用
RestTemplate
; - getPayloadResolver: Http请求输入输出对象,序列化与反序列化处理类;默认使用
Jackson
框架; - getLoggerProcessor: 打印Http日志类;默认使用
logback
框架。如果需要修改日志打印格式,可以实现CatLoggerProcessor
接口;
CatClientProvider
批量注册客户端类;
public interface RemoteProvider extends CatClientProvider {@CatClient(host = "${userService.remoteApi}", connect = 3000, socket = 3000)IUserService userService(); //实际上将IUserService注册成客户端@CatClient(host = "${orderService.remoteApi}")IOrderService orderService(); //将IOrderService注册成客户端
}
采用此方法定义客户端,将@CatClient置于CatClientProvider子类的方法之上,使得@CatClient注解与feign-interface类在物理上分隔开,避免注解污染feign-interface,以便可以多次复用feign-interface;
配合@EnableCatClient#classes使用,可以集中处理,按需加载使用。特别适用于多模块、多客户端feign-interface的场景;
例如:在serverB-facede模块中,与非常多的feign-interface,其中若干个feign-interface实现一个完整的业务流程。
为了避免多个消费端,需要多次手动注册多个feign-interface客户端,可以在serverB-facede模块中创建一个CatClientProvider子类,将相关feign-interface在其子类中预先定义好。
消费端在@EnableCatClient#classes中指定该子类,即可实现批量注册feign-interface客户端;
CatClientFactory
创建客户端发送Http请求相关对象的工厂类。CatClientConfiguration
中的参数适用于全局的默认配置。如果存在部分feign-interface客户端,需要特别的个性化配置,就需要使用自定义CatClientFactory
,返回个性化的配置项;可以实现CatClientFactory
接口,或者继承SimpleClientFactory
,再修改@CatClient#factory参数值;
@CatClient(host = “${orderhost}”, factory = TokenFactory.class)
- getCatHttp: 自定义Http发送类;
- getPayloadResolver: 自定义入参响应序列化与反序列类;
- getLoggerProcessor: 自定义日志格式打印类;
- getResultHandler: 自定义http响应处理类;
- newSendHandler: 自定义http发送流程类;返回对象必须是多例!
CatSendInterceptor
Http发送请求流程中的拦截器;可以重写CatClientConfiguration#getMethodInterceptor方法修改全局默认的拦截器;也可以通过@CatClient#interceptor为指定的feign-interface修改;
全局拦截器,和自定义拦截器只能生效一个!(支持多个拦截器下下下个版本再加)
拦截器有4个切入点,对应CatSendProcessor的四个方法:
- executeConfigurationResolver: 处理http配置项前后;对应CatSendProcessor#doConfigurationResolver方法,处理Http请求相关配置:host、url、读取超时、请求方式、请求头参数、解析自定义标记;
- executeVariableResolver: 处理入参数据前后;对应CatSendProcessor#doVariableResolver、CatSendProcessor#postVariableResolver2个方法:如果在调用远程API,需要额外处理参数、或添加签名等,可以在此处添加;
:: doVariableResolver,入参转字符串、或表单对象;
:: postVariableResolver,入参转换之后处理,这是给子类提供重写的方法; - executeHttpSend: 发送Http请求前后;对应CatSendProcessor#postHttpSend方法;如果启用重连,那么该方法会执行多次!
- postComplete: 在成功、异常回调方法之后、拆响应包装器之前执行;此处可以再次对响应对象进行修改;对于异常流程,可以继续抛出异常;
调用流程示意:
CatMethodAopInterceptor#intercept│CatClientContextHolder#executeConfigurationResolver│CatSendProcessor#doConfigurationResolver <------- CatSendInterceptor#executeConfigurationResolver│CatClientContextHolder#executeVariableResolver│CatSendProcessor#doVariableResolver <----┬---- CatSendInterceptor#executeVariableResolver│ :CatSendProcessor#postVariableResolver <--┘│CatClientContextHolder#executeRequest│CatSendProcessor#postHttpSend <------- CatSendInterceptor#executeHttpSend│[[CatResultProcessor#onHttpError]]│CatResultProcessor#resultToBean│CatClientContextHolder#postComplete <------- CatSendInterceptor#postComplete│CatResultProcessor#onFinally│
return <───────────────┘
CatHttp
Http发送请求工具类,默认实现类CatRestHttp
,底层使用RestTemplate
发送请求。优先是从spring容器中获取RestTemplate,如果spring容器中没有,才会自动创建。
因此,如果spring容器中的RestTemplate配置了负载均衡,那么对应的CatRestHttp同样也有负载均衡特性!
如果需要修改成其他Http框架,可以如下操作:首先需要实现CatHttp
接口;再将实现类编织到客户端Http发送流程中:
方案1:将CatHttp实现类对象,注册到spring容器中;适用于全局;
方案2:重写CatClientConfiguration#getCatHttp方法,使其返回指定CatHttp对象;适用于全局;
方案3:对于特定的API接口,可以利用CatClientFactory。在定义feign-interface客户端时,修改@CatClient#factory值为指定CatClientFactory子类,再重写CatClientFactory#getCatHttp方法,返回自定义CatHttp实现类对象;
CatPayloadResolver
针对于使用POST方式发送IO流情况,将输入输出对象,序列化与反序列化的处理类。如果是GET、POST发送表单数据,应该在CatHttp层进行统一的uri编码
。
默认使用Jackson
框架,catface
中还内置了Fastjson
框架处理类。如果需要使用其他框架、或者使用xml,可以自行实现CatPayloadResolver
接口,实现类编织到Http发送流程中方式,和自定义CatHttp一致;
CatObjectResolver
将feign-interface方法上的复杂数据类型入参,转成表单对象;
catface
不建议使用POST、GET发送太过于复杂的表单对象,推荐使用POST + Json这种一般方式。
虽然内置了入参数据转表单对象的处理类,但是对于怪异的场景如果出现不支持情况,就需要自行编写转换类。实现CatObjectResolver
接口,再执行CatSendProcessor#setObjectResolverSupplier方法手动赋值;CatSendProcessor对象可以在拦截器中获取得到;
CatLoggerProcessor
调用API接口的日志记录处理类。可以自行控制日志打印级别,以及日志格式;
CatResultProcessor
Http响应处理类:
- onHttpError: 当发生Http异常后默认执行流程,最终进行兜底的异常处理;
- canRetry: 异常是否需要重连,如果开启重连,并且发送Http异常,判断是否需要重新请求。一般结合熔断器或者注册中心使用,可以重新选择一个健康的服务端实例;
- resultToBean: 响应报文转结果对象,默认使用
Jackson
框架。如果是通过xml传输信息,需要自行实现CatPayloadResolver接口; - onFinally: 自动拆响应包装器类,结合响应包装器类使用,可以使API接口方法,直接返回业务对象;
只能通过自定义CatClientFactory#getResultHandler修改,为单例;
CatSendProcessor
Http发送请求核心处理类;该对象可以通过CatClientFactory#newSendHandler自动创建,也支持手动创建后,作为feign-interface方法的入参传入。
CatSendProcessor
类虽然提供了扩展的入口,但一般情况下无需修改,如果对Http请求整体流程有比较大的修改,才考虑覆盖重写。例如:搭配注册中心、负载均衡器使用、或者换成Socket协议等。(负载均衡也可以使用RestTemplate实现)
若仅仅是修改入参、添加token、签名,可以直接使用更轻量级的拦截器实现。
- doConfigurationResolver: 初始化http相关配置。可修改项包括:
:: Http连接读取超时:
:: Http请求方式: 虽然在@CatClient中声明是使用POST发送Json字符串,但是此处也可以修改成POST发送表单方式;
:: 远程服务端host、API调用的url: 可以使用@PathVariable、取环境配置参数${xxx}等方式动态给url赋值,也可以在此方法中修改host、url;
:: 自定义的标记数据: 根据@CatNote标记,获取到环境配置参数、动态取入参的属性值,可以对Host、url、入参、请求方式等,执行更自由修改;例如,集成负载均衡、自定义路由规则; - doVariableResolver: 请求入参的默认处理。如果是POST、GET表单方式,将方法入参转成表单对象;如果POST发送IO流,则将入参对象序列化成字符串;此方法可以将入参模型,修改成表单对象;转成Json字符串;添加公共参数;添加Token;计算签名;记录请求信息;
- doVariableResolver: 处理入参后处理。默认是个空方法,提供给子类重写。
- postHttpSend: 全部数据准备充分之后,发起Http请求;如果需要换成其他协议,如Socket,重写此方法,将最终响应结果存储到CatClientContextHolder#setResponseObject中;
AbstractResponesWrapper
响应包装器类处理类;
- getWrapperClass: 获取响应包装器类的class;用于判断API接口方法的响应对象,是包装器类、还是直接业务数据类;
- getWrapperType: 将业务数据类型Type,组装到响应包装器类中,返回标准响应Type的引用;例如,包装器类是ResponseDTO<T>,业务数据类型为User,API接口返回的原始数据类型应该为ResponseDTO<User>。对于使用拆包装器类的API接口方法,需要将方法的响应类(即业务数据类型),组装到包装器类中。
public <T> CatTypeReference getWrapperType(Type type){//type 为业务数据的类型,可以是User、List<Order>、Long、String[]等//ResponseDTO是响应包装器类,type最终会替换掉T的位置,最终结果是ResponseDTO<Type>//注意后面的一对花括号不能少!return new CatTypeReference<ResponseDTO<T>>(type){}; }
- checkValid: 校验返回的业务数据是否正确。需要注意异常分为两大类:
:: Http异常 由http请求造成的异常,例如:403 404 500 503,读取超时等,此类异常可以重新连接,或者换远程服务实例调用;
:: 业务异常 此类为服务端接收到请求,并且通过业务逻辑判断,得出不处理该请求,并将消息成功返回给Http调用者。例如:调用取消订单,服务端判断该订单已经使用,不可以取消。API接口调用成功,但是响应为逻辑处理失败。
checkValid方法主要是针对于业务异常场景,当发生业务异常时,程序该如何处理?可以选择继续抛出,由最顶层的@ControllerAdvice统一处理异常;也可以自行解析业务异常编码,再修改返回默认业务对象; - getValue: 从响应包装器类中获取业务数据;
- createEntryOnSuccess: 服务器端加响应包装器类,当成功执行;
- createEntryOnException: 服务器端加响应包装器类,当异常执行;
无论是CatResultProcessor#onHttpError继续抛出、还是AbstractResponesWrapper#checkValid校验失败抛出,都会造成应用层调用feign-interface方法发生异常。
但是在定义feign-interface方法时,方法可以不显示抛出异常,因此在调用时,应当清楚feign-interface方法会隐式抛出异常,需要注意如果发生异常该如何处理。
当应用层执行feign-interface方法后,希望无论是成功还是失败,都要有结果返回,然后应用层再根据执行结果,自行处理异常,那么不应该使用自动拆包装器类!
仅当应用层执行方法后,对于异常流程没有严格要求时,才会建议使用!
CatClientBuilders
静态方法创建CatClient客户端。可以在非spring环境中使用,也可以在运行过程中手动创建,或者单元测试时期使用;
CatHttpRetryConfigurer
当发生Http异常时,重新连接策略:
- enable: 是否开启重连;默认false;
- retries: 重连次数,不包含第一次调用!默认2,实际上最多会调用3次;
- status: 重连的状态码:多个用逗号隔开;可以为500,501,401或400-410,500-519,419或
*
或any,默认500-520; - method: 需要重连的请求方式,多个用逗号隔开;可以为post,get 或
*
或any,默认any; - exception: 需要重连的异常、或其子类;多个用逗号隔开;可以为java.io.IOException 或
*
或any,默认空; - tags: 需要重连的客户端分组,在@CatClient#tags中配置;多个用逗号隔开,默认空;
- note: 需要重连的API方法标记;多个用逗号隔开;会匹配@CatMethod#notes中的值;当配置的note值,在@CatNote#value中存在时,触发重连;
例如:note=bugcat匹配@CatNote(key=“name”, value=“bugcat”)、@CatNote(“bugcat”),不会匹配@CatNote(key=“bugcat”, value=“972245132”) - noteMatch: 需要重连的API方法标记键值对;在配置文件中,使用单引号包裹的Json字符串,默认值
'{}'
;当noteMatch设置的键值对,在@CatMethod#notes的键值对中完全匹配时,触发重连:
note-match=‘{“name”:“bugcat”,“age”:“17”}’,会匹配notes={@CatNote(key=“name”, value=“bugcat”), @CatNote(key=“age”, value=“17”)};
如果@CatNote采用springEL表达式形式,可以实现运行时,根据入参决定是否需要重连!
例如:当设置note=save,其中@CatMethod(notes = @CatNote(“#{req.methodName}”)),或者note-match=‘{“method”:“save”}’、对应@CatMethod(notes = @CatNote(key=“method”, value=“#{req.methodName}”))时,如果请求入参req的methodName值为save,会触发重连,其他则不会;
其他说明
1. 方法入参注解
@ModelAttribute、@RequestBody、@RequestParam在同一个方法中,只能三选一,可以和@RequestHeader、@PathVariable共存;
- @ModelAttribute: 用于标记复杂对象,只能存在一个;表示使用表单方式发送参数;
- @RequestBody: 用于标记复杂对象,只能存在一个;表示使用POST IO流方式发送参数;可以使用@CatNote为参数取别名;
- @RequestParam: 用于标记基础数据类型、字符串、日期对象,可以有多组;表示使用表单方式发送参数;
- @RequestHeader: 表示请求头参数,可以有多组;
- @PathVariable: 表示uri参数,可以有多组;
默认情况下@ModelAttribute、@RequestBody代表把对应的对象转换成表单、或者字符串。但是具体数据格式,需要参考拦截器、CatSendProcessor子类中的自定义逻辑;
2. 方法响应类
- 如果响应是Object类型,那么会返回Http的原始响应,无论是否启用了拆响应包装器类;
- 如果响应是Date类型,默认日期格式为
yyyy-mm-dd HH:mi:ss.SSS
,可以使用@JsonFormat#pattern
、JSONField#format
修改格式;
3. 示例
/*** 定义一个客户端;* 远程服务器地址为:${core-server.remoteApi},需要从环境变量中获取;* 该客户端定义了一个拦截器:TokenInterceptor;* 单独配置了http链接、读取超时:3000ms;* 其他配置为默认值,参考CatClientConfiguration;* 并且该客户端,配置了自动拆包装器ResponseEntityWrapper,实际API接口返回数据类型为ResponseEntity<T>;* 如果方法的返回类型不是ResponseEntity(除Object类型以外),一律推定需要使用自动拆包装器!* */
@CatResponesWrapper(ResponseEntityWrapper.class)
@CatClient(host = "${core-server.remoteApi}", interceptor = TokenInterceptor.class, connect = 3000, socket = 3000)
public interface TokenRemote {/*** CatSendProcessor手动创建并且作为方法入参传入;* 定义了2个标记:username、pwd,其标记值从环境配置中获取demo.username、demo.pwd对应的参数值;* 方法有默认实现,当发生Http异常后,会自动执行,并将结果作为Http请求的结果返回;* 虽然添加了自动拆响应包装器类,但是该方法返回数据类型仍然是ResponseEntity,* 所以依旧按正常流程解析,将原始响应,转成ResponseEntity<String>对象后,再返回;* */@CatMethod(value = "/cat/getToken", method = RequestMethod.POST, notes = {@CatNote(key = "username", value = "${demo.username}"), @CatNote(key = "pwd", value = "${demo.pwd}")})default ResponseEntity<String> getToken(CatSendProcessor sender) {return ResponseEntity.fail("-1", "当前网络异常!");}/*** 定义了1个标记,标记的key和value都是'needToken'这个字符串;* 方法返回数据类型String,和配置的包装器类型不一致,因此推定需要自动拆包装器,实际返回数据类型应该为ResponseEntity<String>;* 先将原始数据转换ResponseEntity<String>,再获取泛型属性对应值返回;* */@CatMethod(value = "/cat/sendDemo", method = RequestMethod.POST, notes = @CatNote("needToken"))String sendDemo1(@RequestBody Demo demo);/*** 将token参数,作为请求头参数传输;* 该方法返回类型为Object,为内定的特定数据类型,直接返回最原始的响应字符串;* */@CatMethod(value = "/cat/sendDemo", method = RequestMethod.POST)Object sendDemo2(@RequestBody Demo demo, @RequestHeader("token") String token);/*** 动态url,具体访问地址,由方法入参url确定;* */@CatMethod(value = "{sendurl}", method = RequestMethod.POST)default ResponseEntity<Void> sendDemo3(@PathVariable("sendurl") String url, @RequestHeader("token") String token, @RequestBody String req) {return ResponseEntity.fail("-1", "默认异常!");}/*** 给入参OrderInfo取了别名:'order';* 自定义了一个标记,标记的key='routeId',其value为入参OrderInfo的oid属性值;* 实际返回数据类型应该是ResponseEntity<Void>;* 如果返回的是基础数据类型,对应的ResponseEntity<基础数据类型包装类>;* */@CatMethod(value = "/order/edit", notes = @CatNote(key = "routeId", value = "#{order.oid}"), method = RequestMethod.POST)void sendDemo4(@CatNote("order") @RequestBody OrderInfo orderInfo);
}
/*** 拦截器* */
@Component
public class TokenInterceptor implements CatSendInterceptor {/*** 使用拦截器修改参数* */@Overridepublic void executeVariableResolver(CatClientContextHolder context, Intercepting intercepting) throws Exception {CatSendProcessor sendHandler = context.getSendHandler();sendHandler.setTracerId(String.valueOf(System.currentTimeMillis())); //设置日志id,可以通过日志id查询本次请求所有内容。如果不指定,自动使用uuidJSONObject notes = sendHandler.getNotes(); //所有的自定义标记都存放在这里CatHttpPoint httpPoint = sendHandler.getHttpPoint();String need = notes.getString("needToken");//使用note标记是否需要添加签名if( CatToosUtil.isNotBlank(need)){String token = TokenInfo.getToken();httpPoint.getHeaderMap().put("token", token);//将token存入到请求头中System.out.println(token);}intercepting.executeInternal(); // 执行默认参数处理}/*** token管理* */private static class TokenInfo {private static TokenInfo info = new TokenInfo();public static String getToken(){return info.getToken(System.currentTimeMillis());}private TokenRemote tokenRemote = CatClientUtil.getBean(TokenRemote.class);private long keepTime;private String value;private String getToken(long now){if( now > keepTime ){TokenSend sender = new TokenSend(); // 获取token的时候,显示使用指定CatSendProcessor实例ResponseEntity<String> bean = tokenRemote.getToken(sender);keepTime = System.currentTimeMillis() + 3600;value = bean.getData();return value;} else {return value;}}}/*** 获取token的时候单独处理器;* 一般情况使用拦截器即可,此处演示作用,使用继承CatSendProcessor形式修改参数* */private static class TokenSend extends CatSendProcessor {@Overridepublic void postVariableResolver(CatClientContextHolder context){String pwd = notes.getString("pwd"); //notes 已经在postConfigurationResolver方法中解析完毕String username = notes.getString("username");MultiValueMap<String, Object> keyValueParam = this.getHttpPoint().getKeyValueParam();keyValueParam.add("username", username);keyValueParam.add("pwd", pwd);//注意feign-interface中的getToken方法,//原getToken方法没有“有效的”入参,但是实际发送Http请求的时候,却有2组请求参数!//此特性可以非常灵活调整feign-interface的入参数量、请求方式等}}
}
/*** http响应包装器类处理。包装器类为:ResponseEntity<T>;* 如果在客户端,则为拆包装器;* 如果在服务端,则为加包装器;** @see AbstractResponesWrapper* @author bugcat* */
public class ResponseEntityWrapper extends AbstractResponesWrapper<ResponseEntity>{/*** 返回包装器类class* */@Overridepublic Class<ResponseEntity> getWrapperClass() {return ResponseEntity.class;}/*** 组装包装器类中的实际泛型* */@Overridepublic <T> CatTypeReference getWrapperType(Type type){return new CatTypeReference<ResponseEntity<T>>(type){};}/*** 拆包装器,并且自动校验业务是否成功?* 本示例直接继续抛出异常;* */@Overridepublic void checkValid(ResponseEntity wrapper) throws Exception {if( ResponseEntity.succ.equals(wrapper.getErrCode())){//正常} else {//业务异常记录日志CatClientContextHolder contextHolder = CatClientContextHolder.getContextHolder(); //CatClientContextHolder可以获取到本次http请求相关上下文对象,里面包含请求相关的各种参数CatClientLogger lastCatLog = contextHolder.getSendHandler().getHttpPoint().getLastCatLog();lastCatLog.setErrorMessge("[" + wrapper.getErrCode() + "]" + wrapper.getErrMsg());//业务异常,可以直接继续抛出,在公共的异常处理类中,统一处理throw new RuntimeException(lastCatLog.getErrorMessge());}}/*** 拆包装器,获取包装器类中的业务对象* */@Overridepublic Object getValue(ResponseEntity wrapper) {return wrapper.getData();}/*** 服务端成功之后加包装器类* */@Overridepublic ResponseEntity createEntryOnSuccess(Object value, Class methodReturnClass) {return ResponseEntity.ok(value);}/*** 服务端当发生异常时加包装器* */@Overridepublic ResponseEntity createEntryOnException(Throwable throwable, Class methodReturnClass) {throwable.printStackTrace();return ResponseEntity.fail("-1", throwable.getMessage() == null ? "NullPointerException" : throwable.getMessage());}
}
cat-server 模块
此模块也可以单独使用,但是更多是搭配feign-interface类型的客户端使用;具体的业务类实现feign-interface,业务类的方法便可以通过Http模式调用;
cat-server
为了支持自动加响应包装器类,以及整合自家的cat-client
客户端,因此做了看似很复杂的逻辑,但是如果看到文档最后catface
部分,就会发现很合理了。
1. feign-interface -----------------------┐↑ : │ : 3. asm增强interface│ :│ ↓│ 4. Enhancer-Interface│ ↑│ ││ ││ │ 5. 使用cglib│ │2. CatServer <═════════════════════╗ │ ║ │║ │6. cglib-controller <══════════ 7. http调用
- feign-interface: 包含@PostMapping、@GetMapping、@RequestBody、@RequestParam等注解的Interface接口;
- CatServer: 具体的业务类,实现了feign-interface,其类上加有
@CatServer
注解; - 使用asm对feign-interface增强处理:如果配置了加响应包装器类功能,修改interface的方法返回对象,统一为响应包装器类;如果使用了catface模式,将方法入参列表转成虚拟入参对象;并将feign-interface的、方法级、入参级注解,转移到新Interface上;
- Enhancer-Interface: 增强后的Interface,与原feign-interface没有任何结构上的关系;
- 使用cglib对Enhancer-Interface动态代理,生成Controller角色的类,并注册到spring容器;
- cglib-controller: 动态代理生成的Controller对象,持有CatServer实现类的引用(对象适配模式);
- http访问cglib-controller对象方法,cglib-controller预处理入参之后,再执行CatServer对应的方法;
Enhancer-Interface、cglib-controller 是自动成的类;Http请求指向cglib-controller,cglib-controller做预处理之后,再执行CatServer业务类的方法,看上去就好像Http请求是直接调用到CatServer业务类。待业务处理完毕之后,cglib-controller再判断是否需要添加响应包装器类,最终返回结果;
@EnableCatServer
这个注解表示启用CatServer服务端。该注解依赖于spring容器
,置于任何spring bean之上,例如:springboot项目启动类上、或者任意包含@Component
注解的类上;
- value: 扫描的包路径,处于这个目录中的feign-interface都会被注册成服务端。默认值是被标记类的同级目录;
- classes: 指定服务端class进行注册,优先度高于包路径;
- configuration: 生成服务端的一些配置项和默认值;
@CatServer
该注解用于定义某个feign-interface为服务端接口。包含:服务端别名、拦截器
、自定义标记、响应处理类;
//feign-interface,@CatMethod也可以换成@RequestMapping
@Api(tags = "Catface - 用户操作api")
@CatResponesWrapper(ResponseEntityWrapper.class)
public interface UserService {@ApiOperation("分页查询用户")@CatMethod(value = "/user/userPage")ResponseEntity<PageInfo<UserInfo>> userPage(@ModelAttribute("vi") UserPageVi vi);@ApiOperation("根据用户id查询用户信息")@CatMethod(value = "/user/get/{uid}", method = RequestMethod.GET, notes = @CatNote("user"))UserInfo userInfo(@PathVariable("uid") String uid);@ApiOperation("编辑用户")@CatMethod(value = "/user/save", method = RequestMethod.POST, notes = @CatNote(key = "name", value = "#{vi.name}"))ResponseEntity<Void> userSave(@RequestBody @CatNote("vi") UserSaveVi vi) throws Exception;@ApiOperation("设置用户状态")@CatMethod(value = "/user/status", method = RequestMethod.GET)void status(@RequestParam("uid") String userId, @RequestParam("status") String status);}
//服务端具体实现类
//此处可以添加事务注解,或其他AOP注解
@CatServer(interceptors = {UserInterceptor2.class, CatServerInterceptor.class, UserInterceptor.class, CatServerInterceptor.GroupOff.class}) //自定义拦截器 + 全局拦截器,无拦截器组
public class UserServiceImpl implements UserService{@Overridepublic ResponseEntity<PageInfo<UserInfo>> userPage(UserPageVi vi) {//具体实现return ResponseEntity.ok(page);}@Overridepublic UserInfo userInfo(String uid) {//具体实现return info;}@Overridepublic ResponseEntity<Void> userSave(UserSaveVi vi) {//具体实现return ResponseEntity.ok(null);}@Overridepublic void status(String userId, String status) {System.out.println("userSave >>> userId=" + userId + ", status=" + status);}
}
- value: 服务端组件的别名,默认首字母小写;
- resultHandler: 结果处理类,如果配置了加响应包装器类,在此处添加;默认值受CatServerConfiguration控制;
- tags: 自定义标记;用于拦截器组匹配;
- interceptors: 拦截器;cglib-controller调用CatServer过程中的拦截器链。其值有4种类型:
- 为空: 表示启用拦截器组,和全局默认拦截器;默认拦截器会被CatServerConfiguration#getServerInterceptor的返回对象替换;
- CatServerInterceptor.GroupOff.class: 关闭拦截器组;
- CatServerInterceptor.NoOp.class: 关闭自定义拦截器和全局默认拦截器;
- 其他值: 表示启用自定义拦截器;
拦截器规则:
1. 拦截器组
,是在运行中动态匹配,除非在interceptors中配置了CatServerInterceptor.GroupOff.class
,否则总是生效;
2. interceptors值如果为空、或者没有自定义拦截器
类型,则全局默认拦截器生效,可以使用CatServerInterceptor.NoOp.class
关闭这一功能;若存在任一一个自定义拦截器
类型,则会忽略全局默认拦截器;
3. 多个自定义拦截器,按配置的先后顺序执行;如果需要执行全局默认拦截器,可以使用CatServerInterceptor.class
占位;
:: @CatServer(): 启用拦截器组,和全局默认拦截器;
:: @CatServer(interceptors = {A.class, CatServerInterceptor.class}): 启用拦截器组、A拦截器、和全局默认拦截器。此处CatServerInterceptor.class表示全局拦截器的占位符,故A拦截器先于全局执行;
:: @CatServer(interceptors = {A.class}): 启用拦截器组,和自定义拦截器;
:: @CatServer(interceptors = {UserInterceptor.GroupOff.class}): 关闭拦截器组;仅全局默认拦截器有效;
:: @CatServer(interceptors = {UserInterceptor.NoOp.class, A.class}): 仅拦截器组有效,关闭全局拦截器和自定义拦截器;
:: @CatServer(interceptors = {CatServerInterceptor.class, A.class, UserInterceptor.GroupOff.class}): 关闭拦截器组;全局拦截器、A拦截器有效;
:: @CatServer(interceptors = {UserInterceptor.NoOp.class, UserInterceptor.GroupOff.class, A.class, B.class}): 关闭所有拦截器;
@CatBefore
配置入参处理器类;在执行业务类方法之前执行,用于验证、修改、打印方法入参;
CatServerConfiguration
生成服务端的一些配置项和默认值。配合@EnableCatServer使用,可用于修改@CatServer
、@CatResponesWrapper
注解的实际默认值;
- getWrapper: 加包装器类处理类,对应@CatResponesWrapper#value;
- getResultHandler: 默认的结果处理类;
- getServerInterceptor: 全局的默认拦截器;用于替换CatServer#interceptors中CatServerInterceptor.class位置;
- getInterceptorGroup: 拦截器组;在运行过程中动态匹配;
CatParameterResolver
参入预处理类,通过@CatBefore配置。在执行业务类方法之前执行,用于验证、修改、打印方法入参等;
CatResultHandler
feign-interface实现类的返回值处理类。配合响应包装器使用,可以将返回对象、异常转换成统一风格的响应;
CatServerInterceptor
在cglib-controller调用CatServer过程中的自定义拦截器;可以用于验证调用权限、必要缓存注入、记录输入输出入参日志等;
- CatServerInterceptor.NoOp.class: 特殊枚举类,关闭自定义拦截器和全局默认拦截器;
- CatServerInterceptor.GroupOff.class: 特殊枚举类,关闭拦截器组;
- preHandle: 某个拦截器可以被多个服务端引用,在执行拦截器内容之前执行,用于判断是否满足前置要求:
:: false 表示不满足,不执行拦截器内容;
:: true 表示满足,需要执行拦截器;
:: 抛出异常,默认情况下表示立刻结束拦截器链,不执行CatServer的对应方法;以上2种布尔返回值,仍然会执行CatServer方法! - postHandle: 拦截器内容;
CatInterceptorGroup
拦截器组,在运行过程中动态匹配,优先于自定义拦截器执行;如果服务端没有配置CatServerInterceptor.GroupOff.class,则总是执行;一般用于记录日志、验证权限使用;
- matcher: 匹配方法;在运行过程中,根据入参和调用的上下文进行匹配校验;
- getInterceptorFactory: 当匹配方法返回true后执行,返回满足该分组要求的拦截器集合;
- getOrder: 执行顺序,越小越先执行;
其他说明
cglib-controller在调用CatServer业务类过程中,以及CatServer业务类内部发生的异常,均可以通过CatResultHandler#onError统一处理。
但是对于Http请求cglib-controller过程中的异常(403、404、500、入参验证不通过)等,只能通过@ControllerAdvice
处理!
CatServer组件中内置了CatControllerAssist
异常处理类,可以通过cat-server.controller-assist.enable=false
关闭;
如果feign-interface的实现类,被其他类继承了,并且该子类上也存在@CatServer
注解,那么Http请求会指向子类的方法!
cat-face 模块
把cat-client和cat-server结合使用,好像就可以实现最开始提出的「客户端、服务端用过Interface耦合实现无感知调用」?答案是,也不完全是!是「如是」.jpg
客户端与服务端共享feign-interface、入参和返回对象的数据类型。其中客户端发起Http请求的url,是通过feign-interface方法上的@CatClient注解获取,服务端注册Controller的url,也是通过@CatClient注解获取,也就是说@CatClient#value()无论返回什么值,客户端总能找到对应的服务端!同样的,Http请求方式也是如此。
既然如此,何不固定请求方式为POST、url通过feign-interface的特征值自动生成,那岂不是可以省下@CatClient注解不用写了?
例如url生成规则:自定义命名空间 + feign-interface组件别名 + 方法名;
至于feign-interface方法入参,转Http请求报文这部分比较麻烦。POST请求只能传输form表单对象、或IO流,考虑到方法入参的复杂性,因此有2种转换方案:
-
在客户端,将方法的每个入参,都统一转成字符串,然后使用“入参名 = 字符串” 组成form表单对象。在服务端生成cglib-controller时,Http入参全部使用字符串接收,然后再逐个转成实际数据类型,并验证入参有效性;
-
在客户端,将方法的入参,组合成一个“入参名: 入参对象” 的Map,再将Map序列化成一个Json字符串;在服务端生成cglib-controller时,将原入参列表,转成一个虚拟的入参对象,入参对象的属性,就是原入参名;这样Http请求的Json,可以直接转成虚拟入参对象,并自动执行入参验证框架;
但是由于Interface在编译成class字节码之后,参数名会被擦除(可以使用@CatNote为参数取别名),实际上的参数名应该是:arg0、arg1、…、argX;故:
方案1示意:
UserInfo param8(@ApiParam("参数map") Map<String, Object> map,@ApiParam("参数vi1") @Valid UserPageVi vi1,@ApiParam("参数vi2") UserPageVi vi2,@ApiParam("参数status") @NotNull(message = "status 不能为空") @CatNote("status") Boolean status,@ApiParam("参数vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3);/*** 最后Http请求格式为* url:/feign-interface别名/param8* query:* arg0="{\"mapKey\":\"mapValue\"}"* arg1="{\"name\":\"vi1's name\"}"* arg2="{\"label\":\"vi2's label\"}"* arg3="false"* arg4={\"errCode\":\"1", \"data\":"{\"total\": \"12\", \"list\":"[{\"qname\":\"vi3\"}]"}"}"* */
方案2示例:
UserInfo param8(@ApiParam("参数map") Map<String, Object> map,@ApiParam("参数vi1") @Valid UserPageVi vi1,@ApiParam("参数vi2") UserPageVi vi2,@ApiParam("参数status") @NotNull(message = "status 不能为空") @CatNote("status") Boolean status,@ApiParam("参数vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3);/*** 最后服务端cglib-controller* url:/feign-interface别名/param8* class Virtual {* private Map<String, Object> arg0;* @Valid private UserPageVi vi1;* private UserPageVi vi2;* private Boolean status;* @Valid private ResponseEntity<PageInfo<UserPageVi>> arg4;* }* UserInfo param8(@Valid @RequestBody Virtual virtual);* */
第1种方案,实现起来比较容易。缺点是:记录入参日志时,入参全部是字符串,在打印的时候会出现引号转义;服务端Controller的入参都是字符串,swagger生成的接口文档没有详细的字段说明,不够友好;
第2种方案,实现起来比较麻烦。不存在方案1的缺点,但是由于在服务端生成一个虚拟的入参对象,因此在feign-interface中不能出现方法重载!
catface中主要使用方案2
转换参数;
@Catface
标记feign-interface为catface模式;表示将feign-interface中的所有方法都注册成客户端API,方法上、方法入参上可以没有任何注解!
在客户端,方法上的入参列表,会先转换成Map,Map键为arg0
~argX
按顺序自动生成,值为入参对象;然后再转换成Json字符串,POST + Json方式发起Http请求。请求的url为:配置的命名空间 + feign-interface别名 + 方法名,因此,这需要feign-interface中的方法名不能相同,即不能存在重载方法!
在服务端,会为每个方法自动生成一个虚拟入参对象,方法入参会转换成虚拟入参对象的属性;这样Http入参Json字符串,可以直接转换成方法入参对应的数据类型;
- value: feign-interface别名;默认是首字母小写。
- namespace: 命名空间;统一的url前缀,默认空;
最终生成的url为:[/命名空间]/feign-interface别名/方法名
@CatNotes
在catface模式下,为feign-interface方法添加自定义标记;
- value: 自定义标记;
- scope: 自定义标记适用范围:
:: All: 适用于客户端、服务端;
:: Cilent: 标记仅在作为客户端使用时生效;
:: Server: 标记仅在作为服务端使用时生效;
最后feign-interface可以简化成如下形式:
//@Api、@ApiOperation、@ApiParam是swagger框架的注解,如果没有这方面需求,可以删除;
//@NotBlank、@NotNull、@Valid、@Validated是springMVC验证框架注解;@Api(tags = "Catface - 精简模式")
@Catface
@CatResponesWrapper(ResponseEntityWrapper.class)
public interface FaceDemoService{UserInfo queryById(@NotBlank(message = "userId不能为空") String userId);@ApiOperation("api - param2")ResponseEntity<UserInfo> enable(String userId, Integer status);@CatNotes(value = {@CatNote(key = "uname", value = "#{user.name}")}, scope = CatNotes.Scope.Cilent)@CatNotes(value = {@CatNote(key = "uid", value = "#{user.id}")}, scope = CatNotes.Scope.Server)UserPageVi query(@CatNote("user") UserInfo vi);PageInfo<UserPageVi> queryByBean(String userId, UserInfo vi, @CatNote("isStatus") Boolean status);int param8(@ApiParam("参数map") Map<String, Object> map,@ApiParam("参数vi1") @Valid UserPageVi vi1,@ApiParam("参数vi2") UserPageVi vi2,@ApiParam("参数status") @NotNull(message = "status 不能为空") @CatNote("status") Boolean status,@ApiParam("参数vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3);default void dosomething(@ApiParam("参数map") Map<String, Object> map, @ApiParam("参数vi1") @Validated UserPageVi vi1,@ApiParam("参数date") Date date,@ApiParam("参数status") Integer status,@ApiParam("参数decimal") BigDecimal decimal,@ApiParam("参数vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3) {CatClientContextHolder holder = CatClientContextHolder.getContextHolder();Throwable exception = holder.getException();System.out.println("异常:" + exception.getMessage());return null;}
}
其他说明
除了feign-interface中方法不能重载,还要注意一点的是:如果在生产环境上迭代升级feign-interface,假设将FaceDemoService#dosomething方法入参有增减,无论是先更新客户端、还是先更新服务端,都会造成该API接口参数接收会错位!
一般这种情况,可以事先给入参取别名,这样在接收入参时,会根据参数名匹配,而不是参数顺序;或者采用面向对象开发,保持方法入参上只有一个入参对象,增减参数数量,转换成增减对象属性多少的问题。
相关文章:
catface,使用Interface定义Controller,实现基于Http协议的RPC调用
catface 前言cat-client 模块EnableCatClientCatClientCatMethodCatNoteCatResponesWrapperCatClientConfigurationCatClientProviderCatClientFactoryCatSendInterceptorCatHttpCatPayloadResolverCatObjectResolverCatLoggerProcessorCatResultProcessorCatSendProcessorAbst…...
Linux:LVS (NAT群集搭建)
模拟环境 外网入口服务器 外网 192.168.8.88 内网ip 192.168.254.4 web1 服务器 ip 192.168.254.1 网关: 192.168.254.4 web2 服务器 ip 192.168.254.2 网关: 192.168.254.4 共享存储服务器 ip 192.168.254.3 介绍 访问 外网192.16…...
音乐格式转换mp3怎么转?跟着步骤操作一遍
音乐格式转换mp3怎么转?mp3,一种音频数据压缩格式,由于其极具优势的文件尺寸小和高质量音效,自诞生之日起就占据了主流音乐格式的头把交椅,并且至今仍然受到用户的青睐,稳居音乐领域的霸主地位。在我们繁忙…...
it监控系统可以电脑吗?有什么效果
IT业务监控已经成为公司不可或缺的一部分,以确保业务的正常运行,提高企业的竞争能力。本文将详细介绍IT业务监控的必要性、实施方法以及如何选择合适的监控工具。 IT业务监控的必要性 确保业务稳定运行 IT业务监控可以实时检测公司的工作流程&#x…...
jvs-智能bi(自助式数据分析)9.1更新内容
jvs-智能bi更新功能 1.报表增加权限功能(服务、模板、数据集、数据源可进行后台权限分配) 每个报表可以独立设置权限,通过自定义分配,给不同的人员分配不同的权限。 2.报表新增执行模式 可选择首次报表加载数据为最新数据和历…...
MyBatis-Plus-扩展操作(3)
3.扩展 代码生成 逻辑删除 枚举处理器 json处理器 配置加密 分页插件 3.1 代码生成 https://blog.csdn.net/weixin_41957626/article/details/132651552 下载下面的插件 红色的是刚刚生成的。 我觉得不如官方的那个好用,唯一的好处就是勾选的选项能够看的懂得。…...
react 中 antd 的 样式和 tailwind 样式冲突
问题原因:在使用 tailwindcss 时,会导入大量的 tailwindcss 默认属性,而默认样式中 button, [typebutton] 包含了 background-color: transparent; 从而导致 antd Button 按钮背景色变成透明。解决办法:禁止 tailwindcss 的默认属…...
获取该虚拟机的所有权失败,主机上的某个应用程序正在使用该虚拟机
点击“openstack-controller”虚机 打开出现如下错误,点击“获取所有权” 点击“取消” 这时候不要删除虚拟机,这种错误一般是由于虚拟机没有正常关闭引起的。 找到openstack-controller的虚拟磁盘文件及配置文件存放的位置,删除openstack-…...
2024届校招-Java开发笔试题-S4卷
有三种题型:单项选择题(10道)、不定项选择题(10道)、编程题(3道) 下面是一些回忆的题目: 1.哪种设计模式将对象的创建与使用分离,通过工厂类创建对象 答:工…...
数据分析面试题(2023.09.08)
数据分析流程 总体分为四层:需求层、数据层、分析层和结论层 一、统计学问题 1、贝叶斯公式复述并解释应用场景 公式:P(A|B) P(B|A)*P(A) / P(B)应用场景:如搜索query纠错,设A为正确的词,B为输入的词,那…...
jenkins 报错fatal:could not read Username for ‘XXX‘:No such device or address
#原因:机器做迁移,或者断电,遇到突发情况 #解决: 一.排查HOME和USER环境变量 可以在项目执行shell脚本的时候echo $HOME和USER 也可以在构建记录位置点击compare environment 对比两次构建的环境变量 二.查看指定节点的git凭证 查…...
LRU算法之我见
文章目录 一、LRU算法是什么?二、使用原理三、代码实现总结 一、LRU算法是什么? LRU算法又称最近最少使用算法,它是是大部分操作系统为最大化页面命中率而广泛采用的一种页面置换算法。是一种缓存淘汰策略,根据使用频率来淘汰无用…...
【第20例】华为 IPD 体系 | IPD 的底层思考逻辑(限制版)
目录 简介 更新情况 IPD体系 CSDN学院 专栏目录 作者简介 简介 最近随着华为 Mate 60 系列的爆火发布。 这家差不多沉寂了 4 年的企业再次映入大众的眼帘。 其实,华为手机业务发展的元年最早可以追溯...
spaCy库的实体链接踩坑,以及spaCy-entity-linker的knowledge_base下载问题
问题1. spacy Can’t find factory for ‘entityLinker’ 1)问题 写了一个实体链接类,代码如下: nlp spacy.load("en_core_web_md")class entieyLink:def __init__(self, doc, nlp):self.nlp nlpself.doc self.nlp(doc)# Che…...
【数据结构】红黑树的插入与验证
文章目录 一、基本概念1.时代背景2. 基本概念3.基本性质 二、实现原理1. 插入1.1变色1.2旋转变色①左旋②右旋③右左双旋④左右双旋 2.验证 源码总结 一、基本概念 1.时代背景 1972年鲁道夫拜尔(Rudolf Bayer)发明了一种数据结构,这是一种特殊的B树4阶情况。这些树…...
Pycharm----将Anaconda建立的环境导入
首先打开项目设置,点击添加 随后点击现有环境,点击三个。。。号进行添加 最后找到你Anaconda安装文件夹,envs找到你建立的环境名称,找到python.exe将它导入即可让现在的python环境为你建立的环境,同时还需要更改终端方…...
数字花园的指南针:微信小程序排名的提升之道
微信小程序,是一片数字花园,其中各种各样的小程序竞相绽放,散发出各自独特的芬芳。在这个花园中,排名优化就像是精心照料花朵的园丁,让我们一同走进这个数字花园,探寻如何提升微信小程序的排名优化…...
LRU与LFU的c++实现
LRU 是时间维度上最少使用 维持一个链表,最近使用的放在表头 淘汰表尾 LFU 是实际使用频率的最少使用 每一个对应的频率维持一个链表, 淘汰最低频率的最后一个 1. LRU LRU(Least Recently Used,最近最少使用)是一种常…...
什么是Docker和Docker-Compose?
Docker的构成 Docker仓库:https://hub.docker.com Docker自身组件 Docker Client:Docker的客户端 Docker Server:Docker daemon的主要组成部分,接受用户通过Docker Client发出的请求,并按照相应的路由规则实现路由分发…...
三.listview或tableviw显示
一.使用qt creator 转变类型 变形为listview或tableviw 二.导出ui文件为py文件 # from123.py 为导出 py文件 form.ui 为 qt creator创造的 ui 文件 pyuic5 -o x:\xxx\from123.py form.uifrom123.py listview # -*- coding: utf-8 -*-# Form implementation generated fro…...
【算法】一文带你从浅至深入门dp动态规划
文章目录 一、前言二、动态规划理论基础1、基本概念2、动态规划五部曲【✔】3、出错了如何排查? 三、实战演练🗡0x00 斐波那契数0x01 第N个泰波那契数0x02 爬楼梯0x03 三步问题0x04 使用最小花费爬楼梯⭐解法一解法二 0x05 解码方法* 四、总结与提炼 一、…...
超简单免费转换ape到flac
1. 安装最新版的ffmpeg 2. 安装cywin环境 3. 设置path到ffmpeg export PATH$PATH:"PATH/TO/FFMPEG/BIN" 4.到ape所在的目录,执行以下命令 find . -iname "*.ape" | while read line; do fb${line::-4}; fn"$fb.flac";echo ffm…...
JavaScript混淆加密
什么是JS混淆加密? JavaScript混淆加密是一种通过对源代码进行变换,使其变得难以理解和分析的技术。它的目标是增加攻击者破解代码的难度,同时保持代码的功能不受影响。混淆加密的目的是使代码难以逆向工程,从而防止攻击者窃取知…...
Java8特性-Lambda表达式
📕概述 在Java 8中引入了Lambda表达式作为一项重要的语言特性,可以堪称是一种语法糖。Lambda表达式使得以函数式编程的方式解决问题变得更加简洁和便捷。 Lambda表达式的语法如下: (parameters) -> expression (参数) -> {代码}其中&…...
通过Power Platform自定义D365CE业务需求 - 1. Microsoft Power Apps 简介
Microsoft Power Apps是一个趋势性的、无代码和无代码的商业应用程序开发平台,配有一套应用程序、服务和连接器。其数据平台为构建适合任何业务需求的自定义业务应用程序提供了快速开发环境。随着无代码、少代码应用程序开发的引入,任何人都可以快速构建低代码应用程序,并与…...
简易实现QT中的virtualkeyboard及问题总结
文章目录 前言:一、虚拟键盘的实现综合代码 二、为什么选用QWidget而不适用QDialog实现键盘三、从窗体a拉起窗体b后,窗体b闪退问题的探讨四、关闭主窗口时子窗口未关闭的问题 前言: 本文章主要包含四部分: 虚拟键盘的实现&#…...
景联文科技可为多模态语音翻译模型提供数据采集支持
8月22日Facebook的母公司Meta Platforms发布了一种能够翻译和转录数十种语言的人工智能模型——SeamlessM4T,可以在日常生活中或者商务交流中为用户提供更便捷的翻译和转录服务。 相较于传统的文本翻译,这项技术的最大区别在于它可以实现端到端的语音翻译…...
定时器分批请求数据
<!DOCTYPE html> <html><script>//需要分页的数组let arr [1,2,3,4,5,6,7,8,9,10]//分割数组,每页3条splitArr(arr,4)/*** 分割数组*/function splitArr(idList,size){//当前页数let num 1//共多少页let count Math.ceil(idList.length / siz…...
【华为OD机试python】报数游戏【2023 B卷|100分】
【华为OD机试】-真题 !!点这里!! 【华为OD机试】真题考点分类 !!点这里 !! 题目描述 100个人围成一圈,每个人有一个编码,编号从1开始到100。 他们从1开始依次报数,报到为M的人自动退出圈圈,然后下一个人接着从1开始报数, 直到剩余的人数小于M。 请问最后剩余的人在原先…...
【深度学习实战—6】:基于Pytorch的血细胞图像分类(通用型图像分类程序)
✨博客主页:米开朗琪罗~🎈 ✨博主爱好:羽毛球🏸 ✨年轻人要:Living for the moment(活在当下)!💪 🏆推荐专栏:【图像处理】【千锤百炼Python】【深…...
h5制作方法/seo网站优化方案摘要
文章目录第一题、01-Number-sequence第二题.02-Number Sequence - Part 2第三题:NUMBER SEQUENCE – PART 3第四题:NUMBER SEQUENCE – PART 4第五题:第六题:17-templer-03-en1.lab2的内容为上次课堂练习的内容,把做过…...
将网站制作成app/比较好的网络推广平台
https://blog.csdn.net/ylforever/article/details/79191182 转载于:https://www.cnblogs.com/2016-cxp/p/10915529.html...
wordpress 显示微博内容/福州网站建设团队
os包是操作文件和目录的包。io的接口Reader,用于从数据源中读取数据并将数据转换成字节流,Writer接口字节流中读取数据,并将数据作为输出写入目标数据源。创建空文件结果:C:golangworking-with-files>go fmt example1.goC:gola…...
wordpress模板+bridge/企业网络营销目标
1. 工作机制 一个数据块在 DataNode 上以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是元数据包括数据块的长度,块数据的校验和,以及时间戳。DataNode 启动后向 NameNode 注册,通过后ÿ…...
wordpress自带图片大小/百度客户端官网
对于终端服务来说,打印始终是个头痛的问题,打印重定向失败后,在尝试了替换厂商通用驱动程序和微软自带驱动程序后,如果仍然无法打印,添加网络打印机或许成为我们的一根救命稻草。 具体来说,就是在服务器端添…...
漳州网站建设公司首选公司/服务器
1,计分板(scoreboard) 记分板用来动态预测设计的响应,施加给DUT的激励同时施加给记分板,记分板中的转换函数把输入的激励全部转换成最后响应的形式,并保存在数据结构中,以传递给检查器,记分板还…...