# 什么是Rest Proxy? Rest Proxy组件是本人在长期的系统对接工作中提炼出的一个可高度复用的组件,目前已经被同事们誉为“神器”的存在。这可能是你用过最爽的http对接神器,具体用起来有多爽呢?做过微服务的同学应该都知道`Feign Ref`模块吧,本组件就是类似那样的一个东东。 具体是这样的:**写一个interface,定义几个method,然后加上注解,完成!**,在业务里直接调用method即可,不需要写任何实现类! ## REST请求代理(基于Http Interface) 本组件基于SpringBoot2.7.7构建,支持完整的AutoConfiguraiton。仅需要极少量配置就可以完成配置。当前最新版本为`1.1.6`,新增了标准REST注解。 ### 特性列表 - 无感知自动注入,只需interface + 注解即可轻松完成对接 - 优雅的API,省去了您的学习烦恼 - 高性能实现,智能结果处理绑定 - 比Retrofit更简单的初始化配置,并且与Spring无缝集成 - 支持文件上传下载无感对接 - 自动解包结果包装类 - 自动转换参数、请求体 - 自定义鉴权 - 动态替换实现(施工中) ## 快速开始 您可以参考源码中`rest-proxy-core`的`test`模块查看测试用例,了解基本用法。 此外,我们提供了`example`项目,您可以直接参考其写法将相关逻辑迁移到您的SpringBoot项目。 具体请查看开源仓库:https://git.flyfish.group/flyfish-group/rest-proxy.git 然后demo的话请切换到`example`分支。 请使用maven引入依赖 ```xml group.flyfish rest-proxy-core 1.1.6 ``` ### 1. 定义请求源 我们提供了非常灵活的**请求源**(Base URL)定义方式。 请求源会在接口调用时作为前缀拼接到每个接口上,方便进行**路径复用**。 组件支持**全局请求源**和**多请求源**以及**注解指定请求源**三种方式,配置都非常的简单。 #### 1.1 全局请求源 ```yaml # application.yml rest: client: # 通过配置文件指定全局请求源,所有未指定请求源的RestService都会以该请求开头 base-url: https://mybase-source.domain ``` #### 1.2 多请求源 ```yaml # application.yml rest: client: urls: # 定义多个请求源,每个请求源有一个唯一的key,支持全限定url以及路径 baidu: https://ug.baidu.com yapi: http://yapi.flyfish.group/api ``` #### 1.3 注解指定请求源 我们支持Service服务(类)级别的请求源,以及Method(方法)级别的请求源指定。 **类请求源:** ```java @RestService(baseUrl = "https://ug.baidu.com") public interface TestService { @GET("/action") String getActionString(); } ``` **方法请求源** ```java @RestService public interface TestService { @GET(baseUrl = "https://ug.baidu.com", uri = "/action") String getActionString(); } ``` ### 2. 定义RestService请求服务类 我们提供了`@RestService`以及一系列的HTTP注解,帮助您快速定义好接口,无需书写任何的实现。 以下是一个基本的示例供您参考: ```java /** * 使用@RestService,如果不指定任何参数, * 默认会取得全局请求源,也就是rest.client.baseUrl的值 */ @RestService public interface TestService { /** * 通过关键字string作为query,查询用户列表 * @param keyword 关键字,会以?keyword={value}的方式传递 * @return 结果 */ @GET("/users") List getUsers(String keyword); /** * 保存用户 * @param user 请求体,使用@RestBody标记为请求体后,参数会自动转换 * @return 保存结果 */ @POST("/users") User saveUser(@RestBody User user); /** * 更新用户信息 * @param id 用户id路径参数。使用@RestPathParam注解结合花括号{}来标记路径参数 * @param user 用户数据 * @return 结果 */ @PUT("/users/{id}") User updateUser(@RestPathParam("id") Long id, @RestBody User user); /** * 更新用户的名称 * @param id 用户id路径参数 * @param name 用户名称,使用 mergeBody选项,将除了指定注解类型的其他参数合并到body中,如{name: ''} * @return 结果 */ @PATCH(value = "/users/{id}", mergedBody = true) User updateUser(@RestPathParam("id") Long id, String name); /** * 删除用户 * @param id 用户id * @return 结果 */ @DELETE("/users/{id}") User deleteUser(@RestPathParam("id") Long id); } ``` 如果您需要单独使用某个请求源,请参考: ```yaml # application.yml rest: client: urls: baidu: https://api.baidu.com other: http://other.com ``` ```java @RestService("baidu") public interface BaiduService { @POST("/id-cards") String updateIdCard(@RestBody IdCardDto idCard); } ``` 此外,我们还支持文件的上传和下载,请参考 ```java @RestService(baseUrl = "http://localhost:8999", timeout = 500) public interface TestRestService { /** * 上传一个预定义的文件,使用Multipart包装类,并传入?type=xxx以及一个字符串body:token=xxx * @param file 文件信息 * @param type 文件类型名称 * @param token 凭据 * @return 结果 */ @POST("/files") FileEntity uploadPart(@RestPart Multipart file, String type, @RestPart("token") Long token); /** * 使用input stream上传一个文件,此外还支持byte[],可以用@RestPart.Filename指定文件名 * @param file 文件 * @param name 文件名 * @return 结果 */ @POST("/files") FileEntity uploadAnno(@RestPart("fbl") InputStream file, @RestPart.Filename("fbl") String name); /** * 下载一个文件为byte[] * @return 下载结果 */ @GET("/files") byte[] downloadByte(); } ``` 调用时,只需要如下步骤 ```java @Service public class Test { @Resource private TestRestService testRestService; /** * 测试入口 */ @Test public void test() { // 使用file对象包装 File file = new File("/Users/wangyu/Desktop/2022年终述职报告.docx"); FileEntity result = testRestService.uploadPart(new Multipart("file", file.getName(), file), "docx", 55L); // 使用input stream try (InputStream in = Files.newInputStream(file.toPath())) { FileEntity entity = testRestService.uploadAnno(in, file.getName()); System.out.println(entity); } catch (IOException e) { throw new RuntimeException(e); } // 下载 testRestService.downloadByte(); } } ``` ### 3. 启用接口扫描 最后,我们在spring boot启动类上添加注解,就可以直接使用了! 这里需要手动指定一下要扫描的basePackages,组件不经过您的允许是不会扫描其他包的。 ```java package group.flyfish.demo; import group.flyfish.rest.annotation.EnableRestApiProxy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableRestApiProxy(basePackages = "group.flyfish.demo.service") public class RestProxyDemoApplication { public static void main(String[] args) { SpringApplication.run(RestProxyDemoApplication.class, args); } } ``` ### 4. 开始愉快的对接吧! 至此,我们的快速开始教程已经完成。 ## 结果映射处理和绑定 大家在实现接口的时候都心照不宣的都一个习惯,那就是将接口响应增加一个包装类,样子大概是这样的: ```json { "success": true, "code": 0, "message": "成功", "result": { "name": "名称", "code": "编码" } } ``` 但是我们在写Java代码时却不这么写。于是乎,这里就涉及到了一个**结果解包**的逻辑。 默认情况下,Rest代理器不会对结果进行解包。但是你可以添加`@AutoMapping`注解来标记自动解包。 这里我们提供了一个默认的数据结构用于解包的缺省行为,具体的格式就是**上面的例子**所定义的那样。 如果您要调用的系统不以该接口包装格式返回,需要您自己书写相应逻辑。您需要实现`RestResultMapping`接口并正确书写`map`和`resolve`方法。 `map`方法用于实现解包逻辑,也就是**如何从包装的结果中取得真实的数据**,比如上面的`result`字段。 此外,该方法内建议自行处理异常结果`code`,并主动抛出`RestClientException`。 `convert`方法用于将我们在`@RestService`中定义的`method`的返回值进行包装,保证返回真实的类型。 比如方法签名为:`User getUser()`,包装类为:`Result`,这时我们需要返回`Result`类型,可以使用我们提供的工具类`TypeResolveUtils` 中的`wrap`方法进行包装。具体可以参考下面的例子。 以下是系统内的默认实现逻辑,仅供参考: ```java /** * 默认缺省的结果映射 * * @author wangyu */ @Slf4j public class DefaultRestResultMapping implements RestResultMapping { /** * 模糊的结果映射 * * @param body 结果 * @return 映射后的结果 */ @Override @SuppressWarnings("unchecked") public T map(Object body) throws RestClientException { // 多一步类型检查,保证转换的结果类型正确 if (body instanceof RestResult) { RestResult result = (RestResult) body; if (result.isSuccess()) { return (T) result.getResult(); } log.error("【RestProxy】请求发生异常!状态码:{},时间:{},信息:{}", result.getCode(), DateUtils.formatDate(new Date(result.getTimestamp()), "yyyy-MM-dd HH:mm:ss"), result.getMessage()); throw new RestClientException(result.getMessage()); } return (T) body; } /** * 解析返回类型 * * @param resultType 返回类型 * @return 结果 */ @Override public Type resolve(Type resultType) { return TypeResolveUtils.wrap(resultType, RestResult.class); } } ``` 完成上面的步骤后,就可以使用`@AutoMapping`注解标记解包处理了! 最后实现的效果,就是从`TestService`到`TestUnwrapService`,如下: ```java @RestService public interface TestService { RestResult getUser(); } ``` ```java @RestService @AutoMapping(DefaultRestResultMapping.class) public interface TestUnwrapService { User getUser(); } ``` ## 定制您的rest客户端 我们提供了许多丰富的定制选项来帮助您更好的使用组件。 ### 1. 配置文件定制 以下是一个全量的配置参数说明: ```yaml # application.yml rest: client: # 总是信任ssl证书 always-trust: true # 全局连接超时时间 connection-timeout: 30s # 全局请求源 base-url: http://22.11.33.22:5001 # 多请求源配置,key: url urls: other: https://ug.baidu.com yapi: http://yapi.flyfish.group/api ``` ### 2. 配置类定制 我们还提供了很多配置类用于深度定制您的程序。 #### 2.1 配置自定义钩子 您可以在程序实现`RestPropertiesModifier`接口并注册为SpringBean来完成运行时对配置的修改。 一般使用场景是,我们的多请求源可能是从其他bean、数据库或者缓存读取出来的,所以需要通过代码来设置,如下: ```java public class UrlMappingAutoConfigure { /** * 从WorkflowProperties这个bean读取url配置并放入 * @param workflow 其他bean * @return 结果 */ @Bean public RestPropertiesModifier configureUrls(WorkflowProperties workflow) { return properties -> { // urls请求源map,可以直接操作 Map urls = properties.getUrls(); String baseUrl = workflow.getEngine().getBaseUrl(); String businessBaseUrl = workflow.getEngine().getBusinessBaseUrl(); String controlBaseUrl = workflow.getEngine().getControlBaseUrl(); // 配置基础路径集合 FlowableUrlMapping.URLS.forEach((key, url) -> urls.put(key, baseUrl + url)); FlowableUrlMapping.BUSINESS_URLS.forEach((key, url) -> urls.put(key, businessBaseUrl + url)); FlowableUrlMapping.CONTROL_URLS.forEach((key, url) -> urls.put(key, controlBaseUrl + url)); }; } } ``` #### 2.2 配置注入钩子 如果您的程序中需要在一些bean中注入rest客户端的参数类`RestClientProperties`,请不要直接使用@Resource。 请务必使用`PropertiesConfigurable`钩子来完成,否则会导致项目bean的依赖问题。 以下是框架内部的使用实例: ```java package group.flyfish.rest.core.factory; /** * 生产httpClient * * @author wangyu */ @Slf4j public final class HttpClientFactoryBean implements FactoryBean, PropertiesConfigurable { // 使用非公平锁 private final ReentrantLock lock = new ReentrantLock(); // 客户端实例,单例 private volatile CloseableHttpClient client; // 配置,配置没进来就不初始化 private RestClientProperties properties; // ...代码省略 @Override public Class getObjectType() { return CloseableHttpClient.class; } /** * 配置属性,完成初始化 * * @param properties 属性 */ @Override public void configure(RestClientProperties properties) { this.properties = properties; } } ``` #### 2.3 请求接口鉴权配置 请求鉴权在接口对接时是很重要的一环,我们依旧提供了灵活的方式支持 ##### 2.3.1 全局默认鉴权配置 您可以直接声明一个单例的`RestAuthProvider`实例来指定全局鉴权逻辑。 如果您声明了多个bean,则需要通过`@Primary`告诉具体的注入bean是哪个。 如下: ```java /** * 使用@Component注解注册为bean即可被自动配置 * * @author wangyu */ @Component public class YapiAuthProvider implements RestAuthProvider { // yapi控制token private static final String token = "e5172a42e62e0497b79e3c7df7b4ec1429399558f9d9d28c0152bd39ba4c217a"; /** * 通过入侵client提供鉴权 * yapi是使用query鉴权的,所以增加query即可 * * @param builder rest客户端构建器 */ @Override public void provide(RestClientBuilder builder) { // 支持添加认证头的方式,在此处也可以调用其他rest服务获取接口 // builder.addHeader("Authorization", "token") builder.addParam("token", token); } } ``` ##### 2.3.2 类级别指定鉴权 除了全局鉴权外,还支持类级别的鉴权,只需要您在`@RestService`注解中指定class即可。 ```java /** * yapi服务,支持鉴权 * * @author wangyu */ @RestService(value = "yapi", authProvider = YapiAuthProvider.class) public interface YapiService { } ``` ##### 2.4 类级别和方法级别的客户端配置 除了指定全局配置外,您还可以通过注解指定一些请求配置参数,请参考: ```java package group.flyfish.rest.annotation; import java.lang.annotation.*; /** * 标记服务为rest proxy * * @author wangyu */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RestService { /** * 通过标识符找到地址,不需要#开头 * * @return 结果 */ String value() default ""; /** * 服务级别的基本url,字典请使用#开头 * * @return 结果 */ String baseUrl() default ""; /** * 超时时间,-1则取默认,0表示无限 * * @return 结果 */ int timeout() default -1; /** * 鉴权提供者类 * * @return 具体实现了RestAuthProvider的类 */ Class authProvider() default Object.class; } ``` ```java package group.flyfish.rest.annotation; import group.flyfish.rest.enums.HttpMethod; import org.springframework.core.annotation.AliasFor; import java.lang.annotation.*; /** * 启用Rest请求的方法会自动代理实现, * 并封装返回值 * * @author wangyu */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RestApi { /** * uri的别名 * * @return 结果 */ @AliasFor("uri") String value() default ""; /** * 请求uri,使用次标注必须指定BaseUrl或者配置(现在还不支持) * * @return uri */ @AliasFor("value") String uri() default ""; /** * 请求方法 * * @return 结果 */ HttpMethod method() default HttpMethod.GET; /** * 多个参数时使用合并的body * * @return 结果 */ boolean mergedBody() default false; /** * 可选指定的url,不会从默认地址请求 * * @return url */ String url() default ""; /** * 基本路径,包含host * * @return baseUrl */ String baseUrl() default ""; /** * 是否带上认证token * * @return 结果 */ boolean credentials() default false; } ``` # 赞助和打赏 如果您觉得我的项目对您有帮助,请star或者请我喝杯☕️,感谢~ 同时欢迎各位小伙伴提交P/R,共同维护这个项目。 ![微信收款码](./walllet.jpg)