Compare commits

..

No commits in common. "example" and "main" have entirely different histories.

99 changed files with 6096 additions and 738 deletions

23
.gitignore vendored
View File

@ -1,10 +1,20 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.idea
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
@ -13,12 +23,6 @@ target/
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
@ -31,3 +35,6 @@ build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

Binary file not shown.

View File

@ -1,2 +0,0 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

605
README.md Normal file
View File

@ -0,0 +1,605 @@
# 什么是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
<dependency>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy-core</artifactId>
<version>1.1.6</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)

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

316
mvnw vendored
View File

@ -1,316 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`\\unset -f command; \\command -v java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

188
mvnw.cmd vendored
View File

@ -1,188 +0,0 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

192
pom.xml
View File

@ -1,58 +1,188 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rest-proxy-demo</name>
<description>rest-proxy-demo</description>
<artifactId>rest-proxy</artifactId>
<version>1.2.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<commons-collection.version>4.4</commons-collection.version>
<commons.lang.version>2.6</commons.lang.version>
<sdk.version>1.2.0</sdk.version>
</properties>
<packaging>pom</packaging>
<modules>
<module>rest-proxy-api</module>
<module>rest-proxy-core</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy-core</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons-collection.version}</version>
</dependency>
<dependency>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy-api</artifactId>
<version>${sdk.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 许可证信息 -->
<licenses>
<!-- Apache许可证 -->
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<!-- SCM信息 -> git在gitee上托管 -->
<scm>
<url>https://git.flyfish.group/flyfish-group/rest-proxy.git</url>
</scm>
<!-- 开发人员信息 -->
<developers>
<developer>
<name>wangyu</name>
<email>wybaby168@gmail.com</email>
<organization>http://flyfish.group</organization>
<timezone>+8</timezone>
</developer>
</developers>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<profiles>
<profile>
<id>ossrh</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<show>package</show>
<doclint>none</doclint>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

21
rest-proxy-api/pom.xml Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy</artifactId>
<version>1.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rest-proxy-api</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,21 @@
package group.flyfish.rest.annotation;
import java.lang.annotation.*;
/**
* 自动解包意味着错误时自动抛出异常
*
* @author wangyu
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoMapping {
/**
* 结果映射器
*
* @return 结果
*/
Class<?> value() default Object.class;
}

View File

@ -0,0 +1,69 @@
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;
}

View File

@ -0,0 +1,14 @@
package group.flyfish.rest.annotation;
import java.lang.annotation.*;
/**
* rest请求体标记
*
* @author wangyu
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestBody {
}

View File

@ -0,0 +1,21 @@
package group.flyfish.rest.annotation;
import java.lang.annotation.*;
/**
* rest请求头标记
*
* @author wangyu
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestHeader {
/**
* 头的名称
*
* @return 结果
*/
String value() default "";
}

View File

@ -0,0 +1,21 @@
package group.flyfish.rest.annotation;
import java.lang.annotation.*;
/**
* 请求参数变量注解
*
* @author wangyu
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestParam {
/**
* 显式指定变量名防止类型名擦除
*
* @return 结果
*/
String value() default "";
}

View File

@ -0,0 +1,15 @@
package group.flyfish.rest.annotation;
import java.lang.annotation.*;
/**
* 注解一个map或者一个对象将所有值作为参数表传入
*
* @author wangyu
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestParams {
}

View File

@ -0,0 +1,37 @@
package group.flyfish.rest.annotation;
import java.lang.annotation.*;
/**
* rest请求体标记
*
* @author wangyu
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestPart {
/**
* 请求部分名称
*
* @return 传入multipart中key
*/
String value() default "file";
/**
* 绑定文件名称
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface Filename {
/**
* 请求部分名称
*
* @return 传入multipart中key
*/
String value() default "file";
}
}

View File

@ -0,0 +1,21 @@
package group.flyfish.rest.annotation;
import java.lang.annotation.*;
/**
* 路径变量注解
*
* @author wangyu
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestPathParam {
/**
* 显式指定变量名防止类型名擦除
*
* @return 结果
*/
String value() default "";
}

View File

@ -0,0 +1,42 @@
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;
}

View File

@ -0,0 +1,61 @@
package group.flyfish.rest.annotation.methods;
import group.flyfish.rest.annotation.RestApi;
import group.flyfish.rest.enums.HttpMethod;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 标准的DELETE声明
*
* @author wangyu
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestApi(method = HttpMethod.DELETE)
public @interface DELETE {
/**
* uri的别名
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
String value() default "";
/**
* 请求uri使用次标注必须指定BaseUrl或者配置现在还不支持
*
* @return uri
*/
@AliasFor(annotation = RestApi.class)
String uri() default "";
/**
* 可选指定的url不会从默认地址请求
*
* @return url
*/
@AliasFor(annotation = RestApi.class)
String url() default "";
/**
* 基本路径包含host
*
* @return baseUrl
*/
@AliasFor(annotation = RestApi.class)
String baseUrl() default "";
/**
* 是否带上认证token
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
boolean credentials() default false;
}

View File

@ -0,0 +1,60 @@
package group.flyfish.rest.annotation.methods;
import group.flyfish.rest.annotation.RestApi;
import group.flyfish.rest.enums.HttpMethod;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 标准的GET声明
* @author wangyu
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestApi(method = HttpMethod.GET)
public @interface GET {
/**
* uri的别名
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
String value() default "";
/**
* 请求uri使用次标注必须指定BaseUrl或者配置现在还不支持
*
* @return uri
*/
@AliasFor(annotation = RestApi.class)
String uri() default "";
/**
* 可选指定的url不会从默认地址请求
*
* @return url
*/
@AliasFor(annotation = RestApi.class)
String url() default "";
/**
* 基本路径包含host
*
* @return baseUrl
*/
@AliasFor(annotation = RestApi.class)
String baseUrl() default "";
/**
* 是否带上认证token
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
boolean credentials() default false;
}

View File

@ -0,0 +1,67 @@
package group.flyfish.rest.annotation.methods;
import group.flyfish.rest.annotation.RestApi;
import group.flyfish.rest.enums.HttpMethod;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 标准的GET声明
*
* @author wangyu
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestApi(method = HttpMethod.PATCH)
public @interface PATCH {
/**
* uri的别名
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
String value() default "";
/**
* 请求uri使用次标注必须指定BaseUrl或者配置现在还不支持
*
* @return uri
*/
@AliasFor(annotation = RestApi.class)
String uri() default "";
/**
* 多个参数时使用合并的body
*
* @return 结果
*/
boolean mergedBody() default false;
/**
* 可选指定的url不会从默认地址请求
*
* @return url
*/
@AliasFor(annotation = RestApi.class)
String url() default "";
/**
* 基本路径包含host
*
* @return baseUrl
*/
@AliasFor(annotation = RestApi.class)
String baseUrl() default "";
/**
* 是否带上认证token
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
boolean credentials() default false;
}

View File

@ -0,0 +1,67 @@
package group.flyfish.rest.annotation.methods;
import group.flyfish.rest.annotation.RestApi;
import group.flyfish.rest.enums.HttpMethod;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 标准的GET声明
*
* @author wangyu
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestApi(method = HttpMethod.POST)
public @interface POST {
/**
* uri的别名
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
String value() default "";
/**
* 请求uri使用次标注必须指定BaseUrl或者配置现在还不支持
*
* @return uri
*/
@AliasFor(annotation = RestApi.class)
String uri() default "";
/**
* 多个参数时使用合并的body
*
* @return 结果
*/
boolean mergedBody() default false;
/**
* 可选指定的url不会从默认地址请求
*
* @return url
*/
@AliasFor(annotation = RestApi.class)
String url() default "";
/**
* 基本路径包含host
*
* @return baseUrl
*/
@AliasFor(annotation = RestApi.class)
String baseUrl() default "";
/**
* 是否带上认证token
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
boolean credentials() default false;
}

View File

@ -0,0 +1,67 @@
package group.flyfish.rest.annotation.methods;
import group.flyfish.rest.annotation.RestApi;
import group.flyfish.rest.enums.HttpMethod;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 标准的GET声明
*
* @author wangyu
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestApi(method = HttpMethod.PUT)
public @interface PUT {
/**
* uri的别名
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
String value() default "";
/**
* 请求uri使用次标注必须指定BaseUrl或者配置现在还不支持
*
* @return uri
*/
@AliasFor(annotation = RestApi.class)
String uri() default "";
/**
* 多个参数时使用合并的body
*
* @return 结果
*/
boolean mergedBody() default false;
/**
* 可选指定的url不会从默认地址请求
*
* @return url
*/
@AliasFor(annotation = RestApi.class)
String url() default "";
/**
* 基本路径包含host
*
* @return baseUrl
*/
@AliasFor(annotation = RestApi.class)
String baseUrl() default "";
/**
* 是否带上认证token
*
* @return 结果
*/
@AliasFor(annotation = RestApi.class)
boolean credentials() default false;
}

View File

@ -0,0 +1,15 @@
package group.flyfish.rest.configuration;
/**
* rest客户端加载感知
*
* @author wangyu
*/
@FunctionalInterface
public interface RestLoadedAware {
/**
* 所有客户端完成加载
*/
void onClientsLoaded();
}

View File

@ -0,0 +1,22 @@
package group.flyfish.rest.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* 存储文件上传的part
*
* @author wangyu
*/
@Getter
@Setter
@AllArgsConstructor
public class Multipart {
private String name;
private String filename;
private Object data;
}

View File

@ -0,0 +1,9 @@
package group.flyfish.rest.enums;
/**
* Http请求类型
* @author wangyu
*/
public enum HttpMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}

View File

@ -0,0 +1,10 @@
package group.flyfish.rest.enums;
/**
* 响应类型
*
* @author wangyu
*/
public enum ResponseType {
NORMAL, TEXT, JSON, BINARY, OBJECT
}

View File

@ -0,0 +1,35 @@
package group.flyfish.rest.mapping;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
/**
* rest请求的结果映射
*
* @author wangyu
*/
public interface RestResultMapping {
/**
* 注册了的映射
*/
Map<Class<?>, RestResultMapping> MAPPINGS = new HashMap<>();
/**
* 模糊的结果映射
*
* @param result 结果
* @param <T> 泛型
* @return 映射后的结果
*/
<T> T map(Object result);
/**
* 解析返回类型
*
* @param resultType 返回类型
* @return 结果
*/
Type resolve(Type resultType);
}

82
rest-proxy-core/pom.xml Normal file
View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy</artifactId>
<version>1.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rest-proxy-core</artifactId>
<dependencies>
<dependency>
<groupId>group.flyfish</groupId>
<artifactId>rest-proxy-api</artifactId>
<version>${sdk.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
<excludes>
<exclude>dev/*</exclude>
<exclude>prod/*</exclude>
</excludes>
</testResource>
<testResource>
<directory>${project.basedir}/src/test/resources/${config.dir}</directory>
</testResource>
</testResources>
</build>
</project>

View File

@ -0,0 +1,25 @@
package group.flyfish.rest.annotation;
import group.flyfish.rest.configuration.RestClientConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* 启用restapi自动代理
*
* @author wangyu
*/
@Import(RestClientConfiguration.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableRestApiProxy {
/**
* 基本扫描路径
*
* @return 结果
*/
String[] basePackages() default "group.flyfish.rest";
}

View File

@ -0,0 +1,107 @@
package group.flyfish.rest.configuration;
import group.flyfish.rest.configuration.configure.RestObjectMapperProvider;
import group.flyfish.rest.core.factory.DefaultHttpClientProvider;
import group.flyfish.rest.core.factory.HttpClientFactoryBean;
import group.flyfish.rest.core.factory.HttpClientProvider;
import group.flyfish.rest.mapping.RestResultMapping;
import group.flyfish.rest.registry.RestApiRegistry;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolver;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolverComposite;
import group.flyfish.rest.registry.proxy.support.resolvers.*;
import group.flyfish.rest.utils.DataUtils;
import group.flyfish.rest.utils.JacksonUtil;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import java.util.Arrays;
import java.util.List;
/**
* rest请求相关配置
*
* @author wangyu
*/
public class RestClientConfiguration {
/**
* 实例化参数bean
*
* @return 结果
*/
@ConfigurationProperties(prefix = "rest.client", ignoreUnknownFields = false)
@Bean
public RestClientProperties restClientProperties() {
return new RestClientProperties();
}
/**
* http client工厂bean
*
* @return 结果
*/
@Bean
@ConditionalOnMissingBean(CloseableHttpClient.class)
public HttpClientFactoryBean httpClientFactoryBean() {
return new HttpClientFactoryBean();
}
/**
* 创建默认的提供者默认使用factory bean创建的client
*
* @return 结果
*/
@Bean
@ConditionalOnMissingBean
public HttpClientProvider httpClientProvider() {
return new DefaultHttpClientProvider();
}
/**
* 注册rest自动代理
*
* @return 结果
*/
@Bean
public RestApiRegistry restApiRegistry(RestArgumentResolverComposite composite, HttpClientProvider provider,
List<RestResultMapping> mappings) {
// 先注册映射们
if (DataUtils.isNotEmpty(mappings)) {
mappings.forEach(mapping -> RestResultMapping.MAPPINGS.put(mapping.getClass(), mapping));
}
// 最后实例化
return new RestApiRegistry(composite, provider);
}
/**
* 一个很重要的bean反向解析各种参数
*
* @return 结果
*/
@Bean
public RestArgumentResolverComposite restArgumentResolverComposite() {
List<RestArgumentResolver> resolvers = Arrays.asList(
new RestPathParamArgumentResolver(),
new RestPartArgumentResolver(),
new RestPartArgumentResolver.FilenameResolver(),
new RestBodyArgumentResolver(),
new RestHeaderArgumentResolver(),
new RestParamArgumentResolver()
);
return new RestArgumentResolverComposite(resolvers);
}
/**
* 更新原生的jackson object mapper
*
* @return 结果
*/
@Bean
public JacksonUtil restJacksonUtil(ObjectProvider<RestObjectMapperProvider> provider) {
provider.ifAvailable(bean -> JacksonUtil.setObjectMapper(bean.provide()));
return new JacksonUtil();
}
}

View File

@ -0,0 +1,106 @@
package group.flyfish.rest.configuration;
import group.flyfish.rest.configuration.configure.PropertiesConfigurable;
import group.flyfish.rest.configuration.modifier.RestPropertiesModifier;
import group.flyfish.rest.core.auth.RestAuthProvider;
import group.flyfish.rest.registry.proxy.RestInvokers;
import group.flyfish.rest.utils.DataUtils;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* 客户端的配置
*
* @author wangyu
* 整合入spring boot简化配置提高业务复用性
*/
@Data
@Slf4j
public class RestClientProperties implements InitializingBean {
/**
* 超时时间默认30s
*/
private Duration connectionTimeout = Duration.ofSeconds(30);
/**
* 基本url
*/
private String baseUrl;
/**
* ssl无条件新人
*/
private Boolean alwaysTrust = true;
/**
* 定义的内容字典可以支持动态取值使用#variable
*/
private Map<String, String> urls = new HashMap<>();
/**
* 默认的认证提供者
*/
private RestAuthProvider authProvider;
/**
* 修改器们
*/
@Getter(AccessLevel.NONE)
@Setter(onMethod_ = @Autowired)
private ObjectProvider<RestPropertiesModifier> modifiers;
/**
* 配置感知项
*/
@Getter(AccessLevel.NONE)
@Setter(onMethod_ = @Autowired)
private ObjectProvider<PropertiesConfigurable> configures;
/**
* 加载感知
*/
@Getter(AccessLevel.NONE)
@Setter(onMethod_ = @Autowired)
private ObjectProvider<RestLoadedAware> aware;
/**
* 获取字典url
*
* @param key
* @return 结果
*/
public String getDictUrl(String key) {
if (DataUtils.isEmpty(urls)) {
return null;
}
return urls.get(key);
}
@Autowired
public void setDefaultAuthProvider(ObjectProvider<RestAuthProvider> provider) {
this.authProvider = provider.getIfAvailable();
}
@Override
public void afterPropertiesSet() {
// 配置修改
modifiers.forEach(modifier -> modifier.modify(this));
// 服务初始化
RestInvokers.configure(this);
// 配置感知
configures.forEach(item -> item.configure(this));
// 加载感知
aware.forEach(RestLoadedAware::onClientsLoaded);
}
}

View File

@ -0,0 +1,18 @@
package group.flyfish.rest.configuration.configure;
import group.flyfish.rest.configuration.RestClientProperties;
/**
* 属性感知
*
* @author wangyu
*/
public interface PropertiesConfigurable {
/**
* 配置属性完成初始化
*
* @param properties 属性
*/
void configure(RestClientProperties properties);
}

View File

@ -0,0 +1,19 @@
package group.flyfish.rest.configuration.configure;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* object mapper配置器
*
* @author wangyu
*/
@FunctionalInterface
public interface RestObjectMapperProvider {
/**
* 提供项目级别的object mapper
*
* @return 结果
*/
ObjectMapper provide();
}

View File

@ -0,0 +1,18 @@
package group.flyfish.rest.configuration.modifier;
import group.flyfish.rest.configuration.RestClientProperties;
/**
* rest属性修改器
*
* @author wangyu
*/
public interface RestPropertiesModifier {
/**
* 修改属性发生在afterPropertiesSet之前
*
* @param properties 属性
*/
void modify(RestClientProperties properties);
}

View File

@ -0,0 +1,225 @@
package group.flyfish.rest.constants;
import group.flyfish.rest.core.ThreadPoolManager;
import group.flyfish.rest.core.builder.TypedMapBuilder;
import group.flyfish.rest.core.resolver.*;
import group.flyfish.rest.enums.HttpMethod;
import group.flyfish.rest.enums.ResponseType;
import org.apache.http.client.config.RequestConfig;
import java.util.Map;
import java.util.concurrent.ExecutorService;
/**
* rest客户端需要的常量都在这里
*
* @author wangyu
*/
public interface RestConstants {
/**
* 请求配置
*/
RequestConfig REQUEST_CONFIG = RequestConfig.custom().setConnectTimeout(3000).build();
/**
* 请求解析器
*/
Map<HttpMethod, HttpMethodResolver> RESOLVER_MAP = resolverBuilder()
.with(HttpMethod.GET, new HttpGetResolver())
.with(HttpMethod.POST, new HttpPostResolver())
.with(HttpMethod.PUT, new HttpPutResolver())
.with(HttpMethod.PATCH, new HttpPatchResolver())
.with(HttpMethod.DELETE, new HttpDeleteResolver())
.build();
/**
* 线程池
*/
ExecutorService DEFAULT_EXECUTOR = ThreadPoolManager.defaultCachedThreadPool();
/**
* 使用的MIME后缀映射
* 自定义的MIME TYPE可以过滤
*/
Map<String, String> MIME_MAP = TypedMapBuilder.stringMapBuilder()
.with("ai", "application/postscript")
.with("aif", "audio/x-aiff")
.with("aifc", "audio/x-aiff")
.with("aiff", "audio/x-aiff")
.with("asc", "text/plain")
.with("au", "audio/basic")
.with("avi", "video/x-msvideo")
.with("bcpio", "application/x-bcpio")
.with("bin", "application/octet-stream")
.with("c", "text/plain")
.with("cc", "text/plain")
.with("ccad", "application/clariscad")
.with("cdf", "application/x-netcdf")
.with("class", "application/octet-stream")
.with("cpio", "application/x-cpio")
.with("cpt", "application/mac-compactpro")
.with("csh", "application/x-csh")
.with("css", "text/css")
.with("dcr", "application/x-director")
.with("dir", "application/x-director")
.with("dms", "application/octet-stream")
.with("doc", "application/msword")
.with("drw", "application/drafting")
.with("dvi", "application/x-dvi")
.with("dwg", "application/acad")
.with("dxf", "application/dxf")
.with("dxr", "application/x-director")
.with("eps", "application/postscript")
.with("etx", "text/x-setext")
.with("exe", "application/octet-stream")
.with("ez", "application/andrew-inset")
.with("f", "text/plain")
.with("f90", "text/plain")
.with("fli", "video/x-fli")
.with("gif", "image/gif")
.with("gtar", "application/x-gtar")
.with("gz", "application/x-gzip")
.with("h", "text/plain")
.with("hdf", "application/x-hdf")
.with("hh", "text/plain")
.with("hqx", "application/mac-binhex40")
.with("htm", "text/html")
.with("html", "text/html")
.with("ice", "x-conference/x-cooltalk")
.with("ief", "image/ief")
.with("iges", "model/iges")
.with("igs", "model/iges")
.with("ips", "application/x-ipscript")
.with("ipx", "application/x-ipix")
.with("jpe", "image/jpeg")
.with("jpeg", "image/jpeg")
.with("jpg", "image/jpeg")
.with("js", "application/x-javascript")
.with("kar", "audio/midi")
.with("latex", "application/x-latex")
.with("lha", "application/octet-stream")
.with("lsp", "application/x-lisp")
.with("lzh", "application/octet-stream")
.with("m", "text/plain")
.with("man", "application/x-troff-man")
.with("me", "application/x-troff-me")
.with("mesh", "model/mesh")
.with("mid", "audio/midi")
.with("midi", "audio/midi")
.with("mif", "application/vnd.mif")
.with("mime", "www/mime")
.with("mov", "video/quicktime")
.with("movie", "video/x-sgi-movie")
.with("mp2", "audio/mpeg")
.with("mp3", "audio/mpeg")
.with("mp4", "video/mpeg")
.with("mpe", "video/mpeg")
.with("mpeg", "video/mpeg")
.with("mpg", "video/mpeg")
.with("mpga", "audio/mpeg")
.with("ms", "application/x-troff-ms")
.with("msh", "model/mesh")
.with("nc", "application/x-netcdf")
.with("oda", "application/oda")
.with("pbm", "image/x-portable-bitmap")
.with("pdb", "chemical/x-pdb")
.with("pdf", "application/pdf")
.with("pgm", "image/x-portable-graymap")
.with("pgn", "application/x-chess-pgn")
.with("png", "image/png")
.with("pnm", "image/x-portable-anymap")
.with("pot", "application/mspowerpoint")
.with("ppm", "image/x-portable-pixmap")
.with("pps", "application/mspowerpoint")
.with("ppt", "application/mspowerpoint")
.with("ppz", "application/mspowerpoint")
.with("pre", "application/x-freelance")
.with("prt", "application/pro_eng")
.with("ps", "application/postscript")
.with("qt", "video/quicktime")
.with("ra", "audio/x-realaudio")
.with("ram", "audio/x-pn-realaudio")
.with("ras", "image/cmu-raster")
.with("rgb", "image/x-rgb")
.with("rm", "audio/x-pn-realaudio")
.with("roff", "application/x-troff")
.with("rpm", "audio/x-pn-realaudio-plugin")
.with("rtf", "text/rtf")
.with("rtx", "text/richtext")
.with("scm", "application/x-lotusscreencam")
.with("set", "application/set")
.with("sgm", "text/sgml")
.with("sgml", "text/sgml")
.with("sh", "application/x-sh")
.with("shar", "application/x-shar")
.with("silo", "model/mesh")
.with("sit", "application/x-stuffit")
.with("skd", "application/x-koan")
.with("skm", "application/x-koan")
.with("skp", "application/x-koan")
.with("skt", "application/x-koan")
.with("smi", "application/smil")
.with("smil", "application/smil")
.with("snd", "audio/basic")
.with("sol", "application/solids")
.with("spl", "application/x-futuresplash")
.with("src", "application/x-wais-source")
.with("step", "application/STEP")
.with("stl", "application/SLA")
.with("stp", "application/STEP")
.with("sv4cpio", "application/x-sv4cpio")
.with("sv4crc", "application/x-sv4crc")
.with("swf", "application/x-shockwave-flash")
.with("t", "application/x-troff")
.with("tar", "application/x-tar")
.with("tcl", "application/x-tcl")
.with("tex", "application/x-tex")
.with("texi", "application/x-texinfo")
.with("texinfo", "application/x-texinfo")
.with("tif", "image/tiff")
.with("tiff", "image/tiff")
.with("tr", "application/x-troff")
.with("tsi", "audio/TSP-audio")
.with("tsp", "application/dsptype")
.with("tsv", "text/tab-separated-values")
.with("txt", "text/plain")
.with("unv", "application/i-deas")
.with("ustar", "application/x-ustar")
.with("vcd", "application/x-cdlink")
.with("vda", "application/vda")
.with("viv", "video/vnd.vivo")
.with("vivo", "video/vnd.vivo")
.with("vrml", "model/vrml")
.with("wav", "audio/x-wav")
.with("wrl", "model/vrml")
.with("xbm", "image/x-xbitmap")
.with("xlc", "application/vnd.ms-excel")
.with("xll", "application/vnd.ms-excel")
.with("xlm", "application/vnd.ms-excel")
.with("xls", "application/vnd.ms-excel")
.with("xlw", "application/vnd.ms-excel")
.with("xml", "text/xml")
.with("xpm", "image/x-xpixmap")
.with("xwd", "image/x-xwindowdump")
.with("xyz", "chemical/x-pdb")
.with("zip", "application/zip ")
.with("apk", "application/vnd.android.package-archive")
.with("*", "application/octet-stream")
.build();
static TypedMapBuilder<HttpMethod, HttpMethodResolver> resolverBuilder() {
return TypedMapBuilder.builder();
}
// 响应类型映射
Map<String, ResponseType> RESPONSE_TYPE_MAP = TypedMapBuilder.<String, ResponseType>builder()
.with(TypeConstants.STRING, ResponseType.TEXT)
.with(TypeConstants.BYTE_ARRAY, ResponseType.BINARY)
.build();
// 提示消息
String MSG_THREAD_POOL_EMPTY = "线程池未指定或为空!";
String MSG_IO_ERROR = "发起请求时出现异常!";
String MSG_UNKNOWN_HOST = "未知的请求地址!";
String MSG_TIME_OUT = "发起请求时服务端响应超时,请检查服务器连接!";
String MSG_REQUEST_ERROR = "请求接口{0}状态异常!代码:{1}";
}

View File

@ -0,0 +1,13 @@
package group.flyfish.rest.constants;
/**
* 类型常量
*
* @author wangyu
*/
public interface TypeConstants {
String STRING = "java.lang.String";
String BYTE_ARRAY = "[B";
}

View File

@ -0,0 +1,57 @@
package group.flyfish.rest.core;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 线程池管理
*
* @author wangyu
* 用于管理Http异步执行池
*/
public class ThreadPoolManager {
public static ExecutorService defaultCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
DefaultThreadFactory.createDefault());
}
/**
* 默认的线程工厂
*/
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
POOL_NUMBER.getAndIncrement() +
"-thread-";
}
private static DefaultThreadFactory createDefault() {
return new DefaultThreadFactory();
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
}

View File

@ -0,0 +1,18 @@
package group.flyfish.rest.core.auth;
import group.flyfish.rest.core.client.RestClientBuilder;
/**
* rest认证提供者
*
* @author wangyu
*/
public interface RestAuthProvider {
/**
* 通过入侵client提供鉴权
*
* @param builder rest客户端构建器
*/
void provide(RestClientBuilder builder);
}

View File

@ -0,0 +1,166 @@
package group.flyfish.rest.core.builder;
import group.flyfish.rest.utils.DataUtils;
import group.flyfish.rest.utils.JacksonUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Map参数构造器
*
* @author Mr.Wang
* @apiNote 通过重载过滤已知类型的空值并分别处理
*/
public final class MapParamBuilder {
private static final String PAGE_KEY = "pageUtil";
private static final String EMPTY_PATTERN = "{}";
private Map<String, Object> params;
private MapParamBuilder() {
this.params = new HashMap<>();
}
public static MapParamBuilder builder() {
return new MapParamBuilder();
}
public static MapParamBuilder of(Map<String, Object> initialParams) {
MapParamBuilder builder = new MapParamBuilder();
builder.withAll(initialParams);
return builder;
}
public static MapParamBuilder empty() {
MapParamBuilder builder = new MapParamBuilder();
builder.params = Collections.emptyMap();
return builder;
}
@SuppressWarnings("unchecked")
public static <T> T any(Map<String, Object> map, String key) {
Object value = map.get(key);
return (T) value;
}
public MapParamBuilder with(String key, Object value) {
if (DataUtils.isNotBlank(key) && value != null) {
this.params.put(key, value);
}
return this;
}
public boolean has(String key) {
return this.params.containsKey(key);
}
public MapParamBuilder with(String key, Collection<?> value, Collection<?> defaultValue) {
if (DataUtils.isNotBlank(key) && DataUtils.isNotEmpty(value)) {
this.params.put(key, value);
} else {
this.params.put(key, defaultValue);
}
return this;
}
public MapParamBuilder with(String key, String value) {
if (DataUtils.isNotBlank(key) && DataUtils.isNotBlank(value)) {
this.params.put(key, value);
}
return this;
}
public MapParamBuilder with(String key, Integer value) {
// 过滤负值无意义的值
if (DataUtils.isNotBlank(key) && value != null) {
this.params.put(key, value);
}
return this;
}
/**
* 交换键位对应的值
*
* @param oldKey 要被交换的key
* @param newKey 要交换的key
* @return 结果
*/
public MapParamBuilder exchange(String oldKey, String newKey) {
if (this.params.containsKey(oldKey) && this.params.containsKey(newKey)) {
Object oldValue = this.params.get(oldKey);
Object newValue = this.params.get(newKey);
this.params.put(oldKey, newValue);
this.params.put(newKey, oldValue);
}
return this;
}
/**
* 替换key为新的key值不变
*
* @param oldKey 旧的key
* @param newKey 新的key
* @return 结果
*/
public MapParamBuilder replace(String oldKey, String newKey) {
Object value = this.params.get(oldKey);
if (null != value) {
this.params.remove(oldKey);
this.params.put(newKey, value);
}
return this;
}
public MapParamBuilder clear(String key) {
this.params.remove(key);
return this;
}
public MapParamBuilder with(String key, Long value) {
// 过滤负值无意义的值
if (DataUtils.isNotBlank(key) && value != null) {
this.params.put(key, value);
}
return this;
}
public MapParamBuilder withAll(Map<String, ?> params) {
if (DataUtils.isNotEmpty(params)) {
params.forEach(this::with);
}
return this;
}
public MapParamBuilder withPage(Map<String, Object> params) {
if (params.containsKey(PAGE_KEY)) {
this.params.put("page", params.get(PAGE_KEY));
}
return this;
}
@SuppressWarnings("unchecked")
public <T> T take(String key) {
return (T) this.params.get(key);
}
public boolean isEmpty() {
return DataUtils.isEmpty(params);
}
public Map<String, Object> build() {
return this.params;
}
@Override
public String toString() {
if (DataUtils.isEmpty(this.params)) {
return EMPTY_PATTERN;
}
return JacksonUtil.toJson(this.params).orElse(null);
}
}

View File

@ -0,0 +1,54 @@
package group.flyfish.rest.core.builder;
import group.flyfish.rest.utils.JacksonUtil;
import java.util.HashMap;
import java.util.Map;
/**
* 有具体泛型类型的Map构建器
* 提供基本的非空校验
*
* @author Mr.Wang
*/
public final class TypedMapBuilder<K, V> {
private final Map<K, V> params;
private TypedMapBuilder() {
this.params = new HashMap<>();
}
public static <K, V> TypedMapBuilder<K, V> builder() {
return new TypedMapBuilder<>();
}
public static TypedMapBuilder<String, String> stringMapBuilder() {
return new TypedMapBuilder<>();
}
public static TypedMapBuilder<String, Object> stringObjectBuilder() {
return new TypedMapBuilder<>();
}
public TypedMapBuilder<K, V> with(K key, V value) {
if (key != null && value != null) {
this.params.put(key, value);
}
return this;
}
public TypedMapBuilder<K, V> withAll(Map<K, V> values) {
values.forEach(this::with);
return this;
}
public Map<K, V> build() {
return params;
}
@Override
public String toString() {
return JacksonUtil.toJson(this).orElse(null);
}
}

View File

@ -0,0 +1,356 @@
package group.flyfish.rest.core.client;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import group.flyfish.rest.constants.RestConstants;
import group.flyfish.rest.core.exception.RestClientException;
import group.flyfish.rest.core.factory.HttpClientFactoryBean;
import group.flyfish.rest.core.factory.HttpClientProvider;
import group.flyfish.rest.enums.ResponseType;
import group.flyfish.rest.utils.JacksonUtil;
import group.flyfish.rest.utils.RestLogUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import static group.flyfish.rest.constants.RestConstants.DEFAULT_EXECUTOR;
/**
* Rest请求客户端 Apache http实现
*
* @author Mr.Wang
* <p>
* @apiNote 1. 全builder调用用户系统内部相互通信
* 2. 支持异步回调
* 3. 多样性组合
* 4. 解耦实现
* 5. 支持上传文件FormDataJSON支持自定义无侵入扩展
* 6. 新增单例httpClient模式复用连接让应用更高效
*/
@Slf4j
final class DefaultRestClient extends RestErrorHandler implements RestClient {
private final HttpRequestBase request;
private boolean async = false;
private Consumer<HttpEntity> consumer;
private ExecutorService executorService;
private ResponseType responseType = ResponseType.NORMAL;
private Class<?> resultClass;
private TypeReference<?> typeReference;
private JavaType resultType;
private HttpClientProvider clientProvider;
/**
* 内部构造方法不对外公开
*
* @param request 请求信息
*/
DefaultRestClient(HttpRequestBase request, HttpClientProvider provider) {
this.request = request;
this.clientProvider = provider;
}
/**
* 内部构造方法不对外公开
*
* @param request 请求信息
*/
DefaultRestClient(HttpRequestBase request) {
this.request = request;
this.clientProvider = HttpClientFactoryBean::createSSLClient;
}
/**
* 设置请求失败时的回调
*
* @param errorConsumer 错误回调
* @return 结果
*/
@Override
public RestClient onError(Consumer<RestClientException> errorConsumer) {
this.errorConsumer = errorConsumer;
return this;
}
/**
* 设置响应类型
*
* @param responseType 响应类型
* @return 结果
*/
@Override
public RestClient responseType(ResponseType responseType) {
this.responseType = responseType;
return this;
}
/**
* 设置客户端提供者
*
* @param provider 客户端提供者
*/
@Override
public void setClient(HttpClientProvider provider) {
this.clientProvider = provider;
}
/**
* 标记线程池执行
*
* @return 结果
*/
@Override
public RestClient async() {
this.async = true;
this.executorService = DEFAULT_EXECUTOR;
return this;
}
/**
* 标记指定线程池执行
*
* @param executorService 线程池
* @return 结果
*/
@Override
public RestClient async(ExecutorService executorService) {
this.async = true;
this.executorService = executorService;
return this;
}
/**
* 异步执行接收结果
*
* @param consumer 结果
*/
@Override
public void execute(Consumer<HttpEntity> consumer) {
this.consumer = consumer;
if (this.async) {
if (this.executorService == null) {
handleError(RestConstants.MSG_THREAD_POOL_EMPTY);
}
this.executorService.submit(this::executeSafety);
} else {
executeSafety();
}
}
/**
* 静默执行抛弃全部异常
*/
@Override
public void executeSilent() {
executeSafety();
}
/**
* 执行请求返回Map
*
* @return map
* @throws IOException 异常
*/
@Override
public Map<String, Object> executeForMap() throws IOException {
this.responseType = ResponseType.JSON;
return innerExecute();
}
/**
* 执行请求返回字符串
*
* @return 字符串
* @throws IOException 异常
*/
@Override
public String executeForString() throws IOException {
this.responseType = ResponseType.TEXT;
return innerExecute();
}
/**
* 安全的执行
*/
private void executeSafety() {
try {
execute();
} catch (IOException e) {
handleError(RestConstants.MSG_IO_ERROR, e);
}
}
/**
* 执行并序列化该方法会安全的返回对象或者空
*
* @param clazz
* @param <T> 泛型
* @return 结果
*/
@Nullable
@Override
public <T> T execute(Class<T> clazz) {
this.responseType = resolveType(clazz);
this.resultClass = clazz;
try {
return innerExecute();
} catch (IOException e) {
log.error("请求时发生异常!", e);
}
return null;
}
/**
* 执行并序列化使用复杂的自动构造的类型
*
* @param type jackson的强化类型
* @param <T> 泛型
* @return 结果
*/
@Nullable
@Override
public <T> T execute(JavaType type) {
this.responseType = resolveType(type.getRawClass());
this.resultType = type;
try {
return innerExecute();
} catch (IOException e) {
log.error("请求时发生异常!", e);
}
return null;
}
/**
* 执行序列化使用类型引用
*
* @param typeReference jackson 类型引用
* @param <T> 泛型
* @return 序列化结果
*/
@Nullable
@Override
public <T> T execute(TypeReference<T> typeReference) {
this.responseType = ResponseType.OBJECT;
this.typeReference = typeReference;
try {
return innerExecute();
} catch (IOException e) {
log.error("请求时发生异常!", e);
}
return null;
}
/**
* 执行请求返回响应实体自行处理
*
* @return 响应实体
* @throws IOException 异常
*/
@Override
public <T> T execute() throws IOException {
return innerExecute();
}
/**
* 内部执行方法预处理结果
*
* @param <T> 泛型
* @return 结果
*/
private <T> T innerExecute() throws IOException {
try (CloseableHttpResponse response = clientProvider.getClient().execute(request)) {
RestLogUtils.log(response);
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if (HttpStatus.valueOf(statusLine.getStatusCode()).isError()) {
handleError(request.getURI(), statusLine.getStatusCode(), handleEntity(entity));
} else {
return handleEntity(entity);
}
} catch (UnknownHostException e) {
handleError(RestConstants.MSG_UNKNOWN_HOST, e);
} catch (ConnectTimeoutException e) {
handleError(RestConstants.MSG_TIME_OUT, e);
} finally {
request.releaseConnection();
}
return null;
}
/**
* 解析目标类型
*
* @param clazz 简单类型
* @return 响应类型
*/
private ResponseType resolveType(Class<?> clazz) {
return RestConstants.RESPONSE_TYPE_MAP.getOrDefault(clazz.getName(), ResponseType.OBJECT);
}
/**
* 处理响应体
*
* @param entity 响应体
* @return 结果
* @throws IOException 异常
*/
private <T> T handleEntity(HttpEntity entity) throws IOException {
if (null == entity) {
return null;
}
if (consumer != null) {
consumer.accept(entity);
return null;
}
return resolveResponse(entity);
}
/**
* 解析结果
*
* @param entity 响应体
* @param <T> 泛型
* @return 结果
*/
@SuppressWarnings("unchecked")
private <T> T resolveResponse(HttpEntity entity) throws IOException {
switch (responseType) {
case TEXT:
return (T) EntityUtils.toString(entity);
case JSON:
return (T) JacksonUtil.json2Map(EntityUtils.toString(entity));
case BINARY:
try (InputStream in = entity.getContent()) {
return (T) StreamUtils.copyToByteArray(in);
}
case OBJECT:
if (null != this.resultClass) {
return (T) JacksonUtil.fromJson(EntityUtils.toString(entity), this.resultClass);
}
if (null != this.typeReference) {
return (T) JacksonUtil.fromJson(EntityUtils.toString(entity), this.typeReference);
}
if (null != this.resultType) {
return (T) JacksonUtil.fromJson(EntityUtils.toString(entity), this.resultType);
}
default:
return (T) entity;
}
}
}

View File

@ -0,0 +1,142 @@
package group.flyfish.rest.core.client;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import group.flyfish.rest.core.exception.RestClientException;
import group.flyfish.rest.core.factory.HttpClientProvider;
import group.flyfish.rest.enums.ResponseType;
import org.apache.http.HttpEntity;
import org.springframework.lang.Nullable;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
/**
* Rest请求客户端
*
* @author Mr.Wang
* <p>
* @apiNote 1. 全builder调用用户系统内部相互通信
* 2. 支持异步回调
* 3. 多样性组合
* 4. 解耦实现
* 5. 支持上传文件FormDataJSON支持自定义无侵入扩展
* 6. 新增单例httpClient模式复用连接让应用更高效
*/
public interface RestClient {
/**
* 新增一个构建器
*
* @return 结果
*/
static RestClientBuilder create() {
return new RestClientBuilder();
}
/**
* 设置客户端提供者
*
* @param provider 客户端提供者
*/
void setClient(HttpClientProvider provider);
/**
* 标记线程池执行
*
* @return 结果
*/
RestClient async();
/**
* 标记指定线程池执行
*
* @param executorService 线程池
* @return 结果
*/
RestClient async(ExecutorService executorService);
/**
* 设置响应类型
*
* @param responseType 响应类型
* @return 结果
*/
RestClient responseType(ResponseType responseType);
/**
* 异步执行接收结果
*
* @param consumer 结果
*/
void execute(Consumer<HttpEntity> consumer);
/**
* 静默执行抛弃全部异常
*/
void executeSilent();
/**
* 执行请求返回Map
*
* @return map
* @throws IOException 异常
*/
Map<String, Object> executeForMap() throws IOException;
/**
* 执行请求返回字符串
*
* @return 字符串
* @throws IOException 异常
*/
String executeForString() throws IOException;
/**
* 执行并序列化该方法会安全的返回对象或者空
*
* @param clazz
* @param <T> 泛型
* @return 结果
*/
@Nullable
<T> T execute(Class<T> clazz);
/**
* 执行并序列化使用复杂的自动构造的类型
*
* @param type jackson的强化类型
* @param <T> 泛型
* @return 结果
*/
@Nullable
<T> T execute(JavaType type);
/**
* 执行序列化使用类型引用
*
* @param typeReference jackson 类型引用
* @param <T> 泛型
* @return 序列化结果
*/
@Nullable
<T> T execute(TypeReference<T> typeReference);
/**
* 执行请求返回响应实体自行处理
*
* @return 响应实体
* @throws IOException 异常
*/
<T> T execute() throws IOException;
/**
* 设置请求失败时的回调
*
* @param errorConsumer 错误回调
* @return 结果
*/
RestClient onError(Consumer<RestClientException> errorConsumer);
}

View File

@ -0,0 +1,209 @@
package group.flyfish.rest.core.client;
import group.flyfish.rest.entity.Multipart;
import group.flyfish.rest.enums.HttpMethod;
import group.flyfish.rest.utils.DataUtils;
import group.flyfish.rest.utils.JacksonUtil;
import group.flyfish.rest.utils.RequestContext;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpRequestBase;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static group.flyfish.rest.constants.RestConstants.REQUEST_CONFIG;
import static group.flyfish.rest.constants.RestConstants.RESOLVER_MAP;
/**
* 主要的builder核心构建
*
* @author wangyu
*/
public class RestClientBuilder {
private String url;
private HttpMethod method = HttpMethod.GET;
private Map<String, Object> params;
private String body;
private Map<String, String> headers;
private List<Multipart> multipartList;
private boolean multipart;
private boolean credential;
private String charset;
private RequestConfig config;
public String getUrl() {
return url;
}
public RestClientBuilder url(String url) {
this.url = url;
return this;
}
public HttpMethod getMethod() {
return method;
}
public RestClientBuilder method(HttpMethod method) {
this.method = method;
return this;
}
public RestClientBuilder get() {
this.method = HttpMethod.GET;
return this;
}
public RestClientBuilder post() {
this.method = HttpMethod.POST;
return this;
}
public RestClientBuilder multipart() {
this.multipart = true;
return this;
}
public boolean isMultipart() {
return multipart;
}
public Map<String, Object> getParams() {
if (null == params) {
params = new HashMap<>();
}
return params;
}
public RestClientBuilder queryParams(Map<String, Object> params) {
this.params = params;
return this;
}
public RestClientBuilder addParam(String key, Object value) {
if (null == this.params) {
this.params = new HashMap<>();
}
this.params.put(key, value);
return this;
}
public RestClientBuilder charset(String charset) {
this.charset = charset;
return this;
}
public RestClientBuilder withCredential() {
this.credential = true;
return this;
}
public RestClientBuilder config(RequestConfig config) {
this.config = config;
return this;
}
public Charset getCharset() {
return DataUtils.isBlank(charset) ? Charset.defaultCharset() : Charset.forName(charset);
}
public RestClientBuilder addMultipartBody(String name, String filename, Object data) {
return addMultipartBody(new Multipart(name, filename, data));
}
public RestClientBuilder addMultipartBody(Multipart part) {
if (null == this.multipartList) {
this.multipartList = new ArrayList<>();
}
this.multipartList.add(part);
return this;
}
public List<Multipart> getMultipartList() {
if (null == multipartList) {
multipartList = new ArrayList<>();
}
return multipartList;
}
public String getBody() {
return body;
}
public RestClientBuilder body(String body) {
this.body = body;
return this;
}
public RestClientBuilder body(Object body) {
this.body = JacksonUtil.toJson(body).orElse(null);
return this;
}
public Map<String, String> getHeaders() {
if (null == headers) {
headers = new HashMap<>();
}
return headers;
}
public RestClientBuilder headers(Map<String, String> headers) {
this.headers = headers;
return this;
}
public RestClientBuilder addHeader(String key, String value) {
if (null == this.headers) {
this.headers = new HashMap<>();
}
this.headers.put(key, value);
return this;
}
/**
* 匹配解析器
*
* @return 结果
*/
private HttpRequestBase buildRequest() {
HttpRequestBase request = RESOLVER_MAP.getOrDefault(this.method, RESOLVER_MAP.get(HttpMethod.GET))
.resolve(this);
// 添加token凭证
if (credential) {
RequestContext.getCredential().ifPresent(value -> this.addHeader(
RequestContext.AUTHORIZATION_KEY, value)
);
}
// 添加头
getHeaders().forEach(request::addHeader);
// 设置公共设置
request.setConfig(null == config ? REQUEST_CONFIG : config);
// 返回
return request;
}
/**
* 构建client
*
* @return 结果
*/
public RestClient build() {
// 创建请求
HttpRequestBase request = buildRequest();
return new DefaultRestClient(request);
}
}

View File

@ -0,0 +1,64 @@
package group.flyfish.rest.core.client;
import group.flyfish.rest.constants.RestConstants;
import group.flyfish.rest.core.exception.RestClientException;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
import java.text.MessageFormat;
import java.util.function.Consumer;
/**
* rest请求错误处理器
*
* @author wangyu
*/
@Slf4j
public abstract class RestErrorHandler {
protected Consumer<RestClientException> errorConsumer;
/**
* 错误处理
*
* @param e 异常
*/
private void handleError(RestClientException e) {
log.error(e.getMessage());
if (null != errorConsumer) {
errorConsumer.accept(e);
} else {
throw e;
}
}
/**
* 处理常规文本异常
*
* @param message 消息
*/
protected void handleError(String message) {
handleError(new RestClientException(message));
}
/**
* 处理具体发生的异常
*
* @param message 信息
* @param cause 造成的异常
*/
protected void handleError(String message, Exception cause) {
handleError(new RestClientException(message + cause.getMessage(), cause));
}
/**
* 处理请求异常
*
* @param status 状态码
* @param data 响应数据
*/
protected void handleError(URI uri, int status, Object data) {
String message = MessageFormat.format(RestConstants.MSG_REQUEST_ERROR, uri, status);
handleError(new RestClientException(message, status, data));
}
}

View File

@ -0,0 +1,38 @@
package group.flyfish.rest.core.exception;
import lombok.Getter;
/**
* 异常类用于包装异常
*/
public class RestClientException extends RuntimeException {
private static final long serialVersionUID = 4741281547788724661L;
@Getter
private Exception nested;
@Getter
private int statusCode;
private Object bind;
public RestClientException(String message, Exception nested) {
super(message);
this.nested = nested;
}
public RestClientException(String message, int statusCode, Object bind) {
super(message);
this.statusCode = statusCode;
this.bind = bind;
}
public RestClientException(String message) {
super(message);
}
@SuppressWarnings("unchecked")
public <T> T getBind() {
return (T) bind;
}
}

View File

@ -0,0 +1,45 @@
package group.flyfish.rest.core.factory;
import group.flyfish.rest.configuration.RestClientProperties;
import group.flyfish.rest.configuration.configure.PropertiesConfigurable;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
/**
* 默认的http客户端提供者
*
* @author wangyu
*/
public class DefaultHttpClientProvider implements HttpClientProvider, BeanFactoryAware, PropertiesConfigurable {
private CloseableHttpClient client;
private BeanFactory beanFactory;
/**
* 获取client可以自由替换
*
* @return 结果
*/
@Override
public CloseableHttpClient getClient() {
return client;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* 配置属性完成初始化
*
* @param properties 属性
*/
@Override
public void configure(RestClientProperties properties) {
this.client = beanFactory.getBean(CloseableHttpClient.class);
}
}

View File

@ -0,0 +1,90 @@
package group.flyfish.rest.core.factory;
import group.flyfish.rest.configuration.RestClientProperties;
import group.flyfish.rest.configuration.configure.PropertiesConfigurable;
import group.flyfish.rest.utils.DataUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.beans.factory.FactoryBean;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.locks.ReentrantLock;
/**
* 生产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;
/**
* 构建单例的httpClient
*
* @return 结果
*/
private CloseableHttpClient getClient() {
return DataUtils.isTrue(properties.getAlwaysTrust()) ? createSSLClient() : HttpClients.createDefault();
}
/**
* 不信任的证书请求客户端默认客户端
*
* @return 结果
*/
public static CloseableHttpClient createSSLClient() {
//信任所有
try {
SSLContext context = SSLContextBuilder.create().loadTrustMaterial(null, (arg0, arg1) -> true).build();
SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(context);
return HttpClients.custom().setSSLSocketFactory(factory).build();
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
e.printStackTrace();
}
return null;
}
@Override
public CloseableHttpClient getObject() throws Exception {
if (client == null) {
// 非公平锁二次判定定位volatile
lock.lock();
try {
if (client == null) {
client = getClient();
}
} finally {
lock.unlock();
}
}
return client;
}
@Override
public Class<?> getObjectType() {
return CloseableHttpClient.class;
}
/**
* 配置属性完成初始化
*
* @param properties 属性
*/
@Override
public void configure(RestClientProperties properties) {
this.properties = properties;
}
}

View File

@ -0,0 +1,19 @@
package group.flyfish.rest.core.factory;
import org.apache.http.impl.client.CloseableHttpClient;
/**
* http客户端提供者
*
* @author wangyu
*/
@FunctionalInterface
public interface HttpClientProvider {
/**
* 获取client可以自由替换
*
* @return 结果
*/
CloseableHttpClient getClient();
}

View File

@ -0,0 +1,25 @@
package group.flyfish.rest.core.resolver;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.core.resolver.support.AbstractParamResolver;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpRequestBase;
/**
* 删除请求的解析器
*
* @author wangyu
*/
public class HttpDeleteResolver extends AbstractParamResolver implements HttpMethodResolver {
/**
* 解析请求
*
* @param builder 构建器
* @return 结果
*/
@Override
public HttpRequestBase resolve(RestClientBuilder builder) {
return new HttpDelete(resolveParams(builder).getUrl());
}
}

View File

@ -0,0 +1,17 @@
package group.flyfish.rest.core.resolver;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.core.resolver.support.AbstractParamResolver;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
/**
* Get方法解析参数的解析器
*/
public class HttpGetResolver extends AbstractParamResolver implements HttpMethodResolver {
@Override
public HttpRequestBase resolve(RestClientBuilder builder) {
return new HttpGet(resolveParams(builder).getUrl());
}
}

View File

@ -0,0 +1,18 @@
package group.flyfish.rest.core.resolver;
import group.flyfish.rest.core.client.RestClientBuilder;
import org.apache.http.client.methods.HttpRequestBase;
/**
* Http请求解析器
*/
public interface HttpMethodResolver {
/**
* 解析请求
*
* @param builder 构建器
* @return 结果
*/
HttpRequestBase resolve(RestClientBuilder builder);
}

View File

@ -0,0 +1,27 @@
package group.flyfish.rest.core.resolver;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.core.resolver.support.AbstractBodyResolver;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpRequestBase;
/**
* patch请求的解析器
*
* @author wangyu
*/
public class HttpPatchResolver extends AbstractBodyResolver implements HttpMethodResolver {
/**
* 解析请求
*
* @param builder 构建器
* @return 结果
*/
@Override
public HttpRequestBase resolve(RestClientBuilder builder) {
HttpPatch httpPatch = new HttpPatch(builder.getUrl());
httpPatch.setEntity(buildEntity(builder));
return httpPatch;
}
}

View File

@ -0,0 +1,22 @@
package group.flyfish.rest.core.resolver;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.core.resolver.support.AbstractBodyResolver;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
/**
* Post方法解析参数的解析器包括上传
*/
public class HttpPostResolver extends AbstractBodyResolver implements HttpMethodResolver {
@Override
public HttpRequestBase resolve(RestClientBuilder builder) {
HttpEntity entity = buildEntity(builder);
HttpPost post = new HttpPost(builder.getUrl());
post.setEntity(entity);
return post;
}
}

View File

@ -0,0 +1,27 @@
package group.flyfish.rest.core.resolver;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.core.resolver.support.AbstractBodyResolver;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
/**
* put请求解析器
*
* @author wangyu
*/
public class HttpPutResolver extends AbstractBodyResolver implements HttpMethodResolver {
/**
* 解析请求
*
* @param builder 构建器
* @return 结果
*/
@Override
public HttpRequestBase resolve(RestClientBuilder builder) {
HttpPut post = new HttpPut(builder.getUrl());
post.setEntity(buildEntity(builder));
return post;
}
}

View File

@ -0,0 +1,119 @@
package group.flyfish.rest.core.resolver.support;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.utils.DataUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.message.BasicNameValuePair;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static group.flyfish.rest.constants.RestConstants.MIME_MAP;
/**
* 抽象的请求体解析
*
* @author wangyu
*/
public abstract class AbstractBodyResolver extends AbstractParamResolver {
/**
* 构建entity
* 对于纯urlencoded form我们将所有参数处理为formdata
* 对于上传和body请求我们照旧处理query
*
* @param builder 构建器
* @return 结果
*/
protected HttpEntity buildEntity(RestClientBuilder builder) {
return builder.isMultipart() ? buildMultipart(resolveParams(builder)) :
DataUtils.isNotBlank(builder.getBody()) ?
buildJson(resolveParams(builder)) :
buildFormData(builder);
}
/**
* 构建上传数据
*
* @param clientBuilder builder
* @return 结果
*/
private HttpEntity buildMultipart(RestClientBuilder clientBuilder) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
builder.setCharset(clientBuilder.getCharset());
clientBuilder.getMultipartList().forEach(multipart -> {
Object data = multipart.getData();
String name = multipart.getName();
String filename = multipart.getFilename();
if (data instanceof byte[]) {
builder.addBinaryBody(name, (byte[]) data, resolveType(filename), filename);
} else if (data instanceof File) {
builder.addBinaryBody(name, (File) data, resolveType(filename), filename);
} else if (data instanceof InputStream) {
builder.addBinaryBody(name, (InputStream) data, resolveType(filename), filename);
} else {
// 对于无法识别的内容统一处理为string
builder.addTextBody(name, String.valueOf(data));
}
});
// 处理query参数
return builder.build();
}
/**
* 构建JSON方式的POST
*
* @param clientBuilder builder
* @return 结果
*/
private HttpEntity buildJson(RestClientBuilder clientBuilder) {
clientBuilder.addHeader("Content-Type", "application/json;charset=UTF-8");
Charset charset = clientBuilder.getCharset();
StringEntity entity = new StringEntity(clientBuilder.getBody(), charset);
entity.setContentEncoding(charset.toString());
entity.setContentType("application/json");
return entity;
}
/**
* 构建formdata
*
* @param clientBuilder builder
* @return 结果
*/
private HttpEntity buildFormData(RestClientBuilder clientBuilder) {
// 设置参数
Map<String, Object> params = clientBuilder.getParams();
List<NameValuePair> list = params.keySet()
.stream()
.filter(key -> null != params.get(key))
.map(key -> new BasicNameValuePair(key, String.valueOf(params.get(key))))
.collect(Collectors.toList());
if (DataUtils.isNotEmpty(list)) {
return new UrlEncodedFormEntity(list, clientBuilder.getCharset());
}
return null;
}
/**
* 解析内容类型
*
* @param filename 文件名
* @return 结果
*/
private ContentType resolveType(String filename) {
return ContentType.create(MIME_MAP.getOrDefault(DataUtils.getExtension(filename),
ContentType.APPLICATION_OCTET_STREAM.getMimeType()));
}
}

View File

@ -0,0 +1,46 @@
package group.flyfish.rest.core.resolver.support;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.utils.DataUtils;
import java.util.stream.Collectors;
/**
* 抽象的参数解析逻辑
*
* @author wangyu
*/
public abstract class AbstractParamResolver {
/**
* 解析参数
*
* @param builder 构建器
*/
protected RestClientBuilder resolveParams(RestClientBuilder builder) {
if (DataUtils.isNotEmpty(builder.getParams())) {
String start = builder.getUrl().contains("?") ? "&" : "?";
String params = builder.getParams().entrySet().stream()
.map(entry -> entry.getKey() + "=" + valueOf(entry.getValue()))
.collect(Collectors.joining("&"));
builder.url(builder.getUrl() + start + params);
}
return builder;
}
/**
* 解析值
*
* @param value
* @return 结果
*/
private String valueOf(Object value) {
if (value instanceof String) {
return (String) value;
}
if (null != value) {
return String.valueOf(value);
}
return "";
}
}

View File

@ -0,0 +1,158 @@
package group.flyfish.rest.registry;
import group.flyfish.rest.annotation.EnableRestApiProxy;
import group.flyfish.rest.annotation.RestService;
import group.flyfish.rest.core.factory.HttpClientProvider;
import group.flyfish.rest.registry.proxy.RestProxyInvoker;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolverComposite;
import group.flyfish.rest.utils.DataUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.*;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* rest接口注册机
*
* @author wangyu
*/
@RequiredArgsConstructor
@Slf4j
public class RestApiRegistry implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
@Getter
private final RestArgumentResolverComposite composite;
@Getter
private final HttpClientProvider provider;
// bean工厂
private ConfigurableListableBeanFactory beanFactory;
/**
* 动态注册bean
*
* @param registry 注册机
* @throws BeansException 异常
*/
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 包名
List<String> packageNames = new ArrayList<>();
// 找基本包找不到立马报错
beanFactory.getBeansWithAnnotation(EnableRestApiProxy.class)
.forEach((key, value) -> {
EnableRestApiProxy proxy = AnnotationUtils.findAnnotation(value.getClass(), EnableRestApiProxy.class);
if (null == proxy) return;
for (String basePackage : proxy.basePackages()) {
if (DataUtils.isNotBlank(basePackage)) {
packageNames.add(basePackage);
}
}
});
// 不为空时查找
if (DataUtils.isNotEmpty(packageNames)) {
// 初始化反射
try {
RestServiceComponentProvider scanner = new RestServiceComponentProvider();
// 获取扫描器的ClassLoader保证同源
ClassLoader cl = scanner.getClass().getClassLoader();
for (String packageName : packageNames) {
Set<BeanDefinition> bfs = scanner.findCandidateComponents(packageName);
// 不存在不要浪费性能
if (CollectionUtils.isEmpty(bfs)) return;
// 代理并生成子类并注册到ioc容器
bfs.stream()
.map(bf -> resolveType(bf, cl))
.filter(Objects::nonNull)
.forEach(clazz -> registry.registerBeanDefinition(clazz.getName(), generate(clazz)));
}
} catch (IllegalStateException e) {
log.error("初始化Rest映射时出错", e);
}
return;
}
throw new BeanDefinitionValidationException("【RestApi】EnableRestApiProxy注解必须指定有效的basePackage!");
}
/**
* 生成bean定义
*
* @param clazz 目标类型
* @return bean定义
*/
private BeanDefinition generate(Class<?> clazz) {
return BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> RestProxyInvoker.produce(clazz, this))
.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE).getRawBeanDefinition();
}
@SneakyThrows
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// do nothing
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isTrue(beanFactory instanceof ConfigurableListableBeanFactory, "当前bean factory不被支持");
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
private Class<?> resolveType(BeanDefinition bf, ClassLoader cl) {
if (null != bf.getBeanClassName()) {
try {
return ClassUtils.forName(bf.getBeanClassName(), cl);
} catch (ClassNotFoundException e) {
return null;
}
}
return null;
}
/**
* 内部的包扫描器提供特定注解扫描
*
* @author wangyu
*/
private static class RestServiceComponentProvider extends ClassPathScanningCandidateComponentProvider {
private final AnnotationTypeFilter filter = new AnnotationTypeFilter(RestService.class);
private RestServiceComponentProvider() {
super(false);
resetFilters(false);
addIncludeFilter(filter);
}
@Override
protected boolean isCandidateComponent(@NonNull MetadataReader metadataReader) throws IOException {
return filter.match(metadataReader, getMetadataReaderFactory());
}
@Override
protected boolean isCandidateComponent(@NonNull AnnotatedBeanDefinition beanDefinition) {
return true;
}
}
}

View File

@ -0,0 +1,12 @@
package group.flyfish.rest.registry;
/**
* 用于标记rest服务代理
*
* @author wangyu
* @deprecated 该类已经过时请使用新版的注解声明
* @see group.flyfish.rest.annotation.RestService
*/
@Deprecated
public interface RestService {
}

View File

@ -0,0 +1,33 @@
package group.flyfish.rest.registry.proxy;
import group.flyfish.rest.configuration.RestClientProperties;
import java.util.ArrayList;
import java.util.List;
/**
* rest执行器实例集合
*
* @author wangyu
*/
public class RestInvokers {
private static RestClientProperties properties;
private static List<RestProxyInvoker> invokers = new ArrayList<>();
public static void add(RestProxyInvoker invoker) {
if (null != properties) {
invoker.configure(properties);
} else {
invokers.add(invoker);
}
}
public static synchronized void configure(RestClientProperties properties) {
invokers.forEach(invoker -> invoker.configure(properties));
invokers.clear();
invokers = null;
RestInvokers.properties = properties;
}
}

View File

@ -0,0 +1,281 @@
package group.flyfish.rest.registry.proxy;
import com.fasterxml.jackson.databind.JavaType;
import group.flyfish.rest.annotation.AutoMapping;
import group.flyfish.rest.annotation.RestService;
import group.flyfish.rest.configuration.RestClientProperties;
import group.flyfish.rest.configuration.configure.PropertiesConfigurable;
import group.flyfish.rest.core.auth.RestAuthProvider;
import group.flyfish.rest.core.client.RestClient;
import group.flyfish.rest.core.client.RestClientBuilder;
import group.flyfish.rest.mapping.RestResultMapping;
import group.flyfish.rest.registry.RestApiRegistry;
import group.flyfish.rest.registry.proxy.entity.RestMethod;
import group.flyfish.rest.registry.proxy.support.ArgumentResolveContext;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolverComposite;
import group.flyfish.rest.registry.proxy.support.UrlCompiler;
import group.flyfish.rest.registry.wrapper.DefaultRestResultMapping;
import group.flyfish.rest.utils.DataUtils;
import group.flyfish.rest.utils.JacksonUtil;
import group.flyfish.rest.utils.RestLogUtils;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Rest代理执行器
*
* @author wangyu
*/
@Slf4j
public class RestProxyInvoker implements InvocationHandler, PropertiesConfigurable {
// 方法缓存
private final Map<Integer, RestMethod> methods = new ConcurrentHashMap<>();
// 要代理的目标类
private final Class<?> targetType;
// 服务映射
private final RestService restService;
// 配置属性
@Getter
private RestClientProperties properties;
// 初始的基本路径
@Getter
private String baseUrl;
// 超时时间
private RequestConfig config;
// 注册器包含基础信息
private final RestApiRegistry registry;
// 结果映射
private RestResultMapping mapping;
// 鉴权提供者
private RestAuthProvider authProvider;
/**
* 构造器
*
* @param targetType 目标类型
* @param registry 注册器
*/
private RestProxyInvoker(Class<?> targetType, RestApiRegistry registry) {
this.targetType = targetType;
this.registry = registry;
// 注解的优先级高于全局基本路径
this.restService = AnnotationUtils.findAnnotation(targetType, RestService.class);
Assert.notNull(restService, "当前类尚未添加@RestService注解");
}
/**
* 生产一个实现类
*
* @param target 目标
* @param <T> 泛型
* @return 结果
*/
public static <T> T produce(Class<?> target, RestApiRegistry registry) {
RestProxyInvoker invoker = new RestProxyInvoker(target, registry);
RestInvokers.add(invoker);
return DataUtils.cast(Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, invoker));
}
/**
* 完成配置
*
* @param properties 属性
*/
@Override
public void configure(RestClientProperties properties) {
this.properties = properties;
this.config = RequestConfig.custom().setConnectTimeout((int) properties.getConnectionTimeout().toMillis()).setSocketTimeout(restService.timeout()).build();
this.baseUrl = this.findBaseUrl();
this.authProvider = determineAuthProvider();
}
/**
* 执行rest请求的地方这里很简单易懂
*
* @param proxy 代理对象
* @param target 代理方法
* @param args 参数
* @return 结果
* @throws Throwable 可能抛出的异常
*/
@Override
public Object invoke(Object proxy, Method target, Object[] args) throws Throwable {
// 解析方法做基本验证
RestMethod method = methods.computeIfAbsent(target.hashCode(), k -> RestMethod.resolve(target, this));
if (method.isInvalid()) {
throw new IllegalAccessException("【Rest调用】未声明rest配置的方法被调用请检查代码");
}
RestArgumentResolverComposite composite = registry.getComposite();
// 第一步就解析参数
ArgumentResolveContext context = composite.resolve(method, args);
// 构造和调用这里的restClient不保存状态
RestClientBuilder builder = RestClient.create().url(resolveUrl(method.getUrl(), context))
.method(method.getMethod())
.config(config);
// 需要带cookie的带上
if (method.isCredentials()) {
builder.withCredential();
}
// 判断情况赋值参数
if (context.hasBody()) {
builder.body(context.getBody());
}
// 赋值文件体
if (context.hasMultipart()) {
builder.multipart();
context.getFiles().forEach((key, value) -> {
value.setFilename(context.getFilename(key, value.getFilename()));
builder.addMultipartBody(value);
});
}
// 赋值参数们
if (context.hasParams()) {
builder.queryParams(context.getParam());
}
// 赋值头
if (context.hasHeaders()) {
builder.headers(context.getHeaders());
}
// 添加鉴权信息
if (null != authProvider) {
authProvider.provide(builder);
}
// 构建客户端
RestClient client = builder.build();
// 设置客户端
client.setClient(registry.getProvider());
// 是否对结果进行映射
boolean map = null != mapping && !method.isBare();
// 打印请求
RestLogUtils.log(builder);
// 执行请求
Object result = execute(client, method, map);
// 打印结果
RestLogUtils.log(result);
// 结果映射
return map ? mapping.map(result) : result;
}
/**
* 最终执行的方法
*
* @param client rest客户端实例
* @param method 原方法实例
* @param map 是否映射结果
* @return 结果
*/
private Object execute(RestClient client, RestMethod method, boolean map) throws IOException {
// 构建带泛型的返回值自动判断是否是简单类型
JavaType constructed = JacksonUtil.getMapper().constructType(method.getGenericReturnType());
// 特殊处理映射
if (map) {
Type resolved = mapping.resolve(constructed);
// 返回java泛型
if (resolved instanceof JavaType) {
return client.execute((JavaType) resolved);
} else if (resolved instanceof Class) {
// 简单类型
return client.execute((Class<?>) resolved);
}
}
// 优先构建对于map来说只支持模糊map否则可能会报错直接返回
if (ClassUtils.isAssignable(Map.class, method.getReturnType())) {
return client.executeForMap();
}
// 不是map直接返回构建结果类型
return client.execute(constructed);
}
/**
* 找到配置固化的基本url
*/
private String findBaseUrl() {
// 当且仅当存在时进入
if (null != restService) {
// 注解的路径解析
String key = restService.value();
String baseUrl = restService.baseUrl();
// 更友好的处理解包
AutoMapping autoMapping = AnnotationUtils.findAnnotation(targetType, AutoMapping.class);
if (null != autoMapping) {
// 找到返回值处理器
Class<?> clazz = autoMapping.value();
// 处理返回值
if (Object.class.equals(clazz)) {
this.mapping = DefaultRestResultMapping.getInstance();
} else {
this.mapping = RestResultMapping.MAPPINGS.get(clazz);
}
}
// 当key不为空解析字典路径
if (DataUtils.isNotBlank(key)) {
return properties.getDictUrl(key);
} else if (DataUtils.isNotBlank(baseUrl)) {
return baseUrl;
}
}
// 解包生效以标准模式解包
return properties.getBaseUrl();
}
/**
* 决定基本url优先级 方法注解url > 方法注解baseUrl + uri > 全局配置 + uri
*
* @return 结果
*/
private String resolveUrl(String url, ArgumentResolveContext context) {
// 尝试解析路径参数
return context.hasPathParams() ? UrlCompiler.compile(url, context.getPathParams()) : url;
}
/**
* 确定鉴权提供者
*
* @return 实例化的提供者
*/
@SneakyThrows
private RestAuthProvider determineAuthProvider() {
Class<?> candidate = restService.authProvider();
if (ClassUtils.isAssignable(RestAuthProvider.class, candidate)) {
return (RestAuthProvider) candidate.newInstance();
}
return properties.getAuthProvider();
}
/**
* 如果调用到了Object类的方法则绕行
*
* @param method 方法
* @param args 参数
* @return 结果
*/
@Deprecated
private Object passObjectMethod(Method method, Object[] args) {
// 处理基本方法被代理的情况
if (AopUtils.isEqualsMethod(method)) {
Object obj = args[0];
// The target does not implement the equals(Object) method itself.
return null != obj && ClassUtils.isAssignable(targetType, obj.getClass());
} else if (AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return -1;
}
return null;
}
}

View File

@ -0,0 +1,118 @@
package group.flyfish.rest.registry.proxy.entity;
import group.flyfish.rest.annotation.RestApi;
import group.flyfish.rest.enums.HttpMethod;
import group.flyfish.rest.registry.proxy.RestProxyInvoker;
import group.flyfish.rest.utils.DataUtils;
import lombok.Getter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import java.beans.Transient;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.stream.Stream;
/**
* 表示单个请求的最小单元
*
* @author wangyu
*/
public class RestMethod {
// 方法参数缓存避免clone
@Getter
private Parameter[] parameters;
// 解析@Transient注解的结果
@Getter
private boolean bare;
// 解析后的路径
@Getter
private String url;
// http方法
@Getter
private HttpMethod method;
// 是否携带cookie
@Getter
private boolean credentials;
// 多个参数时使用合并的body
@Getter
private boolean mergeBody;
// 带泛型的返回类型
@Getter
private Type genericReturnType;
// 不带泛型的返回类型
@Getter
private Class<?> returnType;
// 是否不可用状态
@Getter
private boolean invalid;
private RestMethod(Method method, RestProxyInvoker invoker) {
// 执行初始化
init(method, invoker);
}
/**
* 解析代理方法
*
* @param method 方法
* @return 结果
*/
public static RestMethod resolve(Method method, RestProxyInvoker invoker) {
return new RestMethod(method, invoker);
}
/**
* 初始化方法
*/
private void init(Method method, RestProxyInvoker invoker) {
RestApi restApi = AnnotatedElementUtils.findMergedAnnotation(method, RestApi.class);
// 无视proxy因为啥也没
if (null == restApi) {
this.invalid = true;
return;
}
this.url = determineUrl(restApi, invoker);
this.method = restApi.method();
this.credentials = restApi.credentials();
this.mergeBody = restApi.mergedBody();
this.parameters = method.getParameters();
this.bare = null != AnnotationUtils.findAnnotation(method, Transient.class);
this.genericReturnType = method.getGenericReturnType();
this.returnType = method.getReturnType();
}
/**
* 决定基本url优先级 方法注解url > 方法注解baseUrl + uri > 全局配置 + uri
*
* @return 结果
*/
private String determineUrl(RestApi restApi, RestProxyInvoker invoker) {
String url;
// 解析url以支持PathVariable
if (DataUtils.isNotBlank(restApi.url())) {
return restApi.url();
} else {
// 构建基础url优先级从小到大依次找同时尝试取字典值
return Stream.of(restApi.baseUrl(), invoker.getBaseUrl())
.filter(DataUtils::isNotBlank)
.findFirst()
// 判定和赋值
.map(found -> found.startsWith("#") ?
invoker.getProperties().getDictUrl(found.substring(1)) : found)
.map(base -> base + restApi.uri())
.orElseThrow(() -> new IllegalArgumentException("【Rest调用】未指定url或baseurl无法调用远端服务器"));
}
}
}

View File

@ -0,0 +1,161 @@
package group.flyfish.rest.registry.proxy.support;
import group.flyfish.rest.entity.Multipart;
import group.flyfish.rest.registry.proxy.entity.RestMethod;
import group.flyfish.rest.utils.DataUtils;
import lombok.Builder;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 参数解析上下文
*
* @author wangyu
*/
@Data
@Builder
public class ArgumentResolveContext {
// 解析的方法
private RestMethod method;
// 参数
private Map<String, Object> param;
// 路径参数
private Map<String, Object> pathParams;
// 请求头
private Map<String, String> headers;
// 请求体
private Object body;
// 文件列表
private Map<String, Multipart> files;
// 文件名列表
private Map<String, String> filenames;
// 设置参数
public void setParam(String key, Object value) {
if (DataUtils.isEmpty(param)) {
param = new HashMap<>();
}
param.put(key, value);
}
// 设置请求体分拣模式
public void setBody(String key, Object value) {
if (null == body || !(body instanceof Map)) {
body = new HashMap<>();
}
Map<String, Object> map = DataUtils.cast(body);
map.put(key, value);
}
/**
* 设置头部
*
* @param headers
*/
public void setHeaders(Map<String, String> headers) {
if (DataUtils.isEmpty(this.headers)) {
this.headers = new HashMap<>();
}
this.headers.putAll(headers);
}
/**
* 设置单个头
*
* @param name 名称
* @param value
*/
public void setHeader(String name, String value) {
if (DataUtils.isEmpty(this.headers)) {
this.headers = new HashMap<>();
}
this.headers.put(name, value);
}
/**
* 设置路径参数
*
* @param key 名称
* @param value
*/
public void setPathParam(String key, Object value) {
if (DataUtils.isEmpty(pathParams)) {
pathParams = new HashMap<>();
}
pathParams.put(key, value);
}
/**
* 设置文件
*
* @param name 文件key
* @param filename 文件名
* @param file 文件数据
*/
public void setMultipart(String name, String filename, Object file) {
setMultipart(new Multipart(name, filename, file));
}
/**
* 添加文件名
*
* @param part 文件部分key
* @param filename 文件名
*/
public void addFilename(String part, String filename) {
if (DataUtils.isEmpty(filenames)) {
filenames = new HashMap<>();
}
filenames.put(part, filename);
}
/**
* 设置文件
*
* @param multipart 文件
*/
public void setMultipart(Multipart multipart) {
if (DataUtils.isEmpty(files)) {
files = new HashMap<>();
}
if (null != multipart && null != multipart.getData()) {
files.put(multipart.getName(), multipart);
}
}
public boolean hasPathParams() {
return DataUtils.isNotEmpty(pathParams);
}
public boolean hasBody() {
return null != body;
}
public boolean hasHeaders() {
return DataUtils.isNotEmpty(headers);
}
public boolean hasParams() {
return DataUtils.isNotEmpty(param);
}
public boolean hasMultipart() {
return DataUtils.isNotEmpty(files);
}
public String getFilename(String part, String initial) {
if (null == filenames || !filenames.containsKey(part)) {
return initial;
}
return filenames.get(part);
}
}

View File

@ -0,0 +1,28 @@
package group.flyfish.rest.registry.proxy.support;
import java.lang.reflect.Parameter;
/**
* 参数解析器
*
* @author wangyu
*/
public interface RestArgumentResolver {
/**
* 是否支持
*
* @param parameter 参数
* @return 结果
*/
boolean support(Parameter parameter);
/**
* 解析
*
* @param context 上下文赋值
* @param parameter 参数
* @param value
*/
void resolve(ArgumentResolveContext context, Parameter parameter, Object value);
}

View File

@ -0,0 +1,53 @@
package group.flyfish.rest.registry.proxy.support;
import group.flyfish.rest.registry.proxy.entity.RestMethod;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.stream.IntStream;
/**
* 解析器集合
*
* @author wangyu
*/
public class RestArgumentResolverComposite {
// 解析器们
private final List<RestArgumentResolver> resolvers;
public RestArgumentResolverComposite(List<RestArgumentResolver> resolvers) {
this.resolvers = resolvers;
}
/**
* 执行解析
*
* @param method 方法
* @param args 参数
* @return 结果
*/
public ArgumentResolveContext resolve(RestMethod method, Object[] args) {
// 上下文
ArgumentResolveContext context = ArgumentResolveContext.builder().method(method).build();
// 参数元
Parameter[] parameters = method.getParameters();
// 循环处理
IntStream.range(0, parameters.length)
.forEach(index -> resolveInternal(context, parameters[index], args[index]));
return context;
}
/**
* 内部解析
*
* @param context 上下文
* @param parameter 参数
* @param value
*/
private void resolveInternal(ArgumentResolveContext context, Parameter parameter, Object value) {
// 开始解析
resolvers.stream().filter(resolver -> resolver.support(parameter)).findFirst()
.ifPresent(resolver -> resolver.resolve(context, parameter, value));
}
}

View File

@ -0,0 +1,53 @@
package group.flyfish.rest.registry.proxy.support;
import group.flyfish.rest.core.builder.MapParamBuilder;
import group.flyfish.rest.utils.DataUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* url编译器
*
* @author wangyu
*/
public class UrlCompiler {
// 匹配正则预编译
private static final Pattern PATTERN = Pattern.compile("\\{\\w+}");
/**
* 地址原路径编译
*
* @param source
* @return 结果
*/
public static String compile(String source, Map<String, Object> params) {
Matcher matcher = PATTERN.matcher(source);
// 开始查找和替换
while (matcher.find()) {
String found = matcher.group();
String key = DataUtils.substringBetween(found, "{", "}");
if (params.containsKey(key)) {
source = source.replace(found, String.valueOf(params.get(key)));
}
}
return source;
}
/**
* 测试
*
* @param args args参数
*/
public static void main(String[] args) {
String result = UrlCompiler.compile("http://www.baidu.com/love/{target}/{mobile}/{shit}", MapParamBuilder.builder()
.with("target", "nanami")
.with("mobile", 1223123)
.with("shit", new HashMap<>())
.build());
System.out.println(result);
}
}

View File

@ -0,0 +1,39 @@
package group.flyfish.rest.registry.proxy.support.resolvers;
import group.flyfish.rest.annotation.RestBody;
import group.flyfish.rest.registry.proxy.support.ArgumentResolveContext;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolver;
import java.lang.reflect.Parameter;
/**
* 请求体参数解析
*
* @author wangyu
*/
public class RestBodyArgumentResolver implements RestArgumentResolver {
/**
* 是否支持
*
* @param parameter 参数
* @return 结果
*/
@Override
public boolean support(Parameter parameter) {
return parameter.isAnnotationPresent(RestBody.class);
}
/**
* 解析
*
* @param context 上下文赋值
* @param parameter 参数
* @param value
*/
@Override
public void resolve(ArgumentResolveContext context, Parameter parameter, Object value) {
// 无视合并body这里的优先级最高
context.setBody(value);
}
}

View File

@ -0,0 +1,46 @@
package group.flyfish.rest.registry.proxy.support.resolvers;
import group.flyfish.rest.annotation.RestHeader;
import group.flyfish.rest.registry.proxy.support.ArgumentResolveContext;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolver;
import group.flyfish.rest.utils.DataUtils;
import java.lang.reflect.Parameter;
import java.util.Map;
/**
* 请求头解析策略
*
* @author wangyu
*/
public class RestHeaderArgumentResolver implements RestArgumentResolver {
/**
* 是否支持
*
* @param parameter 参数
* @return 结果
*/
@Override
public boolean support(Parameter parameter) {
return parameter.isAnnotationPresent(RestHeader.class);
}
/**
* 解析
*
* @param context 上下文赋值
* @param parameter 参数
* @param value
*/
@Override
public void resolve(ArgumentResolveContext context, Parameter parameter, Object value) {
if (value instanceof Map) {
((Map<?, ?>) value).forEach((k, v) -> context.setHeader((String) k, String.valueOf(v)));
} else {
RestHeader header = parameter.getAnnotation(RestHeader.class);
String name = DataUtils.isNotBlank(header.value()) ? header.value() : parameter.getName();
context.setHeader(name, null != value ? String.valueOf(value) : "");
}
}
}

View File

@ -0,0 +1,124 @@
package group.flyfish.rest.registry.proxy.support.resolvers;
import group.flyfish.rest.annotation.RestParam;
import group.flyfish.rest.annotation.RestParams;
import group.flyfish.rest.registry.proxy.support.ArgumentResolveContext;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolver;
import group.flyfish.rest.utils.DataUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.Optional;
/**
* 参数解析
*
* @author wangyu
*/
@Slf4j
public class RestParamArgumentResolver implements RestArgumentResolver {
/**
* 是否支持
*
* @param parameter 参数
* @return 结果
*/
@Override
public boolean support(Parameter parameter) {
return true;
}
/**
* 解析
*
* @param context 上下文赋值
* @param parameter 参数
* @param value
*/
@Override
public void resolve(ArgumentResolveContext context, Parameter parameter, Object value) {
// 当参数包含@RestParams注解使用BeanDescriptor处理
if (null != parameter.getAnnotation(RestParams.class) || ClassUtils.isAssignable(Map.class, parameter.getType())) {
resolveParams(context, parameter, value);
} else {
// 取得合法的名称
String name = Optional.ofNullable(parameter.getAnnotation(RestParam.class))
.map(RestParam::value)
.filter(DataUtils::isNotBlank)
.orElse(parameter.getName());
// 启用合并请求体合并入
if (context.getMethod().isMergeBody()) {
context.setBody(name, value);
} else {
context.setParam(name, parseValue(value));
}
}
}
/**
* 解析多个参数
*
* @param parameter 参数
* @param value
*/
private void resolveParams(ArgumentResolveContext context, Parameter parameter, Object value) {
// 参数注解存在报出错误
if (null != parameter.getAnnotation(RestParam.class)) {
throw new IllegalArgumentException("无法将对象作为一个普通的参数!");
}
// 非空才处理
if (null != value) {
// 是map直接解包赋值
if (ClassUtils.isAssignable(Map.class, parameter.getType())) {
Map<String, Object> values = DataUtils.cast(value);
values.forEach((k, v) -> {
if (null != v) {
context.setParam(k, parseValue(v));
}
});
} else {
// 对象解析后混入
for (PropertyDescriptor propertyDescriptor : BeanUtils.getPropertyDescriptors(value.getClass())) {
if ("class".equalsIgnoreCase(propertyDescriptor.getName())) {
continue;
}
Object v = null;
try {
v = propertyDescriptor.getReadMethod().invoke(value);
} catch (IllegalAccessException | InvocationTargetException e) {
log.error("【Rest客户端】尝试解析参数时发生异常!获取bean的属性表失败!{}", e.getMessage(), e);
}
if (null != v) {
context.setParam(propertyDescriptor.getName(), parseValue(v));
}
}
}
}
}
/**
* 解析值
*
* @param value
* @return 结果
*/
private String parseValue(Object value) {
if (value instanceof String) {
return (String) value;
}
if (value instanceof Iterable) {
Iterable<? extends CharSequence> mapped = DataUtils.cast(value);
return String.join(",", mapped);
}
if (null != value) {
return String.valueOf(value);
}
return "";
}
}

View File

@ -0,0 +1,76 @@
package group.flyfish.rest.registry.proxy.support.resolvers;
import group.flyfish.rest.annotation.RestPart;
import group.flyfish.rest.entity.Multipart;
import group.flyfish.rest.registry.proxy.support.ArgumentResolveContext;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolver;
import java.lang.reflect.Parameter;
/**
* 文件上传参数处理器
*
* @author wangyu
*/
public class RestPartArgumentResolver implements RestArgumentResolver {
/**
* 是否支持
*
* @param parameter 参数
* @return 结果
*/
@Override
public boolean support(Parameter parameter) {
return parameter.isAnnotationPresent(RestPart.class);
}
/**
* 解析
*
* @param context 上下文赋值
* @param parameter 参数
* @param value
*/
@Override
public void resolve(ArgumentResolveContext context, Parameter parameter, Object value) {
RestPart part = parameter.getAnnotation(RestPart.class);
if (value instanceof Multipart) {
context.setMultipart((Multipart) value);
} else if (null != value) {
context.setMultipart(part.value(), null, value);
}
}
/**
* 解析和处理文件名
*/
public static class FilenameResolver implements RestArgumentResolver {
/**
* 是否支持
*
* @param parameter 参数
* @return 结果
*/
@Override
public boolean support(Parameter parameter) {
return parameter.isAnnotationPresent(RestPart.Filename.class);
}
/**
* 解析
*
* @param context 上下文赋值
* @param parameter 参数
* @param value
*/
@Override
public void resolve(ArgumentResolveContext context, Parameter parameter, Object value) {
if (value instanceof String) {
RestPart.Filename filename = parameter.getAnnotation(RestPart.Filename.class);
context.addFilename(filename.value(), (String) value);
}
}
}
}

View File

@ -0,0 +1,41 @@
package group.flyfish.rest.registry.proxy.support.resolvers;
import group.flyfish.rest.annotation.RestPathParam;
import group.flyfish.rest.registry.proxy.support.ArgumentResolveContext;
import group.flyfish.rest.registry.proxy.support.RestArgumentResolver;
import group.flyfish.rest.utils.DataUtils;
import java.lang.reflect.Parameter;
/**
* 解析路径参数
*
* @author wangyu
*/
public class RestPathParamArgumentResolver implements RestArgumentResolver {
/**
* 是否支持
*
* @param parameter 参数
* @return 结果
*/
@Override
public boolean support(Parameter parameter) {
return parameter.isAnnotationPresent(RestPathParam.class);
}
/**
* 解析
*
* @param context 上下文赋值
* @param parameter 参数
* @param value
*/
@Override
public void resolve(ArgumentResolveContext context, Parameter parameter, Object value) {
RestPathParam annotation = parameter.getAnnotation(RestPathParam.class);
String name = DataUtils.isNotBlank(annotation.value()) ? annotation.value() : parameter.getName();
context.setPathParam(name, value);
}
}

View File

@ -0,0 +1,67 @@
package group.flyfish.rest.registry.wrapper;
import group.flyfish.rest.core.exception.RestClientException;
import group.flyfish.rest.mapping.RestResultMapping;
import group.flyfish.rest.utils.TypeResolveUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.utils.DateUtils;
import java.lang.reflect.Type;
import java.util.Date;
/**
* 默认缺省的结果映射
*
* @author wangyu
*/
@Slf4j
public class DefaultRestResultMapping implements RestResultMapping {
/**
* 获取内部类单例
*
* @return 结果
*/
public static DefaultRestResultMapping getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 模糊的结果映射
*
* @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);
}
/**
* 静态初始化器由JVM来保证线程安全
*/
private interface SingletonHolder {
DefaultRestResultMapping INSTANCE = new DefaultRestResultMapping();
}
}

View File

@ -0,0 +1,37 @@
package group.flyfish.rest.registry.wrapper;
import lombok.Data;
/**
* 标准的解包结构等同于全局
*
* @author wangyu
*/
@Data
public class RestResult<T> {
/**
* 成功标志
*/
private boolean success = true;
/**
* 返回处理消息
*/
private String message = "操作成功!";
/**
* 返回代码
*/
private Integer code = 0;
/**
* 返回数据对象 data
*/
private T result;
/**
* 时间戳
*/
private long timestamp = System.currentTimeMillis();
}

View File

@ -0,0 +1,164 @@
package group.flyfish.rest.utils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Map;
/**
* 数据工具类仅限于rest模块使用
*
* @author wangyu
*/
public final class DataUtils {
private static final int INDEX_NOT_FOUND = -1;
private static final String EXTENSION_SEPARATOR = ".";
/**
* The Unix separator character.
*/
private static final char UNIX_SEPARATOR = '/';
/**
* The Windows separator character.
*/
private static final char WINDOWS_SEPARATOR = '\\';
/**
* 判断字符串是否不为空
*
* @param target 目标字符串
* @return 结果
*/
public static boolean isNotBlank(String target) {
return !StringUtils.isEmpty(target);
}
/**
* 强制转换和平滑过渡
*
* @param source 原对象
* @param <T> 原类型
* @param <R> 目标类型
* @return 结果
*/
@SuppressWarnings("unchecked")
public static <T, R> T cast(R source) {
return (T) source;
}
/**
* 判断集合是否不为空
*
* @param collection 集合
* @return 结果
*/
public static boolean isNotEmpty(Collection<?> collection) {
return !CollectionUtils.isEmpty(collection);
}
/**
* 判断map是否不为空
*
* @param map 一个字典
* @return 结果
*/
public static boolean isNotEmpty(Map<?, ?> map) {
return !CollectionUtils.isEmpty(map);
}
/**
* 判断map是否为囧
*
* @param map 一个字典
* @return 结果
*/
public static boolean isEmpty(Map<?, ?> map) {
return CollectionUtils.isEmpty(map);
}
/**
* 判断集合是否为空
*
* @param collection 集合
* @return 结果
*/
public static boolean isEmpty(Collection<?> collection) {
return CollectionUtils.isEmpty(collection);
}
/**
* 是否为空
*
* @param target 目标
* @return 结果
*/
public static boolean isBlank(String target) {
return StringUtils.isEmpty(target);
}
/**
* 查找两个字符串之间的内容
*
* @param str 字符串
* @param open 开始
* @param close 结尾
* @return 结果
*/
public static String substringBetween(final String str, final String open, final String close) {
if (str == null || open == null || close == null) {
return null;
}
final int start = str.indexOf(open);
if (start != INDEX_NOT_FOUND) {
final int end = str.indexOf(close, start + open.length());
if (end != INDEX_NOT_FOUND) {
return str.substring(start + open.length(), end);
}
}
return null;
}
/**
* 判断是不是true
*
* @param value
* @return 结果
*/
public static boolean isTrue(Boolean value) {
return Boolean.TRUE.equals(value);
}
public static String getExtension(final String filename) {
if (filename == null) {
return null;
}
final int index = indexOfExtension(filename);
if (index == INDEX_NOT_FOUND) {
return "";
} else {
return filename.substring(index + 1);
}
}
public static int indexOfExtension(final String filename) {
if (filename == null) {
return INDEX_NOT_FOUND;
}
final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
final int lastSeparator = indexOfLastSeparator(filename);
return lastSeparator > extensionPos ? INDEX_NOT_FOUND : extensionPos;
}
public static int indexOfLastSeparator(final String filename) {
if (filename == null) {
return INDEX_NOT_FOUND;
}
final int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR);
final int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR);
return Math.max(lastUnixPos, lastWindowsPos);
}
}

View File

@ -0,0 +1,171 @@
package group.flyfish.rest.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Jackson序列化工具类
*
* @author Mr.Wang
*/
public final class JacksonUtil {
private static ObjectMapper mapper = new ObjectMapper();
static {
// =========================================================================
// SerializationFeature for changing how JSON is written
// to enable standard indentation ("pretty-printing"):
// mapper.enable(SerializationFeature.INDENT_OUTPUT);
// to allow serialization of "empty" POJOs (no properties to serialize)
// (without this setting, an exception is thrown in those cases)
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// to write java.util.Date, Calendar as number (timestamp):
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// DeserializationFeature for changing how JSON is read as POJOs:
// to prevent exception when encountering unknown property:
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// to allow coercion of JSON empty String ("") to null Object value:
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
// =========================================================================
// JsonParser.Feature for configuring parsing settings:
// to allow C/C++ style comments in JSON (non-standard, disabled by default)
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// (note: with Jackson 2.5, there is also `mapper.enable(feature)` / `mapper.disable(feature)`)
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
// to allow (non-standard) unquoted field names in JSON:
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
// to allow use of apostrophes (single quotes), non standard
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// JsonGenerator.Feature for configuring low-level JSON generation:
// to force escaping of non-ASCII characters:
mapper.configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, true);
}
/**
* 设置jackson
*
* @param objectMapper 映射器
*/
public static void setObjectMapper(ObjectMapper objectMapper) {
JacksonUtil.mapper = objectMapper;
}
/**
* 修复不同环境下统一序列化表现
*
* @param objectMappers jackson om
*/
@Autowired
public void setObjectMapper(ObjectProvider<ObjectMapper> objectMappers) {
objectMappers.ifAvailable(om -> mapper = om);
}
public static <T> T fromJson(final String json) {
return readValue(json, new TypeReference<T>() {
});
}
public static <T> T fromJson(final String json, TypeReference<T> reference) {
return readValue(json, reference);
}
public static <T> T fromJson(final String json, Class<T> clazz) {
if (null == json || "".equals(json)) {
return null;
}
try {
return mapper.readValue(json, clazz);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static <T> T fromJson(final String json, JavaType type) {
if (null == json || "".equals(json)) {
return null;
}
try {
return mapper.readValue(json, type);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// public static Map json2Map(final String json) {
// return fromJson(json, Map.class);
// }
public static <K, V> Map<K, V> json2Map(String json) {
return readValue(json, new TypeReference<Map<K, V>>() {
});
}
public static <T> List<T> json2List(final String json) {
return readValue(json, new TypeReference<List<T>>() {
});
}
public static <T> List<T> json2List(final String json, Class<T> clazz) {
if (null == json || "".equals(json)) {
return Collections.emptyList();
}
try {
return mapper.readValue(json, mapper.getTypeFactory().constructParametricType(List.class, clazz));
} catch (IOException e) {
e.printStackTrace();
}
return Collections.emptyList();
}
public static Optional<String> toJson(final Object obj) {
try {
return Optional.of(mapper.writeValueAsString(obj));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return Optional.empty();
}
private static <T> T readValue(String json, TypeReference<T> valueTypeRef) {
if (null == json || "".equals(json)) {
return null;
}
try {
return mapper.readValue(json, valueTypeRef);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static ObjectMapper getMapper() {
return mapper;
}
}

View File

@ -0,0 +1,90 @@
package group.flyfish.rest.utils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* 请求上下文
*
* @author wangyu
* 基于spring安全调用
*/
public final class RequestContext {
public static final String AUTHORIZATION_KEY = "Authorization";
/**
* 获取当前request
*
* @return 结果
*/
public static Optional<HttpServletRequest> getRequest() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes instanceof ServletRequestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
return Optional.ofNullable(servletRequestAttributes.getRequest());
}
return Optional.empty();
}
/**
* 获取响应
*
* @return 结果
*/
public static Optional<HttpServletResponse> getResponse() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes instanceof ServletRequestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
return Optional.ofNullable(servletRequestAttributes.getResponse());
}
return Optional.empty();
}
/**
* 获取所有的cookie
*
* @return 结果
*/
public static List<Cookie> getCookies() {
return getRequest().flatMap(request -> Optional.ofNullable(request.getCookies()))
.map(cookies -> Arrays.stream(cookies).collect(Collectors.toList()))
.orElse(Collections.emptyList());
}
/**
* 获取并过滤cookie
*
* @param predicate 匹配
* @return 结果
*/
public static List<Cookie> getCookies(Predicate<? super Cookie> predicate) {
return getRequest().flatMap(request -> Optional.ofNullable(request.getCookies()))
.map(cookies -> Arrays.stream(cookies).filter(predicate).collect(Collectors.toList()))
.orElse(Collections.emptyList());
}
/**
* 获取鉴权token相关的cookie
*
* @return 结果
*/
public static Optional<String> getCredential() {
return getRequest().map(request -> request.getHeader(AUTHORIZATION_KEY))
.filter(DataUtils::isNotBlank)
.map(Optional::of)
.orElseGet(() -> getCookies(cookie -> AUTHORIZATION_KEY.equals(cookie.getName())).stream()
.findAny().map(Cookie::getValue));
}
}

View File

@ -0,0 +1,73 @@
package group.flyfish.rest.utils;
import group.flyfish.rest.core.client.RestClientBuilder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpMessage;
import org.apache.http.client.methods.CloseableHttpResponse;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 日志打印相关
*
* @author wangyu
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j
public final class RestLogUtils {
private static final String LOG_PREFIX = "【Rest Invoke】";
/**
* 打印请求日志
*
* @param builder 请求信息
*/
public static void log(RestClientBuilder builder) {
if (log.isDebugEnabled()) {
log.debug("{} {} {}\nRequest Headers: {}\nParameters:{}\nBody:{}", LOG_PREFIX, builder.getMethod(),
builder.getUrl(), resolveMap(builder.getHeaders()), resolveMap(builder.getParams()), builder.getBody());
} else {
log.info("{} {} {}", LOG_PREFIX, builder.getMethod(), builder.getUrl());
}
}
/**
* 打印响应信息
*
* @param response 响应信息
*/
public static void log(CloseableHttpResponse response) {
if (log.isDebugEnabled()) {
log.debug("{} Response Status: {}\nResponse Headers: {}\n", LOG_PREFIX,
response.getStatusLine().getStatusCode(), resolveHeaders(response));
}
}
/**
* 打印结果体
*
* @param result 结果
*/
public static void log(Object result) {
if (log.isDebugEnabled()) {
log.debug("{} Response Body:{}", LOG_PREFIX, JacksonUtil.toJson(result).orElse("no body"));
}
}
private static String resolveHeaders(HttpMessage message) {
return Arrays.stream(message.getAllHeaders())
.map(header -> String.join("=", header.getName(), header.getValue()))
.collect(Collectors.joining(";"));
}
private static String resolveMap(Map<String, ?> headers) {
return headers.entrySet().stream()
.map(header -> String.join("=", header.getKey(), String.valueOf(header.getValue())))
.collect(Collectors.joining(";"));
}
}

View File

@ -0,0 +1,33 @@
package group.flyfish.rest.utils;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.lang.reflect.Type;
/**
* 类型解析工具类
*
* @author wangyu
*/
public final class TypeResolveUtils {
/**
* 使用包装类包装
*
* @param origin 原类型
* @param wrapper 包装类型
* @return 结果
*/
public static Type wrap(Type origin, Class<?> wrapper) {
// 构建泛型的返回值
TypeFactory typeFactory = JacksonUtil.getMapper().getTypeFactory();
if (origin instanceof Class) {
return typeFactory.constructParametricType(wrapper, (Class<?>) origin);
} else if (origin instanceof JavaType) {
return typeFactory.constructParametricType(wrapper, (JavaType) origin);
}
// 无法解析未知类型
return origin;
}
}

View File

@ -0,0 +1,69 @@
package group.flyfish.rest;
import group.flyfish.rest.annotation.RestPart;
import group.flyfish.rest.annotation.RestService;
import group.flyfish.rest.annotation.methods.GET;
import group.flyfish.rest.annotation.methods.POST;
import group.flyfish.rest.container.RestTestContainer;
import group.flyfish.rest.entity.Multipart;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = RestTestContainer.class)
@ComponentScan("sys.test")
@Slf4j
@Component
public class MultipartTest {
@Resource
private TestRestService testRestService;
/**
* 测试入口
*/
@Test
public void test() {
File file = new File("/Users/wangyu/Desktop/2022年终述职报告.docx");
String filename = testRestService.uploadPart(new Multipart("file", file.getName(), file), file.getName(), 55L);
System.out.println(filename);
try (InputStream in = Files.newInputStream(file.toPath())) {
filename = testRestService.uploadAnno(in, file.getName());
System.out.println(filename);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
public void download() {
byte[] data = testRestService.downloadByte();
System.out.println(data.length);
}
@RestService(baseUrl = "http://localhost:8999", timeout = 500)
public interface TestRestService {
@POST("/files")
String uploadPart(@RestPart Multipart file, String name, @RestPart("token") Long token);
@POST("/files")
String uploadAnno(@RestPart("fbl") InputStream file, @RestPart.Filename("fbl") String name);
@GET("/files")
byte[] downloadByte();
}
}

View File

@ -0,0 +1,54 @@
package group.flyfish.rest;
import group.flyfish.rest.core.client.RestClient;
import group.flyfish.rest.core.builder.MapParamBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.nio.charset.Charset;
@RunWith(SpringRunner.class)
@WebAppConfiguration
@Slf4j
public class RestClientTest {
private void success(HttpEntity httpEntity) {
try {
String result =
StreamUtils.copyToString(httpEntity.getContent(), Charset.defaultCharset());
log.info(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testGet() {
RestClient.create()
.get()
.url("https://www.baidu.com")
.build()
.execute(this::success);
}
@Test
public void testPost() {
RestClient.create()
.post()
.url("http://demo.flyfish.group/api/login")
.addHeader("Authorization", "Bearer xbsef92x=")
.body(MapParamBuilder.builder()
.with("username", "admin")
.with("password", "123456")
.build()
)
.build()
.execute(this::success);
}
}

View File

@ -0,0 +1,75 @@
package group.flyfish.rest;
import group.flyfish.rest.annotation.AutoMapping;
import group.flyfish.rest.annotation.RestBody;
import group.flyfish.rest.annotation.RestPathParam;
import group.flyfish.rest.annotation.RestService;
import group.flyfish.rest.annotation.methods.GET;
import group.flyfish.rest.annotation.methods.POST;
import group.flyfish.rest.container.RestTestContainer;
import group.flyfish.rest.domain.TestItem;
import group.flyfish.rest.mapping.TestRestResultMapping;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* rest代理测试
*
* @author wangyu
*/
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = RestTestContainer.class)
@ComponentScan("sys.test")
@Slf4j
@Component
public class RestProxyTest {
@Resource
private TestRestService testRestService;
@Resource
private TestPostService testPostService;
/**
* 测试入口
*/
@Test
public void test() {
List<TestItem> items = testRestService.getCameras("1298064063717781506", "S4NbecfYA1CBGIOB0QDOT4", null);
Map<String, Object> map = new HashMap<>();
map.put("latitude", "");
map.put("longitude", "");
map.put("radius", "");
map.put("type", "1,2,3,4");
Object result = testPostService.getResources(map);
log.info(items.toString());
}
@RestService(baseUrl = "http://60.221.255.208:18092/api/", timeout = 500)
@AutoMapping(TestRestResultMapping.class)
public interface TestRestService {
@GET("/video/{platformId}/cameras/all")
List<TestItem> getCameras(@RestPathParam String platformId, String regionCode, String name1);
}
@RestService(baseUrl = "http://220.194.160.4:8083/interface", timeout = 500)
@AutoMapping(TestRestResultMapping.class)
public interface TestPostService {
@POST("/getResources")
Map<String, List<Map<String, Object>>> getResources(@RestBody Map<String, Object> body);
}
}

View File

@ -0,0 +1,60 @@
package group.flyfish.rest;
import group.flyfish.rest.annotation.*;
import group.flyfish.rest.annotation.methods.GET;
import group.flyfish.rest.container.RestTestContainer;
import group.flyfish.rest.domain.TestResult;
import group.flyfish.rest.mapping.TestRestResultMapping;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = RestTestContainer.class)
@ComponentScan("sys.test")
@Slf4j
@Component
public class SimpleProxyTest {
@Resource
private TestSimpleService testSimpleService;
@Test
public void test() {
log.info(TestResult.class.getMethods()[0].toGenericString());
Map<String, Object> values = new HashMap<>();
values.put("a", 1);
values.put("b", 2);
TestResult<Void> query = new TestResult<>();
query.setMsg("asdasd");
Map<String, Object> result = testSimpleService.getDirectories("123456", "admin", values, query);
if (null != result) {
log.info(result.toString());
}
}
@RestService("dsep")
@AutoMapping(TestRestResultMapping.class)
public interface TestSimpleService {
/**
* 资产目录列表
*
* @param token token
* @return 结果
*/
@GET("/financial/public/income")
Map<String, Object> getDirectories(@RestHeader("Authorization") String token, @RestParam("userId") String userId,
Map<String, Object> others, @RestParams TestResult<Void> result);
}
}

View File

@ -0,0 +1,19 @@
package group.flyfish.rest.container;
import group.flyfish.rest.annotation.EnableRestApiProxy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "group.flyfish.rest")
@EnableRestApiProxy
public class RestTestContainer {
/**
* 唯一启动类
*
* @param args 程序参数
*/
public static void main(String[] args) {
SpringApplication.run(RestTestContainer.class, args);
}
}

View File

@ -0,0 +1,19 @@
package group.flyfish.rest.domain;
import lombok.Data;
/**
* 资产目录视图实体
*
* @author wangyu
*/
@Data
public class DataDirectoryVo {
private String id;
private String directoryName;
//更新周期
private String updateCycleCode;
private String updateCycleName;
}

View File

@ -0,0 +1,18 @@
package group.flyfish.rest.domain;
import lombok.Data;
/**
* 测试项目
*
* @author wangyu
*/
@Data
public class TestItem {
private String name;
private String code;
private String url;
}

View File

@ -0,0 +1,18 @@
package group.flyfish.rest.domain;
import lombok.Data;
/**
* 测试用的返回值
*
* @param <T> 泛型
*/
@Data
public class TestResult<T> {
private Integer code;
private String msg;
private T data;
}

View File

@ -0,0 +1,42 @@
package group.flyfish.rest.mapping;
import group.flyfish.rest.core.exception.RestClientException;
import group.flyfish.rest.domain.TestResult;
import group.flyfish.rest.utils.TypeResolveUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Type;
@Component
public class TestRestResultMapping implements RestResultMapping {
/**
* 模糊的结果映射
*
* @param result 结果
* @return 映射后的结果
*/
@Override
@SuppressWarnings("unchecked")
public <T> T map(Object result) {
if (result instanceof TestResult) {
TestResult<?> mapped = (TestResult<?>) result;
if (null != mapped.getCode() && (mapped.getCode() == 200 || mapped.getCode() == 0)) {
return (T) mapped.getData();
}
throw new RestClientException("发生了超级异常:" + mapped.getMsg());
}
return (T) result;
}
/**
* 解析返回类型
*
* @param resultType 返回类型
* @return 结果
*/
@Override
public Type resolve(Type resultType) {
return TypeResolveUtils.wrap(resultType, TestResult.class);
}
}

View File

@ -0,0 +1,14 @@
rest:
client:
connection-timeout: 10s
base-url: http://localhost:8089
always-trust: true
urls:
dsep: http://220.194.160.6:8091/jeecg-boot/api
bdmp: http://localhost:8081
damp: http://60.221.255.208:18092/api
debug: true
logging:
level:
group.flyfish.rest: debug

View File

@ -1,15 +0,0 @@
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);
}
}

View File

@ -1,22 +0,0 @@
package group.flyfish.demo.controller;
import group.flyfish.demo.service.other.OtherTestService;
import group.flyfish.rest.utils.JacksonUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("others")
public class OtherController {
@Resource
private OtherTestService testService;
@GetMapping
public Object getOther() {
return testService.postPcSearch(JacksonUtil.fromJson("{\"invoke_info\":{\"pos_1\":[{}],\"pos_2\":[{}],\"pos_3\":[{}]}}"));
}
}

View File

@ -1,33 +0,0 @@
package group.flyfish.demo.controller;
import group.flyfish.demo.service.weather.YikeWeatherService;
import group.flyfish.rest.core.builder.MapParamBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("weather")
public class WeatherController {
@Resource
private YikeWeatherService weatherService;
@GetMapping("day")
public Map<String, Object> getDay() {
return weatherService.getDay(MapParamBuilder.builder()
.with("appid", "63627136")
.with("appsecret", "We76ZHMP")
.with("cityid", "101120101")
.with("city", "济南")
.build());
}
@GetMapping("day-straight")
public Map<String, Object> getDayStraight() {
return weatherService.getDayStraight("63627136", "We76ZHMP", "济南");
}
}

View File

@ -1,27 +0,0 @@
package group.flyfish.demo.service.other;
import group.flyfish.rest.annotation.RestApi;
import group.flyfish.rest.annotation.RestBody;
import group.flyfish.rest.annotation.RestService;
import group.flyfish.rest.annotation.methods.GET;
import group.flyfish.rest.annotation.methods.POST;
import org.springframework.web.bind.annotation.PostMapping;
/**
* 使用预定义路径
* 系统自动从application配置获取
*
* @author wangyu
*/
@RestService("other")
public interface OtherTestService {
/**
* 一个简单的post请求
*
* @param body 请求体
* @return 结果
*/
@POST("/mcp/pc/pcsearch")
Object postPcSearch(@RestBody Object body);
}

View File

@ -1,34 +0,0 @@
package group.flyfish.demo.service.weather;
import group.flyfish.rest.annotation.RestParams;
import group.flyfish.rest.annotation.RestService;
import group.flyfish.rest.annotation.methods.GET;
import java.util.Map;
/**
* 易客云天气接口
*
* @author wangyu
*/
@RestService(baseUrl = "https://tianqiapi.com/free")
public interface YikeWeatherService {
/**
* 获取当天日期
*
* @return 结果
* @apiNote 使用@RestParams框架会自动注入所有键值对作为query
*/
@GET("/day")
Map<String, Object> getDay(@RestParams Map<String, Object> params);
/**
* 获取当天日期
*
* @return 结果
* @apiNote 框架会自动将参数解包成query
*/
@GET("/day")
Map<String, Object> getDayStraight(String appid, String appsecret, String city);
}

View File

@ -1,28 +0,0 @@
package group.flyfish.demo.service.yapi;
import group.flyfish.rest.core.auth.RestAuthProvider;
import group.flyfish.rest.core.client.RestClientBuilder;
/**
* yapi调用需要的鉴权配置
*
* @author wangyu
*/
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);
}
}

View File

@ -1,14 +0,0 @@
package group.flyfish.demo.service.yapi;
import group.flyfish.rest.annotation.RestService;
/**
* yapi服务支持鉴权
*
* @author wangyu
*/
@RestService(value = "yapi", authProvider = YapiAuthProvider.class)
public interface YapiService {
}

View File

@ -1,7 +0,0 @@
rest:
client:
always-trust: true
urls:
other: https://ug.baidu.com
yapi: http://yapi.flyfish.group/api
connection-timeout: 10s

View File

@ -1,13 +0,0 @@
package group.flyfish.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class RestProxyDemoApplicationTests {
@Test
void contextLoads() {
}
}

BIN
walllet.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB