rest-proxy/README.md

606 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 什么是Rest Proxy?
Rest Proxy组件是本人在长期的系统对接工作中提炼出的一个可高度复用的组件目前已经被同事们誉为“神器”的存在。这可能是你用过最爽的http对接神器具体用起来有多爽呢做过微服务的同学应该都知道`Feign Ref`模块吧,本组件就是类似那样的一个东东。
具体是这样的:**写一个interface定义几个method然后加上注解完成**在业务里直接调用method即可不需要写任何实现类
## REST请求代理基于Http Interface
本组件基于SpringBoot2.7.7构建支持完整的AutoConfiguraiton。仅需要极少量配置就可以完成配置。当前最新版本为`1.1.5`新增了标准REST注解。
### 特性列表
- 无感知自动注入只需interface + 注解即可轻松完成对接
- 优雅的API省去了您的学习烦恼
- 高性能实现,智能结果处理绑定
- 比Retrofit更简单的初始化配置并且与Spring无缝集成
- 支持文件上传下载无感对接
- 自动解包结果包装类
- 自动转换参数、请求体
- 自定义鉴权
- 动态替换实现(施工中)
## 快速开始
您可以参考源码中`rest-proxy-core`的`test`模块查看测试用例,了解基本用法。
此外,我们提供了`example`项目您可以直接参考其写法将相关逻辑迁移到您的SpringBoot项目。
具体请查看开源仓库https://git.flyfish.group/flyfish-group/rest-proxy.git
然后demo的话请切换到`example`分支。
请使用maven引入依赖
```xml
<dependency>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy-core</artifactId>
<version>1.1.5</version>
</dependency>
```
### 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<User> 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以及一个字符串bodytoken=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<User>`类型,可以使用我们提供的工具类`TypeResolveUtils`
中的`wrap`方法进行包装。具体可以参考下面的例子。
以下是系统内的默认实现逻辑,仅供参考:
```java
/**
* 默认缺省的结果映射
*
* @author wangyu
*/
@Slf4j
public class DefaultRestResultMapping implements RestResultMapping {
/**
* 模糊的结果映射
*
* @param body 结果
* @return 映射后的结果
*/
@Override
@SuppressWarnings("unchecked")
public <T> 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<User> 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<String, String> 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<CloseableHttpClient>, 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)