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的实现类:

  1. 精细化到每个接口的输入输出报文记录方案;
    比如某些API是核心流程,需要记录详细的输入输出报文,以便如果后续出现问题,可以查阅日志内容;而有些API不是那么重要,但调用又非常频繁,此时我们不希望打印它们,以免浪费系统性能、浪费存储空间;对于一般的API,平时的时候就记录一下调用记录,如果发生Http异常、或返回了业务异常,就记录详细的输入输出报文;

  2. 精细化到每个接口的Http等待时间:
    对于中台而言,调用后台接口,不可能无限等待后台接口响应;和日志一样的逻辑,有的API等待时间可以长点,有的又不行;

  3. 自动加、拆响应包装器类
    当服务端的所有响应,为统一的一个数据模型、具体的业务数据,是该模型通过泛型确认的属性时,例如:HttpEntityResponesDTO,称这种似于HttpEntityResponesDTO的数据结构为响应包装器类

    public class ResponesDTO<T> {
    	private String code; 	// code为1表示成功,非1表示失败
    	private String message;	//异常原因说明
    	private T data;			//业务数据
    }
    

    客户端获取到响应对象ResponesDTO之后:
    1. 需要先判断对象是否为null;
    2. 再判断code是否为1,不为1需要进入异常流程;
    3. 最后才能获取到业务数据User;

    catface支持自动拆响应包装器类。当启用后,客户端feign-interface的方法返回数据类型:
    :: 可以是ResponesDTO,保持原流程不变;
    :: 直接为User,而服务端无需做任何修改。当feign-interface的方法成功执行完并返回了User对象,表示Http调用成功,并且响应的code一定是1;如果调用失败、或者code不为1,会自动进入预先设置好的异常流程;

    加响应包装器类,针对服务端而言,是拆包装器类的逆操作。当服务端启用之后,feign-interface和与之对应的Controller方法响应的数据类型,可以直接是Usercatface会自动在最外层加上ResponesDTO

    另外,对于使用继承实现公共属性code、message和业务数据并列的情况,也同样适用于此功能;

  4. 自定义标记功能:
    在feign-interface上添加自定义标记、还可以为单个API接口添加标记。结合拦截器、springEL动态解析入参,便可以灵活实现各种各样的业务逻辑;

  5. 在服务端,通过继承实现API接口升级:
    在Java中,继承本身就可以增强父类功能。此特性也适用于catface,通过继承特性对服务端API接口升级增强,而客户端无需任何修改;

  6. 通过feign-interface生成的Controller,依旧支持swagger;

  7. 其他比较一般的功能:拦截器、修改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@CatPost2个便捷组合注解;


@CatNote

自定义标记注解;有2大类使用场景:

  1. 为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表达式,从方法入参对象中获取标记值。此种方法,必须要为入参取别名;

  1. 为方法入参取别名;一般配合@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的四个方法:

  1. executeConfigurationResolver: 处理http配置项前后;对应CatSendProcessor#doConfigurationResolver方法,处理Http请求相关配置:host、url、读取超时、请求方式、请求头参数、解析自定义标记;
  2. executeVariableResolver: 处理入参数据前后;对应CatSendProcessor#doVariableResolverCatSendProcessor#postVariableResolver2个方法:如果在调用远程API,需要额外处理参数、或添加签名等,可以在此处添加;
    :: doVariableResolver,入参转字符串、或表单对象;
    :: postVariableResolver,入参转换之后处理,这是给子类提供重写的方法;
  3. executeHttpSend: 发送Http请求前后;对应CatSendProcessor#postHttpSend方法;如果启用重连,那么该方法会执行多次!
  4. 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,业务数据类型为User,API接口返回的原始数据类型应该为ResponseDTO。对于使用拆包装器类的API接口方法,需要将方法的响应类(即业务数据类型),组装到包装器类中。
    public <T> CatTypeReference getWrapperType(Type type){
    	//type 为业务数据的类型,可以是User、List、Long、String[]等
    	//ResponseDTO是响应包装器类,type最终会替换掉T的位置,最终结果是ResponseDTO
    	//注意后面的一对花括号不能少!
        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,401400-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#patternJSONField#format修改格式;

3. 示例

/**
 * 定义一个客户端;
 * 远程服务器地址为:${core-server.remoteApi},需要从环境变量中获取;
 * 该客户端定义了一个拦截器:TokenInterceptor;
 * 单独配置了http链接、读取超时:3000ms;
 * 其他配置为默认值,参考CatClientConfiguration;
 * 并且该客户端,配置了自动拆包装器ResponseEntityWrapper,实际API接口返回数据类型为ResponseEntity;
 * 	  如果方法的返回类型不是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对象后,再返回;
     * */
    @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;
     *    先将原始数据转换ResponseEntity,再获取泛型属性对应值返回;
     * */
    @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;
     *    如果返回的是基础数据类型,对应的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 {

    /**
     * 使用拦截器修改参数
     * */
    @Override
    public void executeVariableResolver(CatClientContextHolder context, Intercepting intercepting) throws Exception {
        CatSendProcessor sendHandler = context.getSendHandler();
        sendHandler.setTracerId(String.valueOf(System.currentTimeMillis())); //设置日志id,可以通过日志id查询本次请求所有内容。如果不指定,自动使用uuid
        JSONObject 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 {
        @Override
        public 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;
 * 如果在客户端,则为拆包装器;
 * 如果在服务端,则为加包装器;
 *
 * @see AbstractResponesWrapper
 * @author bugcat
 * */
public class ResponseEntityWrapper extends AbstractResponesWrapper<ResponseEntity>{

    /**
     * 返回包装器类class
     * */
    @Override
    public Class<ResponseEntity> getWrapperClass() {
        return ResponseEntity.class;
    }

    /**
     * 组装包装器类中的实际泛型
     * */
    @Override
    public <T> CatTypeReference getWrapperType(Type type){
        return new CatTypeReference<ResponseEntity<T>>(type){};
    }

    /**
     * 拆包装器,并且自动校验业务是否成功?
     * 本示例直接继续抛出异常;
     * */
    @Override
    public 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());
        }
    }

    /**
     * 拆包装器,获取包装器类中的业务对象
     * */
    @Override
    public Object getValue(ResponseEntity wrapper) {
        return wrapper.getData();
    }

    /**
     * 服务端成功之后加包装器类
     * */
    @Override
    public ResponseEntity createEntryOnSuccess(Object value, Class methodReturnClass) {
        return ResponseEntity.ok(value);
    }

    /**
     * 服务端当发生异常时加包装器
     * */
    @Override
    public 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调用
  1. feign-interface: 包含@PostMapping、@GetMapping、@RequestBody、@RequestParam等注解的Interface接口;
  2. CatServer: 具体的业务类,实现了feign-interface,其类上加有@CatServer注解;
  3. 使用asm对feign-interface增强处理:如果配置了加响应包装器类功能,修改interface的方法返回对象,统一为响应包装器类;如果使用了catface模式,将方法入参列表转成虚拟入参对象;并将feign-interface的、方法级、入参级注解,转移到新Interface上;
  4. Enhancer-Interface: 增强后的Interface,与原feign-interface没有任何结构上的关系;
  5. 使用cglib对Enhancer-Interface动态代理,生成Controller角色的类,并注册到spring容器;
  6. cglib-controller: 动态代理生成的Controller对象,持有CatServer实现类的引用(对象适配模式)
  7. http访问cglib-controller对象方法,cglib-controller预处理入参之后,再执行CatServer对应的方法;

Enhancer-Interfacecglib-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{

    @Override
    public ResponseEntity<PageInfo<UserInfo>> userPage(UserPageVi vi) {
		//具体实现
        return ResponseEntity.ok(page);
    }

    @Override
    public UserInfo userInfo(String uid) {
        //具体实现
        return info;
    }

    @Override
    public ResponseEntity<Void> userSave(UserSaveVi vi) {
        //具体实现
        return ResponseEntity.ok(null);
    }

    @Override
    public void status(String userId, String status) {
        System.out.println("userSave >>> userId=" + userId + ", status=" + status);
    }
}
  • value: 服务端组件的别名,默认首字母小写;
  • resultHandler: 结果处理类,如果配置了加响应包装器类,在此处添加;默认值受CatServerConfiguration控制;
  • tags: 自定义标记;用于拦截器组匹配;
  • interceptors: 拦截器;cglib-controller调用CatServer过程中的拦截器链。其值有4种类型:
    1. 为空: 表示启用拦截器组,和全局默认拦截器;默认拦截器会被CatServerConfiguration#getServerInterceptor的返回对象替换;
    2. CatServerInterceptor.GroupOff.class: 关闭拦截器组;
    3. CatServerInterceptor.NoOp.class: 关闭自定义拦截器和全局默认拦截器;
    4. 其他值: 表示启用自定义拦截器;

      拦截器规则:
      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#interceptorsCatServerInterceptor.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种转换方案:

  1. 在客户端,将方法的每个入参,都统一转成字符串,然后使用“入参名 = 字符串” 组成form表单对象。在服务端生成cglib-controller时,Http入参全部使用字符串接收,然后再逐个转成实际数据类型,并验证入参有效性;

  2. 在客户端,将方法的入参,组合成一个“入参名: 入参对象” 的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 arg0;
	 *   @Valid private UserPageVi vi1;
	 *   private UserPageVi vi2;
	 *   private Boolean status;
	 *   @Valid private ResponseEntity> 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接口参数接收会错位!

一般这种情况,可以事先给入参取别名,这样在接收入参时,会根据参数名匹配,而不是参数顺序;或者采用面向对象开发,保持方法入参上只有一个入参对象,增减参数数量,转换成增减对象属性多少的问题。





你可能感兴趣的:(java,feign服务端,Http,RPC,feign,interface,catface,feignclient)