From 8fcb802cfcc14b8baacc0767a0dc06ee5d2df5e9 Mon Sep 17 00:00:00 2001 From: wangyu <727842003@qq.com> Date: Tue, 10 Jan 2023 18:19:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=EF=BC=8C=E8=BF=81=E7=A7=BB=E5=8E=9F=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E6=AD=A3=E5=BC=8F=E5=BC=80=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 40 +++ README.md | 13 + pom.xml | 89 +++++ rest-proxy-api/pom.xml | 21 ++ .../flyfish/rest/annotation/AutoMapping.java | 21 ++ .../flyfish/rest/annotation/RestApi.java | 69 ++++ .../flyfish/rest/annotation/RestBody.java | 14 + .../flyfish/rest/annotation/RestHeader.java | 21 ++ .../flyfish/rest/annotation/RestParam.java | 21 ++ .../flyfish/rest/annotation/RestParams.java | 15 + .../rest/annotation/RestPathParam.java | 21 ++ .../flyfish/rest/annotation/RestService.java | 35 ++ .../group/flyfish/rest/enums/HttpMethod.java | 9 + .../flyfish/rest/enums/ResponseType.java | 10 + .../rest/mapping/RestResultMapping.java | 35 ++ rest-proxy-core/pom.xml | 90 +++++ .../rest/annotation/EnableRestApiProxy.java | 25 ++ .../group/flyfish/rest/client/RestClient.java | 323 ++++++++++++++++++ .../RestClientConfiguration.java | 77 +++++ .../configuration/RestClientProperties.java | 67 ++++ .../flyfish/rest/constants/RestConstants.java | 209 ++++++++++++ .../flyfish/rest/core/ThreadPoolManager.java | 57 ++++ .../rest/core/builder/MapParamBuilder.java | 166 +++++++++ .../rest/core/builder/RestClientBuilder.java | 199 +++++++++++ .../rest/core/builder/TypedMapBuilder.java | 54 +++ .../flyfish/rest/core/entity/Multipart.java | 22 ++ .../core/exception/RestClientException.java | 40 +++ .../rest/core/produce/HttpClientProducer.java | 101 ++++++ .../core/resolver/HttpDeleteResolver.java | 26 ++ .../rest/core/resolver/HttpGetResolver.java | 18 + .../core/resolver/HttpMethodResolver.java | 18 + .../rest/core/resolver/HttpPatchResolver.java | 27 ++ .../rest/core/resolver/HttpPostResolver.java | 20 ++ .../rest/core/resolver/HttpPutResolver.java | 27 ++ .../support/AbstractBodyResolver.java | 104 ++++++ .../support/AbstractParamResolver.java | 29 ++ .../rest/registry/RestApiRegistry.java | 136 ++++++++ .../flyfish/rest/registry/RestService.java | 12 + .../rest/registry/proxy/RestProxyInvoker.java | 232 +++++++++++++ .../proxy/support/ArgumentResolveContext.java | 100 ++++++ .../proxy/support/RestArgumentResolver.java | 28 ++ .../RestArgumentResolverComposite.java | 55 +++ .../registry/proxy/support/UrlCompiler.java | 53 +++ .../resolvers/RestBodyArgumentResolver.java | 39 +++ .../resolvers/RestHeaderArgumentResolver.java | 46 +++ .../resolvers/RestParamArgumentResolver.java | 104 ++++++ .../RestPathParamArgumentResolver.java | 41 +++ .../wrapper/DefaultRestResultMapping.java | 67 ++++ .../rest/registry/wrapper/RestResult.java | 37 ++ .../group/flyfish/rest/utils/DataUtils.java | 112 ++++++ .../group/flyfish/rest/utils/IocBeans.java | 95 ++++++ .../group/flyfish/rest/utils/JacksonUtil.java | 150 ++++++++ .../flyfish/rest/utils/RequestContext.java | 90 +++++ .../flyfish/rest/utils/TypeResolveUtils.java | 33 ++ .../group/flyfish/rest/RestClientTest.java | 54 +++ .../group/flyfish/rest/RestProxyTest.java | 71 ++++ .../group/flyfish/rest/SimpleProxyTest.java | 59 ++++ .../rest/container/RestTestContainer.java | 19 ++ .../flyfish/rest/domain/DataDirectoryVo.java | 19 ++ .../group/flyfish/rest/domain/TestItem.java | 18 + .../group/flyfish/rest/domain/TestResult.java | 18 + .../rest/mapping/TestRestResultMapping.java | 42 +++ .../src/test/resources/application.yml | 9 + 63 files changed, 3872 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 rest-proxy-api/pom.xml create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/AutoMapping.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestApi.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestBody.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestHeader.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParam.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParams.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestPathParam.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestService.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/enums/HttpMethod.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/enums/ResponseType.java create mode 100644 rest-proxy-api/src/main/java/group/flyfish/rest/mapping/RestResultMapping.java create mode 100644 rest-proxy-core/pom.xml create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/annotation/EnableRestApiProxy.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/client/RestClient.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientConfiguration.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientProperties.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/constants/RestConstants.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/ThreadPoolManager.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/MapParamBuilder.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/RestClientBuilder.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/TypedMapBuilder.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/entity/Multipart.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/exception/RestClientException.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/produce/HttpClientProducer.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpDeleteResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpGetResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpMethodResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPatchResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPostResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPutResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractBodyResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractParamResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestApiRegistry.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestService.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/RestProxyInvoker.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/ArgumentResolveContext.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolverComposite.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/UrlCompiler.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestBodyArgumentResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestHeaderArgumentResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestParamArgumentResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestPathParamArgumentResolver.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/DefaultRestResultMapping.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/RestResult.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/utils/DataUtils.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/utils/IocBeans.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/utils/JacksonUtil.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/utils/RequestContext.java create mode 100644 rest-proxy-core/src/main/java/group/flyfish/rest/utils/TypeResolveUtils.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/RestClientTest.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/RestProxyTest.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/SimpleProxyTest.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/container/RestTestContainer.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/domain/DataDirectoryVo.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestItem.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestResult.java create mode 100644 rest-proxy-core/src/test/java/group/flyfish/rest/mapping/TestRestResultMapping.java create mode 100644 rest-proxy-core/src/test/resources/application.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fa1659 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +.idea + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..479a8db --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# REST请求代理(Http Interface) + +这可能是你用过最爽的http对接神器。 + +## 特性列表 +- 无感知自动注入,只需interface + 注解即可轻松完成对接 +- 优雅的API,省去了您的学习烦恼 +- 高性能实现,智能结果处理绑定 +- 支持文件上传下载无感对接 +- 自动解包结果包装类 +- 自动转换参数、请求体 +- 自定义鉴权(施工中) +- 动态替换实现(施工中) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..87b5d98 --- /dev/null +++ b/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.7 + + + + group.flyfish + rest-proxy + 1.0.0 + + + 8 + 8 + UTF-8 + 1.8 + 2.0.22 + 4.4 + 2.6 + 1.0.0 + + + pom + + rest-proxy-api + rest-proxy-core + + + + + org.projectlombok + lombok + + + + + + + org.apache.commons + commons-collections4 + ${commons-collection.version} + + + commons-io + commons-io + 2.6 + + + org.reflections + reflections + 0.10.2 + + + com.alibaba + fastjson + ${fastjson.version} + + + group.flyfish + rest-proxy-api + ${sdk.version} + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + diff --git a/rest-proxy-api/pom.xml b/rest-proxy-api/pom.xml new file mode 100644 index 0000000..f12e847 --- /dev/null +++ b/rest-proxy-api/pom.xml @@ -0,0 +1,21 @@ + + + + group.flyfish + rest-proxy + 1.0.0 + + 4.0.0 + + rest-proxy-api + + + + org.springframework + spring-context + + + + diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/AutoMapping.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/AutoMapping.java new file mode 100644 index 0000000..7b20549 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/AutoMapping.java @@ -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; +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestApi.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestApi.java new file mode 100644 index 0000000..f9cacd2 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestApi.java @@ -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 ""; + + /** + * 请求方法 + * + * @return 结果 + */ + HttpMethod method() default HttpMethod.GET; + + /** + * 多个参数时使用合并的body + * + * @return 结果 + */ + boolean mergedBody() default false; + + /** + * 可选指定的url,不会从默认地址请求 + * + * @return url + */ + String url() default ""; + + /** + * 请求uri,使用次标注必须指定BaseUrl或者配置(现在还不支持) + * + * @return uri + */ + @AliasFor("value") + String uri() default ""; + + /** + * 基本路径,包含host + * + * @return baseUrl + */ + String baseUrl() default ""; + + /** + * 是否带上认证token + * + * @return 结果 + */ + boolean credentials() default false; +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestBody.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestBody.java new file mode 100644 index 0000000..3701ad8 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestBody.java @@ -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 { +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestHeader.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestHeader.java new file mode 100644 index 0000000..42ceba9 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestHeader.java @@ -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 ""; +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParam.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParam.java new file mode 100644 index 0000000..ce7154c --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParam.java @@ -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 ""; +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParams.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParams.java new file mode 100644 index 0000000..8af0f0c --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestParams.java @@ -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 { + +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestPathParam.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestPathParam.java new file mode 100644 index 0000000..5ff44c7 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestPathParam.java @@ -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 ""; +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestService.java b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestService.java new file mode 100644 index 0000000..50e18e4 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/annotation/RestService.java @@ -0,0 +1,35 @@ +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 结果 + */ + long timeout() default -1L; +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/enums/HttpMethod.java b/rest-proxy-api/src/main/java/group/flyfish/rest/enums/HttpMethod.java new file mode 100644 index 0000000..74b377e --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/enums/HttpMethod.java @@ -0,0 +1,9 @@ +package group.flyfish.rest.enums; + +/** + * Http请求类型 + * @author wangyu + */ +public enum HttpMethod { + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/enums/ResponseType.java b/rest-proxy-api/src/main/java/group/flyfish/rest/enums/ResponseType.java new file mode 100644 index 0000000..b5d7f07 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/enums/ResponseType.java @@ -0,0 +1,10 @@ +package group.flyfish.rest.enums; + +/** + * 响应类型 + * + * @author wangyu + */ +public enum ResponseType { + NORMAL, TEXT, JSON, BINARY, OBJECT +} diff --git a/rest-proxy-api/src/main/java/group/flyfish/rest/mapping/RestResultMapping.java b/rest-proxy-api/src/main/java/group/flyfish/rest/mapping/RestResultMapping.java new file mode 100644 index 0000000..6b3f421 --- /dev/null +++ b/rest-proxy-api/src/main/java/group/flyfish/rest/mapping/RestResultMapping.java @@ -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, RestResultMapping> MAPPINGS = new HashMap<>(); + + /** + * 模糊的结果映射 + * + * @param result 结果 + * @param 泛型 + * @return 映射后的结果 + */ + T map(Object result); + + /** + * 解析返回类型 + * + * @param resultType 返回类型 + * @return 结果 + */ + Type resolve(Type resultType); +} diff --git a/rest-proxy-core/pom.xml b/rest-proxy-core/pom.xml new file mode 100644 index 0000000..1a947d8 --- /dev/null +++ b/rest-proxy-core/pom.xml @@ -0,0 +1,90 @@ + + + + group.flyfish + rest-proxy + 1.0.0 + + 4.0.0 + + rest-proxy-core + + + + group.flyfish + rest-proxy-api + ${sdk.version} + + + org.slf4j + slf4j-api + + + org.springframework.boot + spring-boot + + + com.fasterxml.jackson.core + jackson-databind + + + javax.servlet + javax.servlet-api + + + org.springframework + spring-web + + + org.reflections + reflections + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpmime + + + commons-io + commons-io + + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + test + + + + + + + ${project.basedir}/src/test/resources + + dev/* + prod/* + + + + ${project.basedir}/src/test/resources/${config.dir} + + + + diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/annotation/EnableRestApiProxy.java b/rest-proxy-core/src/main/java/group/flyfish/rest/annotation/EnableRestApiProxy.java new file mode 100644 index 0000000..7daea5f --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/annotation/EnableRestApiProxy.java @@ -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"; +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/client/RestClient.java b/rest-proxy-core/src/main/java/group/flyfish/rest/client/RestClient.java new file mode 100644 index 0000000..03ff79c --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/client/RestClient.java @@ -0,0 +1,323 @@ +package group.flyfish.rest.client; + +import group.flyfish.rest.core.builder.RestClientBuilder; +import group.flyfish.rest.core.exception.RestClientException; +import group.flyfish.rest.core.produce.HttpClientProducer; +import group.flyfish.rest.enums.ResponseType; +import group.flyfish.rest.utils.JacksonUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +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.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +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请求客户端 + * + * @author Mr.Wang + *

+ * @apiNote 1. 全builder调用,用户系统内部相互通信 + * 2. 支持异步回调 + * 3. 多样性组合 + * 4. 解耦实现 + * 5. 支持上传文件、FormData,JSON、支持自定义无侵入扩展 + * 6. 新增单例httpClient模式,复用连接让应用更高效 + */ +@Slf4j +public final class RestClient { + + private static volatile CloseableHttpClient client; + private final HttpRequestBase request; + private boolean async = false; + private Consumer consumer; + private Consumer errorConsumer; + private ExecutorService executorService; + private ResponseType responseType = ResponseType.NORMAL; + private Class resultClass; + private TypeReference typeReference; + private JavaType resultType; + + /** + * 内部构造方法,不对外公开 + * + * @param request 请求信息 + */ + public RestClient(HttpRequestBase request) { + this.request = request; + } + + /** + * 设置单例的客户端 + * + * @param client 客户端 + */ + public static void setClient(CloseableHttpClient client) { + RestClient.client = client; + } + + /** + * 新增一个构建器 + * + * @return 结果 + */ + public static RestClientBuilder create() { + return new RestClientBuilder(); + } + + /** + * 设置请求失败时的回调 + * + * @param errorConsumer 错误回调 + * @return 结果 + */ + public RestClient onError(Consumer errorConsumer) { + this.errorConsumer = errorConsumer; + return this; + } + + /** + * 错误处理 + * + * @param e 异常 + */ + private void handleError(RestClientException e) { + if (null != errorConsumer) { + errorConsumer.accept(e); + } else { + throw e; + } + } + + /** + * 设置响应类型 + * + * @param responseType 响应类型 + * @return 结果 + */ + public RestClient responseType(ResponseType responseType) { + this.responseType = responseType; + return this; + } + + /** + * 标记线程池执行 + * + * @return 结果 + */ + public RestClient async() { + this.async = true; + this.executorService = DEFAULT_EXECUTOR; + return this; + } + + /** + * 标记指定线程池执行 + * + * @param executorService 线程池 + * @return 结果 + */ + public RestClient async(ExecutorService executorService) { + this.async = true; + this.executorService = executorService; + return this; + } + + /** + * 异步执行,接收结果 + * + * @param consumer 结果 + */ + public void execute(Consumer consumer) { + this.consumer = consumer; + if (this.async) { + if (this.executorService == null) { + handleError(new RestClientException("线程池未指定或为空!")); + } + this.executorService.submit(this::executeSafety); + } else { + executeSafety(); + } + } + + /** + * 静默执行,抛弃全部异常 + */ + public void executeSilent() { + executeSafety(); + } + + /** + * 执行请求,返回Map + * + * @return map + * @throws IOException 异常 + */ + public Map executeForMap() throws IOException { + this.responseType = ResponseType.JSON; + return innerExecute(); + } + + /** + * 执行请求,返回字符串 + * + * @return 字符串 + * @throws IOException 异常 + */ + public String executeForString() throws IOException { + this.responseType = ResponseType.TEXT; + return innerExecute(); + } + + /** + * 安全的执行 + */ + private void executeSafety() { + try { + execute(); + } catch (IOException e) { + handleError(new RestClientException(e.getMessage(), e, null)); + } + } + + /** + * 执行并序列化,该方法会安全的返回对象或者空 + * + * @param clazz 类 + * @param 泛型 + * @return 结果 + */ + @Nullable + public T execute(Class clazz) { + this.responseType = ResponseType.OBJECT; + this.resultClass = clazz; + try { + return innerExecute(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 执行并序列化,使用复杂的自动构造的类型 + * + * @param type jackson的强化类型 + * @param 泛型 + * @return 结果 + */ + @Nullable + public T execute(JavaType type) { + this.responseType = ResponseType.OBJECT; + this.resultType = type; + try { + return innerExecute(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @Nullable + public T execute(TypeReference typeReference) { + this.responseType = ResponseType.OBJECT; + this.typeReference = typeReference; + try { + return innerExecute(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 执行请求,返回响应实体,自行处理 + * + * @return 响应实体 + * @throws IOException 异常 + */ + public T execute() throws IOException { + return innerExecute(); + } + + /** + * 内部执行方法,预处理结果 + * + * @param 泛型 + * @return 结果 + */ + private T innerExecute() throws IOException { + if (null == client) { + client = HttpClientProducer.produce(); + } + log.info("【Rest Invoke】{} {}", request.getMethod(), request.getURI()); + try (CloseableHttpResponse response = client.execute(request)) { + StatusLine statusLine = response.getStatusLine(); + if (200 == statusLine.getStatusCode()) { + HttpEntity entity = response.getEntity(); + if (consumer != null) { + consumer.accept(entity); + } + return resolveResponse(entity); + } else { + int requestCode = statusLine.getStatusCode(); + log.error(request.getURI() + "接口调用失败,code:" + requestCode); + handleError(new RestClientException("网络请求状态异常!代码:" + requestCode, null, statusLine)); + } + } catch (UnknownHostException e) { + handleError(new RestClientException(e.getMessage(), e, null)); + log.error("未知的请求地址!"); + } finally { + request.releaseConnection(); + } + return null; + } + + /** + * 解析结果 + * + * @param entity 响应体 + * @param 泛型 + * @return 结果 + */ + @SuppressWarnings("unchecked") + private 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; + } + } + +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientConfiguration.java b/rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientConfiguration.java new file mode 100644 index 0000000..da67a01 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientConfiguration.java @@ -0,0 +1,77 @@ +package group.flyfish.rest.configuration; + +import group.flyfish.rest.annotation.EnableRestApiProxy; +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.RestBodyArgumentResolver; +import group.flyfish.rest.registry.proxy.support.resolvers.RestHeaderArgumentResolver; +import group.flyfish.rest.registry.proxy.support.resolvers.RestParamArgumentResolver; +import group.flyfish.rest.registry.proxy.support.resolvers.RestPathParamArgumentResolver; +import group.flyfish.rest.utils.DataUtils; +import group.flyfish.rest.utils.IocBeans; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Lazy; + +import java.util.Arrays; +import java.util.List; + +/** + * rest请求相关配置 + * + * @author wangyu + */ +@EnableConfigurationProperties(RestClientProperties.class) +public class RestClientConfiguration { + + /** + * 注册rest自动代理 + * + * @return 结果 + */ + @Bean + @Lazy + public RestApiRegistry restApiRegistry(RestArgumentResolverComposite composite, + List mappings) { + // 先注册映射们 + if (DataUtils.isNotEmpty(mappings)) { + mappings.forEach(mapping -> RestResultMapping.MAPPINGS.put(mapping.getClass(), mapping)); + } + // 最后实例化 + return new RestApiRegistry(composite); + } + + /** + * IOC容器操作 + * + * @return 结果 + */ + @Bean + @ConditionalOnMissingBean(name = "iocBeans") + public IocBeans iocBeans() { + return new IocBeans(); + } + + /** + * 一个很重要的bean,反向解析各种参数 + * + * @return 结果 + */ + @Bean + public RestArgumentResolverComposite restArgumentResolverComposite() { + List resolvers = Arrays.asList( + new RestPathParamArgumentResolver(), + new RestBodyArgumentResolver(), + new RestHeaderArgumentResolver(), + new RestParamArgumentResolver() + ); + return new RestArgumentResolverComposite(resolvers); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientProperties.java b/rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientProperties.java new file mode 100644 index 0000000..2fbedf8 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/configuration/RestClientProperties.java @@ -0,0 +1,67 @@ +package group.flyfish.rest.configuration; + +import group.flyfish.rest.registry.RestApiRegistry; +import group.flyfish.rest.utils.DataUtils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +/** + * 客户端的配置 + * + * @author wangyu + * 整合入spring boot,简化配置,提高业务复用性 + */ +@Data +@ConfigurationProperties(prefix = "rest.client", ignoreUnknownFields = false) +@Slf4j +public class RestClientProperties implements InitializingBean { + + @Resource + private RestApiRegistry restApiRegistry; + + /** + * 超时时间,默认30s + */ + private Duration connectionTimeout = Duration.ofSeconds(30); + + /** + * 基本url + */ + private String baseUrl; + + /** + * ssl无条件新人 + */ + private Boolean alwaysTrust = true; + + /** + * 定义的内容字典,可以支持动态取值,使用#variable + */ + private Map urls = new HashMap<>(); + + /** + * 获取字典url + * + * @param key 键 + * @return 结果 + */ + public String getDictUrl(String key) { + if (DataUtils.isEmpty(urls)) { + return null; + } + return urls.get(key); + } + + @Override + public void afterPropertiesSet() throws Exception { + log.info("initialized rest client properties。 baseUrl is:{}", this.baseUrl); + restApiRegistry.initialize(this); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/constants/RestConstants.java b/rest-proxy-core/src/main/java/group/flyfish/rest/constants/RestConstants.java new file mode 100644 index 0000000..7161203 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/constants/RestConstants.java @@ -0,0 +1,209 @@ +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 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 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 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 resolverBuilder() { + return TypedMapBuilder.builder(); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/ThreadPoolManager.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/ThreadPoolManager.java new file mode 100644 index 0000000..4226685 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/ThreadPoolManager.java @@ -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; + } + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/MapParamBuilder.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/MapParamBuilder.java new file mode 100644 index 0000000..55ba2e0 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/MapParamBuilder.java @@ -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 params; + + private MapParamBuilder() { + this.params = new HashMap<>(); + } + + public static MapParamBuilder builder() { + return new MapParamBuilder(); + } + + public static MapParamBuilder of(Map 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 any(Map 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 params) { + if (DataUtils.isNotEmpty(params)) { + params.forEach(this::with); + } + return this; + } + + public MapParamBuilder withPage(Map params) { + if (params.containsKey(PAGE_KEY)) { + this.params.put("page", params.get(PAGE_KEY)); + } + return this; + } + + @SuppressWarnings("unchecked") + public T take(String key) { + return (T) this.params.get(key); + } + + public boolean isEmpty() { + return DataUtils.isEmpty(params); + } + + public Map build() { + return this.params; + } + + @Override + public String toString() { + if (DataUtils.isEmpty(this.params)) { + return EMPTY_PATTERN; + } + return JacksonUtil.toJson(this.params).orElse(null); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/RestClientBuilder.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/RestClientBuilder.java new file mode 100644 index 0000000..11c8d0c --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/RestClientBuilder.java @@ -0,0 +1,199 @@ +package group.flyfish.rest.core.builder; + +import group.flyfish.rest.client.RestClient; +import group.flyfish.rest.core.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.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 params; + + private String body; + + private Map headers; + + private List multipartList; + + private boolean multipart; + + private boolean credential; + + private String charset; + + 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 getParams() { + if (null == params) { + params = new HashMap<>(); + } + return params; + } + + public RestClientBuilder queryParams(Map 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 Charset getCharset() { + return DataUtils.isBlank(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + public RestClientBuilder addMultipartBody(String name, String filename, Object data) { + if (null == this.multipartList) { + this.multipartList = new ArrayList<>(); + } + this.multipartList.add(new Multipart(name, filename, data)); + return this; + } + + public List 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 getHeaders() { + if (null == headers) { + headers = new HashMap<>(); + } + return headers; + } + + public RestClientBuilder headers(Map 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(REQUEST_CONFIG); + // 返回 + return request; + } + + /** + * 构建client + * + * @return 结果 + */ + public RestClient build() { + // 创建请求 + HttpRequestBase request = buildRequest(); + return new RestClient(request); + } + + +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/TypedMapBuilder.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/TypedMapBuilder.java new file mode 100644 index 0000000..84cd495 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/builder/TypedMapBuilder.java @@ -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 { + + private final Map params; + + private TypedMapBuilder() { + this.params = new HashMap<>(); + } + + public static TypedMapBuilder builder() { + return new TypedMapBuilder<>(); + } + + public static TypedMapBuilder stringMapBuilder() { + return new TypedMapBuilder<>(); + } + + public static TypedMapBuilder stringObjectBuilder() { + return new TypedMapBuilder<>(); + } + + public TypedMapBuilder with(K key, V value) { + if (key != null && value != null) { + this.params.put(key, value); + } + return this; + } + + public TypedMapBuilder withAll(Map values) { + values.forEach(this::with); + return this; + } + + public Map build() { + return params; + } + + @Override + public String toString() { + return JacksonUtil.toJson(this).orElse(null); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/entity/Multipart.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/entity/Multipart.java new file mode 100644 index 0000000..c713520 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/entity/Multipart.java @@ -0,0 +1,22 @@ +package group.flyfish.rest.core.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; +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/exception/RestClientException.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/exception/RestClientException.java new file mode 100644 index 0000000..9e1aa56 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/exception/RestClientException.java @@ -0,0 +1,40 @@ +package group.flyfish.rest.core.exception; + +/** + * 异常类,用于包装异常 + */ +public class RestClientException extends RuntimeException { + + private static final long serialVersionUID = 4741281547788724661L; + private Exception nested; + + private Object bind; + + public RestClientException(String message, Exception nested) { + super(message); + this.nested = nested; + } + + public RestClientException(String message, Exception nested, Object bind) { + super(message); + this.nested = nested; + this.bind = bind; + } + + public RestClientException(String message) { + super(message); + } + + @SuppressWarnings("unchecked") + public T getBind() { + return (T) bind; + } + + public void setBind(Object bind) { + this.bind = bind; + } + + public Exception getNested() { + return nested; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/produce/HttpClientProducer.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/produce/HttpClientProducer.java new file mode 100644 index 0000000..2a0035c --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/produce/HttpClientProducer.java @@ -0,0 +1,101 @@ +package group.flyfish.rest.core.produce; + +import group.flyfish.rest.configuration.RestClientProperties; +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 javax.net.ssl.SSLContext; +import java.io.NotActiveException; +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 HttpClientProducer { + + // 使用非公平锁 + private static final ReentrantLock lock = new ReentrantLock(); + // 客户端实例,单例 + private static volatile CloseableHttpClient client; + // 配置,配置没进来就不初始化 + private static RestClientProperties properties; + + /** + * 配置信息 + * + * @param properties 属性 + */ + public static void configure(RestClientProperties properties) { + HttpClientProducer.properties = properties; + } + + /** + * 获取属性 + * + * @return 结果 + */ + public static RestClientProperties getProperties() { + return properties; + } + + /** + * 生产客户端 + * + * @return 结果 + */ + public static CloseableHttpClient produce() throws NotActiveException { + if (properties == null) { + log.warn("【rest客户端】未初始化rest客户端配置,生产者未就绪!将使用默认配置!!!"); + properties = new RestClientProperties(); + } + if (client == null) { + // 非公平锁,二次判定,定位volatile + lock.lock(); + try { + if (client == null) { + client = getClient(); + } + } finally { + lock.unlock(); + } + } + return client; + } + + /** + * 构建单例的httpClient + * + * @return 结果 + */ + private static CloseableHttpClient getClient() { + return DataUtils.isTrue(properties.getAlwaysTrust()) ? createSSLClient() : HttpClients.createDefault(); + } + + /** + * 不信任的证书请求客户端 + * + * @return 结果 + */ + private 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; + } + +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpDeleteResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpDeleteResolver.java new file mode 100644 index 0000000..5769543 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpDeleteResolver.java @@ -0,0 +1,26 @@ +package group.flyfish.rest.core.resolver; + +import group.flyfish.rest.core.builder.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) { + resolveParams(builder); + return new HttpDelete(builder.getUrl()); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpGetResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpGetResolver.java new file mode 100644 index 0000000..4b52feb --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpGetResolver.java @@ -0,0 +1,18 @@ +package group.flyfish.rest.core.resolver; + +import group.flyfish.rest.core.builder.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) { + resolveParams(builder); + return new HttpGet(builder.getUrl()); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpMethodResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpMethodResolver.java new file mode 100644 index 0000000..f62fad0 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpMethodResolver.java @@ -0,0 +1,18 @@ +package group.flyfish.rest.core.resolver; + +import group.flyfish.rest.core.builder.RestClientBuilder; +import org.apache.http.client.methods.HttpRequestBase; + +/** + * Http请求解析器 + */ +public interface HttpMethodResolver { + + /** + * 解析请求 + * + * @param builder 构建器 + * @return 结果 + */ + HttpRequestBase resolve(RestClientBuilder builder); +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPatchResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPatchResolver.java new file mode 100644 index 0000000..7c61077 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPatchResolver.java @@ -0,0 +1,27 @@ +package group.flyfish.rest.core.resolver; + +import group.flyfish.rest.core.builder.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; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPostResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPostResolver.java new file mode 100644 index 0000000..333b5b8 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPostResolver.java @@ -0,0 +1,20 @@ +package group.flyfish.rest.core.resolver; + +import group.flyfish.rest.core.builder.RestClientBuilder; +import group.flyfish.rest.core.resolver.support.AbstractBodyResolver; +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) { + HttpPost post = new HttpPost(builder.getUrl()); + post.setEntity(buildEntity(builder)); + return post; + } + +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPutResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPutResolver.java new file mode 100644 index 0000000..6812105 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/HttpPutResolver.java @@ -0,0 +1,27 @@ +package group.flyfish.rest.core.resolver; + +import group.flyfish.rest.core.builder.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; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractBodyResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractBodyResolver.java new file mode 100644 index 0000000..d223830 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractBodyResolver.java @@ -0,0 +1,104 @@ +package group.flyfish.rest.core.resolver.support; + +import group.flyfish.rest.core.builder.RestClientBuilder; +import group.flyfish.rest.utils.DataUtils; +import org.apache.commons.io.FilenameUtils; +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.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 { + + /** + * 构建entity + * + * @param builder 构建器 + * @return 结果 + */ + protected HttpEntity buildEntity(RestClientBuilder builder) { + return builder.isMultipart() ? buildMultipart(builder) + : DataUtils.isNotBlank(builder.getBody()) ? buildJson(builder) + : buildFormData(builder); + } + + /** + * 构建上传数据 + * + * @param clientBuilder builder + * @return 结果 + */ + private HttpEntity buildMultipart(RestClientBuilder clientBuilder) { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + 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, + ContentType.create(MIME_MAP.getOrDefault(FilenameUtils.getExtension(filename), + ContentType.APPLICATION_OCTET_STREAM.getMimeType())), filename); + } else if (data instanceof File) { + builder.addBinaryBody(name, (File) data, + ContentType.create(MIME_MAP.getOrDefault(FilenameUtils.getExtension(filename), + ContentType.APPLICATION_OCTET_STREAM.getMimeType())), filename); + } else { + throw new IllegalArgumentException("上传时,输入的数据不被支持!"); + } + }); + 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 params = clientBuilder.getParams(); + List 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; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractParamResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractParamResolver.java new file mode 100644 index 0000000..a6ea872 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/core/resolver/support/AbstractParamResolver.java @@ -0,0 +1,29 @@ +package group.flyfish.rest.core.resolver.support; + +import group.flyfish.rest.core.builder.RestClientBuilder; +import group.flyfish.rest.utils.DataUtils; + +import java.util.stream.Collectors; + +/** + * 抽象的参数解析逻辑 + * + * @author wangyu + */ +public abstract class AbstractParamResolver { + + /** + * 解析参数 + * + * @param builder 构建器 + */ + protected void resolveParams(RestClientBuilder builder) { + if (DataUtils.isNotEmpty(builder.getParams())) { + String start = builder.getUrl().contains("?") ? "&" : "?"; + String params = builder.getParams().entrySet().stream() + .map(entry -> entry.getKey() + "=" + (null == entry.getValue() ? "" : entry.getValue())) + .collect(Collectors.joining("&")); + builder.url(builder.getUrl() + start + params); + } + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestApiRegistry.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestApiRegistry.java new file mode 100644 index 0000000..fd562e2 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestApiRegistry.java @@ -0,0 +1,136 @@ +package group.flyfish.rest.registry; + +import group.flyfish.rest.annotation.EnableRestApiProxy; +import group.flyfish.rest.annotation.RestService; +import group.flyfish.rest.client.RestClient; +import group.flyfish.rest.configuration.RestClientProperties; +import group.flyfish.rest.core.produce.HttpClientProducer; +import group.flyfish.rest.registry.proxy.RestProxyInvoker; +import group.flyfish.rest.registry.proxy.support.RestArgumentResolverComposite; +import group.flyfish.rest.utils.DataUtils; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.reflections.Reflections; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.*; +import org.springframework.util.Assert; + +import java.io.NotActiveException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * rest接口注册机 + * + * @author wangyu + */ +@RequiredArgsConstructor +@Slf4j +public class RestApiRegistry implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware { + + private final RestArgumentResolverComposite composite; + + // 包名 + private final List packageNames = new ArrayList<>(); + + // 注册的执行器们,最终会使用配置进行初始化 + private final List> invokers = new ArrayList<>(); + + // bean工厂 + private ConfigurableListableBeanFactory beanFactory; + + // rest请求参数 + private RestClientProperties properties; + + /** + * 动态注册bean + * + * @param registry 注册机 + * @throws BeansException 异常 + */ + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + // 找基本包,找不到立马报错 + beanFactory.getBeansWithAnnotation(EnableRestApiProxy.class) + .forEach((key, value) -> { + EnableRestApiProxy proxy = value.getClass().getAnnotation(EnableRestApiProxy.class); + for (String basePackage : proxy.basePackages()) { + if (DataUtils.isNotBlank(basePackage)) { + packageNames.add(basePackage); + } + } + }); + // 不为空时查找 + if (DataUtils.isNotEmpty(packageNames)) { + // 初始化反射 + try { + Reflections reflections = new Reflections(packageNames.toArray()); + // 得到Resource注解的类 + Set> classSet = reflections.getTypesAnnotatedWith(RestService.class); + // 代理并生成子类,并注册到ioc容器 + classSet.forEach(clazz -> registry.registerBeanDefinition(clazz.getName(), + BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> DataUtils.cast(RestProxyInvoker + .produce(clazz, composite, this::bindInvoker))) + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE).getRawBeanDefinition()) + ); + } catch (IllegalStateException e) { + log.error("初始化Rest映射时出错", e); + } + return; + } + throw new BeanDefinitionValidationException("【RestApi】EnableRestApiProxy注解必须指定有效的basePackage!"); + } + + /** + * 初始化bean们 + * + * @param properties 属性 + * @throws NotActiveException 未活动异常 + */ + public void initialize(RestClientProperties properties) throws NotActiveException { + log.info("bean加载完成"); + // 真正加载完成的回调 + this.properties = properties; + // 配置参数 + HttpClientProducer.configure(properties); + // 最后再配置,生成 + RestClient.setClient(HttpClientProducer.produce()); + // 初始化 + this.invokers.forEach(invoker -> invoker.configure(properties)); + } + + /** + * 绑定执行器 + * + * @param invoker 执行器 + * @param 泛型 + */ + private void bindInvoker(RestProxyInvoker invoker) { + if (properties == null) { + this.invokers.add(invoker); + } else { + invoker.configure(properties); + } + } + + @SneakyThrows + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + // do nothing + } + + public void setBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.isTrue(beanFactory instanceof ConfigurableListableBeanFactory, "当前bean factory不被支持!"); + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestService.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestService.java new file mode 100644 index 0000000..8a2ff0d --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/RestService.java @@ -0,0 +1,12 @@ +package group.flyfish.rest.registry; + +/** + * 用于标记rest服务代理 + * + * @author wangyu + * @deprecated 该类已经过时,请使用新版的注解声明 + * @see group.flyfish.rest.annotation.RestService + */ +@Deprecated +public interface RestService { +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/RestProxyInvoker.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/RestProxyInvoker.java new file mode 100644 index 0000000..3ae9727 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/RestProxyInvoker.java @@ -0,0 +1,232 @@ +package group.flyfish.rest.registry.proxy; + +import group.flyfish.rest.annotation.AutoMapping; +import group.flyfish.rest.annotation.RestApi; +import group.flyfish.rest.annotation.RestService; +import group.flyfish.rest.client.RestClient; +import group.flyfish.rest.configuration.RestClientProperties; +import group.flyfish.rest.core.builder.RestClientBuilder; +import group.flyfish.rest.mapping.RestResultMapping; +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 com.fasterxml.jackson.databind.JavaType; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ClassUtils; + +import java.beans.Transient; +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.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Rest代理执行器 + * + * @author wangyu + */ +public class RestProxyInvoker implements InvocationHandler { + + // 要代理的目标类 + private final Class targetType; + // 解析集合 + private final RestArgumentResolverComposite composite; + // 初始的基本路径 + private String baseUrl; + // 基础url + private RestClientProperties properties; + // 结果映射 + private RestResultMapping mapping; + + /** + * 构造器 + * + * @param targetType 目标类型 + * @param composite 解析器 + */ + public RestProxyInvoker(Class targetType, RestArgumentResolverComposite composite) { + this.targetType = targetType; + this.composite = composite; + } + + /** + * 生产一个实现类 + * + * @param target 目标 + * @param 泛型 + * @return 结果 + */ + public static T produce(Class target, RestArgumentResolverComposite composite, Consumer> consumer) { + RestProxyInvoker invoker = new RestProxyInvoker<>(target, composite); + consumer.accept(invoker); + return DataUtils.cast(Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, invoker)); + } + + /** + * 使用配置信息对接口进行配置 + * + * @param properties 配置 + */ + public void configure(RestClientProperties properties) { + this.properties = properties; + this.baseUrl = this.findBaseUrl(); + } + + + /** + * 执行rest请求的地方,这里很简单易懂 + * + * @param proxy 代理对象 + * @param method 代理方法 + * @param args 参数 + * @return 结果 + * @throws Throwable 可能抛出的异常 + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 处理基本方法被代理的情况 + 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; + } + // 无视proxy,因为啥也没 + RestApi restApi = AnnotationUtils.findAnnotation(method, RestApi.class); + if (null == restApi) { + throw new IllegalAccessException("【Rest调用】未声明rest配置的方法被调用!请检查代码!"); + } + // 第一步就解析参数 + ArgumentResolveContext context = composite.resolve(restApi, method, args); + // 构造和调用,这里的restClient不保存状态 + RestClientBuilder builder = RestClient.create() + .url(determineUrl(restApi, context)) + .method(restApi.method()); + // 需要带cookie的带上 + if (restApi.credentials()) { + builder.withCredential(); + } + // 判断情况,赋值参数 + if (context.hasBody()) { + builder.body(context.getBody()); + } + // 赋值参数们 + if (context.hasParams()) { + builder.queryParams(context.getParam()); + } + // 赋值头 + if (context.hasHeaders()) { + builder.headers(context.getHeaders()); + } + // 赋值头部 + RestClient client = builder.build(); + // 是否对结果进行映射 + boolean map = null != mapping && null == AnnotationUtils.findAnnotation(method, Transient.class); + // 执行请求 + Object result = execute(client, method, map); + // 结果映射 + return map ? mapping.map(result) : result; + } + + /** + * 最终执行的方法 + * + * @param client rest客户端实例 + * @param method 原方法实例 + * @param map 是否映射结果 + * @return 结果 + */ + private Object execute(RestClient client, Method 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() { + // 注解的优先级高于全局基本路径 + RestService restService = AnnotationUtils.findAnnotation(targetType, RestService.class); + // 当且仅当存在时进入 + 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 determineUrl(RestApi restApi, ArgumentResolveContext context) { + String url; + // 解析url以支持PathVariable + if (DataUtils.isNotBlank(restApi.url())) { + url = restApi.url(); + } else { + // 构建基础url,优先级从小到大依次找。同时尝试取字典值 + Optional baseUrl = Stream.of(restApi.baseUrl(), this.baseUrl) + .filter(DataUtils::isNotBlank) + .findFirst() + .map(found -> found.startsWith("#") ? properties.getDictUrl(found.substring(1)) : found); + // 判定和赋值 + if (baseUrl.isPresent() && DataUtils.isNotBlank(restApi.uri())) { + url = baseUrl.get() + restApi.uri(); + } else { + throw new IllegalArgumentException("【Rest调用】未指定url或baseurl,无法调用远端服务器!"); + } + } + // 尝试解析路径参数 + return context.hasPathParams() ? UrlCompiler.compile(url, context.getPathParams()) : url; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/ArgumentResolveContext.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/ArgumentResolveContext.java new file mode 100644 index 0000000..f88d4e6 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/ArgumentResolveContext.java @@ -0,0 +1,100 @@ +package group.flyfish.rest.registry.proxy.support; + +import group.flyfish.rest.annotation.RestApi; +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 RestApi annotation; + + // 参数 + private Map param; + + // 路径参数 + private Map pathParams; + + // 请求头 + private Map headers; + + // 请求体 + private Object body; + + // 设置参数 + 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 map = DataUtils.cast(body); + map.put(key, value); + } + + /** + * 设置头部 + * + * @param headers 头 + */ + public void setHeaders(Map 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); + } + + // 设置路径参数 + public void setPathParam(String key, Object value) { + if (DataUtils.isEmpty(pathParams)) { + pathParams = new HashMap<>(); + } + pathParams.put(key, value); + } + + 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); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolver.java new file mode 100644 index 0000000..edc07b8 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolver.java @@ -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); +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolverComposite.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolverComposite.java new file mode 100644 index 0000000..59596fe --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/RestArgumentResolverComposite.java @@ -0,0 +1,55 @@ +package group.flyfish.rest.registry.proxy.support; + +import group.flyfish.rest.annotation.RestApi; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.List; +import java.util.stream.IntStream; + +/** + * 解析器集合 + * + * @author wangyu + */ +public class RestArgumentResolverComposite { + + // 解析器们 + private final List resolvers; + + public RestArgumentResolverComposite(List resolvers) { + this.resolvers = resolvers; + } + + /** + * 执行解析 + * + * @param annotation 注解 + * @param method 方法 + * @param args 参数 + * @return 结果 + */ + public ArgumentResolveContext resolve(RestApi annotation, Method method, Object[] args) { + // 上下文 + ArgumentResolveContext context = ArgumentResolveContext.builder().annotation(annotation).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)); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/UrlCompiler.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/UrlCompiler.java new file mode 100644 index 0000000..26b60eb --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/UrlCompiler.java @@ -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 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); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestBodyArgumentResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestBodyArgumentResolver.java new file mode 100644 index 0000000..4bd20df --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestBodyArgumentResolver.java @@ -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 null != parameter.getAnnotation(RestBody.class); + } + + /** + * 解析 + * + * @param context 上下文,赋值 + * @param parameter 参数 + * @param value 值 + */ + @Override + public void resolve(ArgumentResolveContext context, Parameter parameter, Object value) { + // 无视合并body,这里的优先级最高 + context.setBody(value); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestHeaderArgumentResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestHeaderArgumentResolver.java new file mode 100644 index 0000000..38ddd6a --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestHeaderArgumentResolver.java @@ -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 null != parameter.getAnnotation(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) : ""); + } + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestParamArgumentResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestParamArgumentResolver.java new file mode 100644 index 0000000..39faf12 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestParamArgumentResolver.java @@ -0,0 +1,104 @@ +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.getAnnotation().mergedBody()) { + context.setBody(name, value); + } else { + context.setParam(name, 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 values = DataUtils.cast(value); + values.forEach((k, v) -> { + if (null != v) { + context.setParam(k, String.valueOf(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(), String.valueOf(v)); + } + } + } + } + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestPathParamArgumentResolver.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestPathParamArgumentResolver.java new file mode 100644 index 0000000..945a8a0 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/proxy/support/resolvers/RestPathParamArgumentResolver.java @@ -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 null != parameter.getAnnotation(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(parameter.getName(), value); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/DefaultRestResultMapping.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/DefaultRestResultMapping.java new file mode 100644 index 0000000..48577a3 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/DefaultRestResultMapping.java @@ -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 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(); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/RestResult.java b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/RestResult.java new file mode 100644 index 0000000..0b4d052 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/registry/wrapper/RestResult.java @@ -0,0 +1,37 @@ +package group.flyfish.rest.registry.wrapper; + +import lombok.Data; + +/** + * 标准的解包结构,等同于全局 + * + * @author wangyu + */ +@Data +public class RestResult { + + /** + * 成功标志 + */ + private boolean success = true; + + /** + * 返回处理消息 + */ + private String message = "操作成功!"; + + /** + * 返回代码 + */ + private Integer code = 0; + + /** + * 返回数据对象 data + */ + private T result; + + /** + * 时间戳 + */ + private long timestamp = System.currentTimeMillis(); +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/utils/DataUtils.java b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/DataUtils.java new file mode 100644 index 0000000..0fe8b20 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/DataUtils.java @@ -0,0 +1,112 @@ +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 { + + public static final int INDEX_NOT_FOUND = -1; + + /** + * 判断字符串是否不为空 + * + * @param target 目标字符串 + * @return 结果 + */ + public static boolean isNotBlank(String target) { + return !StringUtils.isEmpty(target); + } + + /** + * 强制转换和平滑过渡 + * + * @param source 原对象 + * @param 原类型 + * @param 目标类型 + * @return 结果 + */ + @SuppressWarnings("unchecked") + public static 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 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); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/utils/IocBeans.java b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/IocBeans.java new file mode 100644 index 0000000..68b160c --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/IocBeans.java @@ -0,0 +1,95 @@ +package group.flyfish.rest.utils; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.Nullable; + +import java.util.Optional; +import java.util.function.Supplier; + +public class IocBeans implements ApplicationContextAware { + + private static IocBeans instance; + + private ApplicationContext applicationContext; + + public IocBeans() { + instance = this; + } + + public static IocBeans sharedInstance() { + return instance; + } + + public static IocBeans sharedInstance(ApplicationContext applicationContext) { + if (null != instance && null == instance.applicationContext) { + instance.applicationContext = applicationContext; + } + return instance; + } + + + /** + * 注册bean + * + * @param name bean的名称 + * @param beanClass bean的类 + * @param beanSupplier bean实例化 + */ + public static T registerBean(String name, Class beanClass, Supplier beanSupplier) { + return sharedInstance().register(name, beanClass, beanSupplier); + } + + /** + * 注册bean + * + * @param name bean的名称 + * @param beanClass bean的类 + * @param beanSupplier bean实例化 + */ + @SuppressWarnings("unchecked") + public T register(String name, Class beanClass, Supplier beanSupplier) { + // 已经包含bean + if (applicationContext.containsBean(name)) { + Object wired = applicationContext.getBean(name); + if (wired.getClass().isAssignableFrom(beanClass)) { + return (T) wired; + } else { + throw new RuntimeException("注册RMI实例出错,Bean名称重复:" + name); + } + } + // 构建bean定义 + BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(beanClass, beanSupplier) + .getRawBeanDefinition(); + // 写入 + return Optional.ofNullable(getBeanFactory()) + .map(factory -> { + factory.registerBeanDefinition(name, beanDefinition); + return applicationContext.getBean(name, beanClass); + }).orElseThrow(() -> new RuntimeException("注册RMI实例出错,BeanFactory不存在!")); + } + + /** + * 获取beanFactory + * + * @return 结果 + */ + @Nullable + public DefaultListableBeanFactory getBeanFactory() { + BeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory(); + if (beanFactory == null) { + beanFactory = applicationContext.getParentBeanFactory(); + } + return (DefaultListableBeanFactory) beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/utils/JacksonUtil.java b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/JacksonUtil.java new file mode 100644 index 0000000..5e8c1e1 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/JacksonUtil.java @@ -0,0 +1,150 @@ +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 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 final 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); + } + + public static T fromJson(final String json) { + return readValue(json, new TypeReference() { + }); + } + + public static T fromJson(final String json, TypeReference reference) { + return readValue(json, reference); + } + + public static T fromJson(final String json, Class clazz) { + if (null == json || "".equals(json)) { + return null; + } + try { + return mapper.readValue(json, clazz); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public static 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 Map json2Map(String json) { + return readValue(json, new TypeReference>() { + }); + } + + public static List json2List(final String json) { + return readValue(json, new TypeReference>() { + }); + } + + public static List json2List(final String json, Class 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 toJson(final Object obj) { + try { + return Optional.of(mapper.writeValueAsString(obj)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + return Optional.empty(); + } + + private static T readValue(String json, TypeReference 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; + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/utils/RequestContext.java b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/RequestContext.java new file mode 100644 index 0000000..7ce660b --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/RequestContext.java @@ -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 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 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 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 getCookies(Predicate 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 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)); + } +} diff --git a/rest-proxy-core/src/main/java/group/flyfish/rest/utils/TypeResolveUtils.java b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/TypeResolveUtils.java new file mode 100644 index 0000000..2d8d870 --- /dev/null +++ b/rest-proxy-core/src/main/java/group/flyfish/rest/utils/TypeResolveUtils.java @@ -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; + } +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/RestClientTest.java b/rest-proxy-core/src/test/java/group/flyfish/rest/RestClientTest.java new file mode 100644 index 0000000..6d51aa6 --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/RestClientTest.java @@ -0,0 +1,54 @@ +package group.flyfish.rest; + +import group.flyfish.rest.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); + } +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/RestProxyTest.java b/rest-proxy-core/src/test/java/group/flyfish/rest/RestProxyTest.java new file mode 100644 index 0000000..0b13eeb --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/RestProxyTest.java @@ -0,0 +1,71 @@ +package group.flyfish.rest; + +import group.flyfish.rest.annotation.*; +import group.flyfish.rest.container.RestTestContainer; +import group.flyfish.rest.domain.TestItem; +import group.flyfish.rest.enums.HttpMethod; +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 items = testRestService.getCameras("1298064063717781506", "S4NbecfYA1CBGIOB0QDOT4", null); + Map 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/") + @AutoMapping(TestRestResultMapping.class) + public interface TestRestService { + + @RestApi(uri = "/video/{platformId}/cameras/all") + List getCameras(@RestPathParam String platformId, String regionCode, String name1); + } + + + @RestService(baseUrl = "http://220.194.160.4:8083/interface") + @AutoMapping(TestRestResultMapping.class) + public interface TestPostService { + + @RestApi(method = HttpMethod.POST, uri = "/getResources") + Map>> getResources(@RestBody Map body); + } +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/SimpleProxyTest.java b/rest-proxy-core/src/test/java/group/flyfish/rest/SimpleProxyTest.java new file mode 100644 index 0000000..f189de8 --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/SimpleProxyTest.java @@ -0,0 +1,59 @@ +package group.flyfish.rest; + +import group.flyfish.rest.annotation.*; +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 values = new HashMap<>(); + values.put("a", 1); + values.put("b", 2); + TestResult query = new TestResult<>(); + query.setMsg("asdasd"); + Map 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 结果 + */ + @RestApi("/financial/public/income") + Map getDirectories(@RestHeader("Authorization") String token, @RestParam("userId") String userId, + Map others, @RestParams TestResult result); + } +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/container/RestTestContainer.java b/rest-proxy-core/src/test/java/group/flyfish/rest/container/RestTestContainer.java new file mode 100644 index 0000000..d0cf698 --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/container/RestTestContainer.java @@ -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); + } +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/domain/DataDirectoryVo.java b/rest-proxy-core/src/test/java/group/flyfish/rest/domain/DataDirectoryVo.java new file mode 100644 index 0000000..8b71459 --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/domain/DataDirectoryVo.java @@ -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; +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestItem.java b/rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestItem.java new file mode 100644 index 0000000..fc16312 --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestItem.java @@ -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; +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestResult.java b/rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestResult.java new file mode 100644 index 0000000..ac6e3c6 --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/domain/TestResult.java @@ -0,0 +1,18 @@ +package group.flyfish.rest.domain; + +import lombok.Data; + +/** + * 测试用的返回值 + * + * @param 泛型 + */ +@Data +public class TestResult { + + private Integer code; + + private String msg; + + private T data; +} diff --git a/rest-proxy-core/src/test/java/group/flyfish/rest/mapping/TestRestResultMapping.java b/rest-proxy-core/src/test/java/group/flyfish/rest/mapping/TestRestResultMapping.java new file mode 100644 index 0000000..7bfe92e --- /dev/null +++ b/rest-proxy-core/src/test/java/group/flyfish/rest/mapping/TestRestResultMapping.java @@ -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 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); + } +} diff --git a/rest-proxy-core/src/test/resources/application.yml b/rest-proxy-core/src/test/resources/application.yml new file mode 100644 index 0000000..1498622 --- /dev/null +++ b/rest-proxy-core/src/test/resources/application.yml @@ -0,0 +1,9 @@ +rest: + client: + connection-timeout: 30s + 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