Compare commits
No commits in common. "example" and "main" have entirely different histories.
23
.gitignore
vendored
23
.gitignore
vendored
@ -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
|
||||
|
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Binary file not shown.
2
.mvn/wrapper/maven-wrapper.properties
vendored
2
.mvn/wrapper/maven-wrapper.properties
vendored
@ -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
202
LICENSE
Normal 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
605
README.md
Normal 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以及一个字符串body:token=xxx
|
||||
* @param file 文件信息
|
||||
* @param type 文件类型名称
|
||||
* @param token 凭据
|
||||
* @return 结果
|
||||
*/
|
||||
@POST("/files")
|
||||
FileEntity uploadPart(@RestPart Multipart file, String type, @RestPart("token") Long token);
|
||||
|
||||
/**
|
||||
* 使用input stream上传一个文件,此外还支持byte[],可以用@RestPart.Filename指定文件名
|
||||
* @param file 文件
|
||||
* @param name 文件名
|
||||
* @return 结果
|
||||
*/
|
||||
@POST("/files")
|
||||
FileEntity uploadAnno(@RestPart("fbl") InputStream file, @RestPart.Filename("fbl") String name);
|
||||
|
||||
/**
|
||||
* 下载一个文件为byte[]
|
||||
* @return 下载结果
|
||||
*/
|
||||
@GET("/files")
|
||||
byte[] downloadByte();
|
||||
}
|
||||
```
|
||||
调用时,只需要如下步骤
|
||||
```java
|
||||
@Service
|
||||
public class Test {
|
||||
|
||||
@Resource
|
||||
private TestRestService testRestService;
|
||||
|
||||
/**
|
||||
* 测试入口
|
||||
*/
|
||||
@Test
|
||||
public void test() {
|
||||
// 使用file对象包装
|
||||
File file = new File("/Users/wangyu/Desktop/2022年终述职报告.docx");
|
||||
FileEntity result = testRestService.uploadPart(new Multipart("file", file.getName(), file), "docx", 55L);
|
||||
// 使用input stream
|
||||
try (InputStream in = Files.newInputStream(file.toPath())) {
|
||||
FileEntity entity = testRestService.uploadAnno(in, file.getName());
|
||||
System.out.println(entity);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// 下载
|
||||
testRestService.downloadByte();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 启用接口扫描
|
||||
最后,我们在spring boot启动类上添加注解,就可以直接使用了!
|
||||
|
||||
这里需要手动指定一下要扫描的basePackages,组件不经过您的允许是不会扫描其他包的。
|
||||
|
||||
```java
|
||||
package group.flyfish.demo;
|
||||
|
||||
import group.flyfish.rest.annotation.EnableRestApiProxy;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableRestApiProxy(basePackages = "group.flyfish.demo.service")
|
||||
public class RestProxyDemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(RestProxyDemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 开始愉快的对接吧!
|
||||
至此,我们的快速开始教程已经完成。
|
||||
|
||||
## 结果映射处理和绑定
|
||||
大家在实现接口的时候都心照不宣的都一个习惯,那就是将接口响应增加一个包装类,样子大概是这样的:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 0,
|
||||
"message": "成功",
|
||||
"result": {
|
||||
"name": "名称",
|
||||
"code": "编码"
|
||||
}
|
||||
}
|
||||
```
|
||||
但是我们在写Java代码时却不这么写。于是乎,这里就涉及到了一个**结果解包**的逻辑。
|
||||
|
||||
默认情况下,Rest代理器不会对结果进行解包。但是你可以添加`@AutoMapping`注解来标记自动解包。
|
||||
|
||||
这里我们提供了一个默认的数据结构用于解包的缺省行为,具体的格式就是**上面的例子**所定义的那样。
|
||||
如果您要调用的系统不以该接口包装格式返回,需要您自己书写相应逻辑。您需要实现`RestResultMapping`接口并正确书写`map`和`resolve`方法。
|
||||
|
||||
`map`方法用于实现解包逻辑,也就是**如何从包装的结果中取得真实的数据**,比如上面的`result`字段。
|
||||
此外,该方法内建议自行处理异常结果`code`,并主动抛出`RestClientException`。
|
||||
|
||||
`convert`方法用于将我们在`@RestService`中定义的`method`的返回值进行包装,保证返回真实的类型。
|
||||
比如方法签名为:`User getUser()`,包装类为:`Result`,这时我们需要返回`Result<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)
|
316
mvnw
vendored
316
mvnw
vendored
@ -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
188
mvnw.cmd
vendored
@ -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
192
pom.xml
@ -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
21
rest-proxy-api/pom.xml
Normal 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>
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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 "";
|
||||
}
|
@ -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 "";
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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 "";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package group.flyfish.rest.configuration;
|
||||
|
||||
/**
|
||||
* rest客户端加载感知
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RestLoadedAware {
|
||||
|
||||
/**
|
||||
* 所有客户端完成加载
|
||||
*/
|
||||
void onClientsLoaded();
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package group.flyfish.rest.enums;
|
||||
|
||||
/**
|
||||
* Http请求类型
|
||||
* @author wangyu
|
||||
*/
|
||||
public enum HttpMethod {
|
||||
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package group.flyfish.rest.enums;
|
||||
|
||||
/**
|
||||
* 响应类型
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public enum ResponseType {
|
||||
NORMAL, TEXT, JSON, BINARY, OBJECT
|
||||
}
|
@ -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
82
rest-proxy-core/pom.xml
Normal 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>
|
@ -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";
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
@ -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}";
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package group.flyfish.rest.constants;
|
||||
|
||||
/**
|
||||
* 类型常量
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public interface TypeConstants {
|
||||
|
||||
String STRING = "java.lang.String";
|
||||
|
||||
String BYTE_ARRAY = "[B";
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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. 支持上传文件、FormData,JSON、支持自定义无侵入扩展
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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. 支持上传文件、FormData,JSON、支持自定义无侵入扩展
|
||||
* 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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 "";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package group.flyfish.rest.registry;
|
||||
|
||||
/**
|
||||
* 用于标记rest服务代理
|
||||
*
|
||||
* @author wangyu
|
||||
* @deprecated 该类已经过时,请使用新版的注解声明
|
||||
* @see group.flyfish.rest.annotation.RestService
|
||||
*/
|
||||
@Deprecated
|
||||
public interface RestService {
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,无法调用远端服务器!"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) : "");
|
||||
}
|
||||
}
|
||||
}
|
@ -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 "";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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(";"));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
14
rest-proxy-core/src/test/resources/application.yml
Normal file
14
rest-proxy-core/src/test/resources/application.yml
Normal 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
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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\":[{}]}}"));
|
||||
}
|
||||
}
|
@ -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", "济南");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
rest:
|
||||
client:
|
||||
always-trust: true
|
||||
urls:
|
||||
other: https://ug.baidu.com
|
||||
yapi: http://yapi.flyfish.group/api
|
||||
connection-timeout: 10s
|
@ -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
BIN
walllet.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
Loading…
Reference in New Issue
Block a user