From d1d4875f0d19da3b9807e458fc32cfa653fe8d41 Mon Sep 17 00:00:00 2001 From: hongqiaowei Date: Tue, 30 Mar 2021 11:52:12 +0800 Subject: [PATCH] sync master (#21) --- README.en-us.md | 7 +- README.md | 5 +- js/common.js | 27 +- ...chema-validator-i18n-support-1.0.39_4.jar} | Bin 193750 -> 195422 bytes pom.xml | 98 ++++- src/main/java/we/FizzAppContext.java | 3 +- src/main/java/we/FizzGatewayApplication.java | 2 +- src/main/java/we/config/SystemConfig.java | 80 +++- .../java/we/constants/CommonConstants.java | 24 +- src/main/java/we/filter/AggregateFilter.java | 29 +- .../filter/FilterExceptionHandlerConfig.java | 1 + src/main/java/we/filter/FizzWebFilter.java | 16 +- src/main/java/we/filter/PreprocessFilter.java | 18 +- src/main/java/we/fizz/AggregateResource.java | 2 +- src/main/java/we/fizz/AggregateResult.java | 10 +- src/main/java/we/fizz/AggregateService.java | 7 +- src/main/java/we/fizz/ConfigLoader.java | 95 ++++- src/main/java/we/fizz/Pipeline.java | 74 +++- src/main/java/we/fizz/Step.java | 29 +- src/main/java/we/fizz/StepContext.java | 135 ++++-- .../java/we/fizz/exception/FizzException.java | 10 + .../fizz/exception/FizzRuntimeException.java | 7 + .../java/we/fizz/input/ClientInputConfig.java | 10 +- src/main/java/we/fizz/input/IInput.java | 29 ++ src/main/java/we/fizz/input/Input.java | 28 +- src/main/java/we/fizz/input/InputConfig.java | 31 ++ src/main/java/we/fizz/input/InputFactory.java | 65 ++- src/main/java/we/fizz/input/InputType.java | 25 +- src/main/java/we/fizz/input/PathMapping.java | 110 ++++- src/main/java/we/fizz/input/RPCInput.java | 152 +++++++ src/main/java/we/fizz/input/RPCResponse.java | 48 +++ src/main/java/we/fizz/input/RequestInput.java | 392 ------------------ src/main/java/we/fizz/input/ScriptHelper.java | 12 +- .../input/extension/dubbo/DubboInput.java | 231 +++++++++++ .../extension/dubbo/DubboInputConfig.java | 124 ++++++ .../dubbo/DubboRPCResponse.java} | 17 +- .../input/extension/grpc/GRPCResponse.java | 37 ++ .../fizz/input/extension/grpc/GrpcInput.java | 226 ++++++++++ .../input/extension/grpc/GrpcInputConfig.java | 84 ++++ .../input/extension/mysql/MySQLInput.java | 39 ++ .../mysql}/MySQLInputConfig.java | 6 +- .../input/extension/request/RequestInput.java | 381 +++++++++++++++++ .../request}/RequestInputConfig.java | 34 +- .../extension/request/RequestRPCResponse.java | 37 ++ .../java/we/plugin/auth/ApiConfigService.java | 50 ++- .../plugin/auth/GatewayGroup2apiConfig.java | 3 + .../java/we/plugin/auth/ServiceConfig.java | 6 + src/main/java/we/proxy/CallbackService.java | 22 +- src/main/java/we/proxy/FizzWebClient.java | 31 ++ .../dubbo/ApacheDubboGenericService.java | 127 ++++++ .../dubbo/DubboInterfaceDeclaration.java | 87 ++++ src/main/java/we/proxy/dubbo/DubboUtils.java | 58 +++ .../we/proxy/grpc/GrpcGenericService.java | 70 ++++ .../we/proxy/grpc/GrpcInstanceService.java | 32 ++ .../proxy/grpc/GrpcInstanceServiceImpl.java | 217 ++++++++++ .../proxy/grpc/GrpcInterfaceDeclaration.java | 44 ++ .../proxy/grpc/ListenableFutureAdapter.java | 47 +++ .../java/we/proxy/grpc/client/CallParams.java | 48 +++ .../we/proxy/grpc/client/CallResults.java | 57 +++ .../java/we/proxy/grpc/client/GrpcClient.java | 117 ++++++ .../we/proxy/grpc/client/GrpcProxyClient.java | 88 ++++ .../client/core/CompositeStreamObserver.java | 87 ++++ .../proxy/grpc/client/core/DoneObserver.java | 69 +++ .../client/core/DynamicMessageMarshaller.java | 66 +++ .../client/core/GrpcMethodDefinition.java | 60 +++ .../client/core/ServerReflectionClient.java | 256 ++++++++++++ .../grpc/client/core/ServiceResolver.java | 179 ++++++++ .../grpc/client/utils/ChannelFactory.java | 57 +++ .../client/utils/GrpcReflectionUtils.java | 133 ++++++ .../grpc/client/utils/MessageWriter.java | 54 +++ src/main/java/we/util/MapUtil.java | 43 +- src/main/java/we/util/WebUtils.java | 160 ++++--- src/main/resources/application.yml | 12 +- src/main/resources/js/common.js | 27 +- .../we/filter/FlowControlFilterTests.java | 2 +- src/test/java/we/fizz/group/DevTestGroup.java | 4 + .../java/we/fizz/group/FastTestGroup.java | 4 + .../java/we/fizz/group/SlowTestGroup.java | 4 + .../we/fizz/input/DubboInputMockTests.java | 88 ++++ .../java/we/fizz/input/DubboInputTests.java | 44 ++ .../we/fizz/input/GrpcInputMockTests.java | 120 ++++++ .../java/we/fizz/input/PathMappingTests.java | 63 ++- .../java/we/fizz/input/RequestInputTests.java | 1 + .../ApacheDubboGenericServiceMockTests.java | 58 +++ .../dubbo/ApacheDubboGenericServiceTests.java | 55 +++ src/test/java/we/util/WebUtilsTests.java | 63 +-- 86 files changed, 4836 insertions(+), 774 deletions(-) rename lib/{json-schema-validator-i18n-support-1.0.39_3.jar => json-schema-validator-i18n-support-1.0.39_4.jar} (76%) create mode 100644 src/main/java/we/fizz/exception/FizzException.java create mode 100644 src/main/java/we/fizz/exception/FizzRuntimeException.java create mode 100644 src/main/java/we/fizz/input/IInput.java create mode 100644 src/main/java/we/fizz/input/RPCInput.java create mode 100644 src/main/java/we/fizz/input/RPCResponse.java delete mode 100644 src/main/java/we/fizz/input/RequestInput.java create mode 100644 src/main/java/we/fizz/input/extension/dubbo/DubboInput.java create mode 100644 src/main/java/we/fizz/input/extension/dubbo/DubboInputConfig.java rename src/main/java/we/fizz/input/{MySQLInput.java => extension/dubbo/DubboRPCResponse.java} (76%) create mode 100644 src/main/java/we/fizz/input/extension/grpc/GRPCResponse.java create mode 100644 src/main/java/we/fizz/input/extension/grpc/GrpcInput.java create mode 100644 src/main/java/we/fizz/input/extension/grpc/GrpcInputConfig.java create mode 100644 src/main/java/we/fizz/input/extension/mysql/MySQLInput.java rename src/main/java/we/fizz/input/{ => extension/mysql}/MySQLInputConfig.java (90%) create mode 100644 src/main/java/we/fizz/input/extension/request/RequestInput.java rename src/main/java/we/fizz/input/{ => extension/request}/RequestInputConfig.java (80%) create mode 100644 src/main/java/we/fizz/input/extension/request/RequestRPCResponse.java create mode 100644 src/main/java/we/proxy/dubbo/ApacheDubboGenericService.java create mode 100644 src/main/java/we/proxy/dubbo/DubboInterfaceDeclaration.java create mode 100644 src/main/java/we/proxy/dubbo/DubboUtils.java create mode 100644 src/main/java/we/proxy/grpc/GrpcGenericService.java create mode 100644 src/main/java/we/proxy/grpc/GrpcInstanceService.java create mode 100644 src/main/java/we/proxy/grpc/GrpcInstanceServiceImpl.java create mode 100644 src/main/java/we/proxy/grpc/GrpcInterfaceDeclaration.java create mode 100644 src/main/java/we/proxy/grpc/ListenableFutureAdapter.java create mode 100644 src/main/java/we/proxy/grpc/client/CallParams.java create mode 100644 src/main/java/we/proxy/grpc/client/CallResults.java create mode 100644 src/main/java/we/proxy/grpc/client/GrpcClient.java create mode 100644 src/main/java/we/proxy/grpc/client/GrpcProxyClient.java create mode 100644 src/main/java/we/proxy/grpc/client/core/CompositeStreamObserver.java create mode 100644 src/main/java/we/proxy/grpc/client/core/DoneObserver.java create mode 100644 src/main/java/we/proxy/grpc/client/core/DynamicMessageMarshaller.java create mode 100644 src/main/java/we/proxy/grpc/client/core/GrpcMethodDefinition.java create mode 100644 src/main/java/we/proxy/grpc/client/core/ServerReflectionClient.java create mode 100644 src/main/java/we/proxy/grpc/client/core/ServiceResolver.java create mode 100644 src/main/java/we/proxy/grpc/client/utils/ChannelFactory.java create mode 100644 src/main/java/we/proxy/grpc/client/utils/GrpcReflectionUtils.java create mode 100644 src/main/java/we/proxy/grpc/client/utils/MessageWriter.java create mode 100644 src/test/java/we/fizz/group/DevTestGroup.java create mode 100644 src/test/java/we/fizz/group/FastTestGroup.java create mode 100644 src/test/java/we/fizz/group/SlowTestGroup.java create mode 100644 src/test/java/we/fizz/input/DubboInputMockTests.java create mode 100644 src/test/java/we/fizz/input/DubboInputTests.java create mode 100644 src/test/java/we/fizz/input/GrpcInputMockTests.java create mode 100644 src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceMockTests.java create mode 100644 src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceTests.java diff --git a/README.en-us.md b/README.en-us.md index 23395fc..ddb3407 100644 --- a/README.en-us.md +++ b/README.en-us.md @@ -2,7 +2,7 @@ English | [简体中文](./README.md)

Welcome to Fizz Gateway

- Version + Version Documentation @@ -38,7 +38,7 @@ API access:http://demo.fizzgate.com/proxy/[Service Name]/[API Path] ## Product Features - Cluster management: Fizz gateway nodes are stateless with configuration information that is automatically synchronized, and horizontal expansion of nodes and multi-cluster deployment are supported. -- Service aggregation: support hot service aggregation capabilities, support front-end and back-end coding, and update API anytime and anywhere. +- Service aggregation: support hot http/dubbo/grpc service aggregation capabilities, support front-end and back-end coding, and update API anytime and anywhere. - Load balancing: support round-robin load balancing. - Service discovery: supports discovery of back-end servers from the Eureka registry. - Configuration center: support access to apollo configuration center. @@ -92,6 +92,7 @@ Starting from v1.3.0, the frontend and backend of the management backend are mer | v1.3.0 | v1.3.0 | | v1.4.0 | v1.4.0 | | v1.4.1 | v1.4.1 | +| v1.5.0 | v1.5.0 | Please download the corresponding management backend version according to the version of the community version @@ -106,7 +107,7 @@ Install the following dependent software: -Redis 2.8 or above -MySQL 5.7 or above -Apollo Configuration Center (optional) --Eureka Service Registry +-Nacos or Eureka Service Registry (optional) Dependent installation can refer to detailed deployment tutorial diff --git a/README.md b/README.md index aac1005..2b59875 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

Welcome to Fizz Gateway

- Version + Version Documentation @@ -37,7 +37,7 @@ API地址:http://demo.fizzgate.com/proxy/[服务名]/[API_Path] ## 产品特性 - 集群管理:Fizz网关节点是无状态的,配置信息自动同步,支持节点水平拓展和多集群部署。 -- 服务编排:支持热服务编排能力,支持前后端编码,随时随地更新API。 +- 服务编排:支持HTTP、Dubbo、gRPC协议热服务编排能力,支持前后端编码,随时随地更新API。 - 负载均衡:支持round-robin负载均衡。 - 服务发现:支持从Eureka或Nacos注册中心发现后端服务器。 - 配置中心:支持接入apollo配置中心。 @@ -92,6 +92,7 @@ API地址:http://demo.fizzgate.com/proxy/[服务名]/[API_Path] | v1.3.0 | v1.3.0 | | v1.4.0 | v1.4.0 | | v1.4.1 | v1.4.1 | +| v1.5.0 | v1.5.0 | 请根据社区版的版本下载对应的管理后台版本 diff --git a/js/common.js b/js/common.js index 689ff00..9e52cb2 100644 --- a/js/common.js +++ b/js/common.js @@ -5,7 +5,10 @@ var common = { /* *********** private function begin *********** */ - // 获取上下文中客户端请求对象 + /** + * 获取上下文中客户端请求对象 + * @param {*} ctx 上下文 【必填】 + */ getInputReq: function (ctx){ if(!ctx || !ctx['input'] || !ctx['input']['request']){ return {}; @@ -13,7 +16,12 @@ var common = { return ctx['input']['request'] }, - // 获取上下文步骤中请求接口的请求对象 + /** + * 获取上下文步骤中请求接口的请求对象 + * @param {*} ctx 上下文 【必填】 + * @param {*} stepName 步骤名称 【必填】 + * @param {*} requestName 请求名称 【必填】 + */ getStepReq: function (ctx, stepName, requestName){ if(!ctx || !stepName || !requestName){ return {}; @@ -25,7 +33,12 @@ var common = { return ctx[stepName]['requests'][requestName]['request']; }, - // 获取上下文步骤中请求接口的响应对象 + /** + * 获取上下文步骤中请求接口的响应对象 + * @param {*} ctx 上下文 【必填】 + * @param {*} stepName 步骤名称 【必填】 + * @param {*} requestName 请求名称 【必填】 + */ getStepResp: function (ctx, stepName, requestName){ if(!ctx || !stepName || !requestName){ return {}; @@ -49,7 +62,7 @@ var common = { getInputReqHeader: function (ctx, headerName){ var req = this.getInputReq(ctx); var headers = req['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** @@ -82,7 +95,7 @@ var common = { getInputRespHeader: function (ctx, headerName){ var req = this.getInputReq(ctx); var headers = req['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** @@ -110,7 +123,7 @@ var common = { getStepReqHeader: function (ctx, stepName, requestName, headerName){ var req = this.getStepReq(ctx, stepName, requestName); var headers = req['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** @@ -149,7 +162,7 @@ var common = { getStepRespHeader: function (ctx, stepName, requestName, headerName){ var resp = this.getStepResp(ctx, stepName, requestName); var headers = resp['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** diff --git a/lib/json-schema-validator-i18n-support-1.0.39_3.jar b/lib/json-schema-validator-i18n-support-1.0.39_4.jar similarity index 76% rename from lib/json-schema-validator-i18n-support-1.0.39_3.jar rename to lib/json-schema-validator-i18n-support-1.0.39_4.jar index 58aca0f99201c1170e67b4ce1c8bacd636157b8c..56a3728576b9922789418bc1d69b3d09ac654d85 100644 GIT binary patch delta 30023 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 delta 28494 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 diff --git a/pom.xml b/pom.xml index 4a9f449..acc706d 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ we fizz-gateway-community - 1.4.1 + 1.5.0 fizz-gateway-community @@ -38,9 +38,14 @@ Dysprosium-SR17 5.3.6.RELEASE 0.2.7 - 4.1.59.Final + 4.1.60.Final 4.4.14 2.13.3 + 2.7.5 + 1.16.1 + 3.4.0 + 4.0.1 + 3.5.6 @@ -59,9 +64,9 @@ com.networknt json-schema-validator-i18n-support - 1.0.39_3 + 1.0.39_4 system - ${project.basedir}/lib/json-schema-validator-i18n-support-1.0.39_3.jar + ${project.basedir}/lib/json-schema-validator-i18n-support-1.0.39_4.jar org.springframework.boot @@ -218,12 +223,87 @@ java-jwt 3.12.1 + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-inline + ${mockito.version} + test + - + + org.apache.dubbo + dubbo + ${apache.dubbo.version} + + + + + io.grpc + grpc-all + ${grpc.version} + + + + io.grpc + grpc-services + ${grpc.version} + + + org.projectlombok + lombok + 1.18.16 + + + + + org.apache.curator + curator-client + ${curator.version} + + + org.apache.curator + curator-framework + ${curator.version} + + + org.apache.curator + curator-recipes + ${curator.version} + + + org.apache.zookeeper + zookeeper + ${zookeeper.version} + + + org.slf4j + slf4j-log4j12 + + + log4j + log4j + + + + + + junit + junit + + test + + + diff --git a/src/main/java/we/FizzAppContext.java b/src/main/java/we/FizzAppContext.java index e8f48a1..7e3705c 100644 --- a/src/main/java/we/FizzAppContext.java +++ b/src/main/java/we/FizzAppContext.java @@ -19,7 +19,6 @@ package we; import org.springframework.context.ConfigurableApplicationContext; public class FizzAppContext { - - public static ConfigurableApplicationContext appContext; + public static ConfigurableApplicationContext appContext; } diff --git a/src/main/java/we/FizzGatewayApplication.java b/src/main/java/we/FizzGatewayApplication.java index 2986fc8..1361c40 100644 --- a/src/main/java/we/FizzGatewayApplication.java +++ b/src/main/java/we/FizzGatewayApplication.java @@ -33,7 +33,7 @@ import we.log.LogSendAppender; * fizz gateway application boot entrance * * @author linwaiwai - * @author francis + * @author Francis Dong * @author hongqiaowei * @author zhongjie */ diff --git a/src/main/java/we/config/SystemConfig.java b/src/main/java/we/config/SystemConfig.java index fab4836..ec0d3ea 100644 --- a/src/main/java/we/config/SystemConfig.java +++ b/src/main/java/we/config/SystemConfig.java @@ -31,9 +31,9 @@ import we.util.Constants; import we.util.WebUtils; import javax.annotation.PostConstruct; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author hongqiaowei @@ -44,6 +44,76 @@ public class SystemConfig { private static final Logger log = LoggerFactory.getLogger(SystemConfig.class); + public static final String DEFAULT_GATEWAY_PREFIX = "/proxy"; + + public static final String DEFAULT_GATEWAY_TEST_PREFIX = "/_proxytest"; + + public String gatewayPrefix = DEFAULT_GATEWAY_PREFIX; + + public List appHeaders = Stream.of("fizz-appid").collect(Collectors.toList()); + + public List signHeaders = Stream.of("fizz-sign") .collect(Collectors.toList()); + + public List timestampHeaders = Stream.of("fizz-ts") .collect(Collectors.toList()); + + public List proxySetHeaders = new ArrayList<>(); + + + @NacosValue(value = "${gateway.aggr.proxy_set_headers:}", autoRefreshed = true) + @Value("${gateway.aggr.proxy_set_headers:}") + public void setProxySetHeaders(String hdrs) { + if (StringUtils.isNotBlank(hdrs)) { + for (String h : StringUtils.split(hdrs, ',')) { + proxySetHeaders.add(h.trim()); + } + } + log.info("proxy set headers: " + hdrs); + } + + @NacosValue(value = "${gateway.prefix:/proxy}", autoRefreshed = true) + @Value( "${gateway.prefix:/proxy}" ) + public void setGatewayPrefix(String gp) { + gatewayPrefix = gp; + WebUtils.setGatewayPrefix(gatewayPrefix); + log.info("gateway prefix: " + gatewayPrefix); + } + + @NacosValue(value = "${custom.header.appid:}", autoRefreshed = true) + @Value( "${custom.header.appid:}" ) + public void setCustomAppHeaders(String hdrs) { + if (StringUtils.isNotBlank(hdrs)) { + for (String h : StringUtils.split(hdrs, ',')) { + appHeaders.add(h.trim()); + } + } + WebUtils.setAppHeaders(appHeaders); + log.info("app headers: " + appHeaders); + } + + @NacosValue(value = "${custom.header.sign:}", autoRefreshed = true) + @Value( "${custom.header.sign:}" ) + public void setCustomSignHeaders(String hdrs) { + if (StringUtils.isNotBlank(hdrs)) { + for (String h : StringUtils.split(hdrs, ',')) { + signHeaders.add(h.trim()); + } + } + log.info("sign headers: " + signHeaders); + } + + @NacosValue(value = "${custom.header.ts:}", autoRefreshed = true) + @Value( "${custom.header.ts:}" ) + public void setCustomTimestampHeaders(String hdrs) { + if (StringUtils.isNotBlank(hdrs)) { + for (String h : StringUtils.split(hdrs, ',')) { + timestampHeaders.add(h.trim()); + } + } + log.info("timestamp headers: " + timestampHeaders); + } + + // TODO: below to X + @Value("${log.response-body:false}") private boolean logResponseBody; @@ -67,7 +137,7 @@ public class SystemConfig { } private void afterLogResponseBodySet() { - WebUtils.logResponseBody = logResponseBody; + WebUtils.LOG_RESPONSE_BODY = logResponseBody; log.info("log response body: " + logResponseBody); } @@ -76,7 +146,7 @@ public class SystemConfig { Arrays.stream(StringUtils.split(logHeaders, Constants.Symbol.COMMA)).forEach(h -> { logHeaderSet.add(h); }); - WebUtils.logHeaderSet = logHeaderSet; + WebUtils.LOG_HEADER_SET = logHeaderSet; log.info("log header list: " + logHeaderSet.toString()); } diff --git a/src/main/java/we/constants/CommonConstants.java b/src/main/java/we/constants/CommonConstants.java index 47ba0d2..6c5f127 100644 --- a/src/main/java/we/constants/CommonConstants.java +++ b/src/main/java/we/constants/CommonConstants.java @@ -40,11 +40,17 @@ public class CommonConstants { /** - * WildCard for PathMapping + * Star WildCard for PathMapping */ public static final String WILDCARD_STAR = "*"; + /** + * Tilde WildCard for PathMapping + */ + public static final String WILDCARD_TILDE = "~"; + + /** * Stop the underlying processes and response immediately, using in scripts */ @@ -55,5 +61,21 @@ public class CommonConstants { */ public static final String REDIRECT_URL_KEY = "_redirectUrl"; + + /** + * Content-Length Header + */ + public static final String HEADER_CONTENT_LENGTH = "Content-Length"; + + /** + * Content-Type Header + */ + public static final String HEADER_CONTENT_TYPE = "Content-Type"; + + /** + * JSON Content-Type + */ + public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8"; + } diff --git a/src/main/java/we/filter/AggregateFilter.java b/src/main/java/we/filter/AggregateFilter.java index 3a31cb7..116b082 100644 --- a/src/main/java/we/filter/AggregateFilter.java +++ b/src/main/java/we/filter/AggregateFilter.java @@ -19,8 +19,11 @@ package we.filter; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import javax.annotation.Resource; @@ -61,7 +64,7 @@ import we.util.MapUtil; import we.util.WebUtils; /** - * @author francis + * @author Francis Dong */ @Component @Order(30) @@ -107,10 +110,13 @@ public class AggregateFilter implements WebFilter { Pipeline pipeline = aggregateResource.getPipeline(); Input input = aggregateResource.getInput(); - Map headers = MapUtil.toHashMap(request.getHeaders()); + Map headers = MapUtil.headerToHashMap(request.getHeaders()); Map fizzHeaders = (Map) exchange.getAttributes().get(WebUtils.APPEND_HEADERS); - if(fizzHeaders != null && !fizzHeaders.isEmpty()) { - headers.putAll(fizzHeaders); + if (fizzHeaders != null && !fizzHeaders.isEmpty()) { + Set> entrys = fizzHeaders.entrySet(); + for (Entry entry : entrys) { + headers.put(entry.getKey().toUpperCase(), entry.getValue()); + } } // traceId @@ -148,15 +154,20 @@ public class AggregateFilter implements WebFilter { } return result.subscribeOn(Schedulers.elastic()).flatMap(aggResult -> { LogService.setBizId(traceId); - String jsonString = JSON.toJSONString(aggResult.getBody()); + String jsonString = null; + if(aggResult.getBody() instanceof String) { + jsonString = (String) aggResult.getBody(); + }else { + jsonString = JSON.toJSONString(aggResult.getBody()); + } LOGGER.debug("response body: {}", jsonString); if (aggResult.getHeaders() != null && !aggResult.getHeaders().isEmpty()) { - aggResult.getHeaders().remove("Content-Length"); serverHttpResponse.getHeaders().addAll(aggResult.getHeaders()); + serverHttpResponse.getHeaders().remove(CommonConstants.HEADER_CONTENT_LENGTH); } - if (!serverHttpResponse.getHeaders().containsKey("Content-Type")) { - // defalut content-type - serverHttpResponse.getHeaders().add("Content-Type", "application/json; charset=UTF-8"); + if (!serverHttpResponse.getHeaders().containsKey(CommonConstants.HEADER_CONTENT_TYPE)) { + // default content-type + serverHttpResponse.getHeaders().add(CommonConstants.HEADER_CONTENT_TYPE, CommonConstants.CONTENT_TYPE_JSON); } List headerTraceIds = serverHttpResponse.getHeaders().get(CommonConstants.HEADER_TRACE_ID); if (headerTraceIds == null || !headerTraceIds.contains(traceId)) { diff --git a/src/main/java/we/filter/FilterExceptionHandlerConfig.java b/src/main/java/we/filter/FilterExceptionHandlerConfig.java index 6342a15..aa48502 100644 --- a/src/main/java/we/filter/FilterExceptionHandlerConfig.java +++ b/src/main/java/we/filter/FilterExceptionHandlerConfig.java @@ -88,6 +88,7 @@ public class FilterExceptionHandlerConfig { WebUtils.request2stringBuilder(exchange, b); log.error(b.toString(), LogService.BIZ_ID, exchange.getRequest().getId(), t); String s = RespEntity.toJson(HttpStatus.INTERNAL_SERVER_ERROR.value(), t.getMessage(), exchange.getRequest().getId()); + resp.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); vm = resp.writeWith(Mono.just(resp.bufferFactory().wrap(s.getBytes()))); } else { vm = WebUtils.responseError(exchange, filterExceptionHandler, HttpStatus.INTERNAL_SERVER_ERROR.value(), t.getMessage(), t); diff --git a/src/main/java/we/filter/FizzWebFilter.java b/src/main/java/we/filter/FizzWebFilter.java index ff75451..c870b25 100644 --- a/src/main/java/we/filter/FizzWebFilter.java +++ b/src/main/java/we/filter/FizzWebFilter.java @@ -17,10 +17,13 @@ package we.filter; +import org.springframework.http.HttpStatus; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; +import we.util.Constants; +import we.util.Utils; import we.util.WebUtils; /** @@ -29,10 +32,19 @@ import we.util.WebUtils; public abstract class FizzWebFilter implements WebFilter { + private static final String admin = "admin"; + private static final String actuator = "actuator"; + @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - String serviceId = WebUtils.getClientService(exchange); - if (serviceId == null) { + + String path = exchange.getRequest().getPath().value(); + int secFS = path.indexOf(Constants.Symbol.FORWARD_SLASH, 1); + if (secFS == -1) { + return WebUtils.responseError(exchange, HttpStatus.INTERNAL_SERVER_ERROR.value(), "request path should like /optional-prefix/service-name/real-biz-path"); + } + String s = path.substring(1, secFS); + if (s.equals(admin) || s.equals(actuator)) { return chain.filter(exchange); } else { return doFilter(exchange, chain); diff --git a/src/main/java/we/filter/PreprocessFilter.java b/src/main/java/we/filter/PreprocessFilter.java index aeb26fa..c7c096c 100644 --- a/src/main/java/we/filter/PreprocessFilter.java +++ b/src/main/java/we/filter/PreprocessFilter.java @@ -18,7 +18,6 @@ package we.filter; import com.alibaba.nacos.api.config.annotation.NacosValue; -import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -40,7 +39,9 @@ import we.util.ReactorUtils; import we.util.WebUtils; import javax.annotation.Resource; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.Function; /** @@ -75,11 +76,6 @@ public class PreprocessFilter extends FizzWebFilter { Map eas = exchange.getAttributes(); eas.put(WebUtils.FILTER_CONTEXT, fc); eas.put(WebUtils.APPEND_HEADERS, appendHdrs); - String app = WebUtils.getHeaderValue(exchange, WebUtils.APP_HEADER); - if (StringUtils.isNotBlank(app)) { - eas.put(WebUtils.APP_HEADER, app); - } - Mono vm = statPluginFilter.filter(exchange, null, null); return chain(exchange, vm, authPluginFilter).defaultIfEmpty(ReactorUtils.NULL) .flatMap( @@ -110,11 +106,11 @@ public class PreprocessFilter extends FizzWebFilter { } private void afterAuth(ServerWebExchange exchange, ApiConfig ac) { - String bs = null, bp; + String bs = null, bp = null; if (ac == null) { bs = WebUtils.getClientService(exchange); bp = WebUtils.getClientReqPath(exchange); - } else { + } else if (ac.type != ApiConfig.Type.CALLBACK) { if (ac.type != ApiConfig.Type.REVERSE_PROXY) { bs = ac.backendService; } @@ -123,7 +119,9 @@ public class PreprocessFilter extends FizzWebFilter { if (bs != null) { WebUtils.setBackendService(exchange, bs); } - WebUtils.setBackendPath(exchange, bp); + if (bp != null) { + WebUtils.setBackendPath(exchange, bp); + } } private Mono chain(ServerWebExchange exchange, Mono m, PluginFilter pf) { diff --git a/src/main/java/we/fizz/AggregateResource.java b/src/main/java/we/fizz/AggregateResource.java index b3603f8..2d0dccb 100644 --- a/src/main/java/we/fizz/AggregateResource.java +++ b/src/main/java/we/fizz/AggregateResource.java @@ -20,7 +20,7 @@ import we.fizz.input.Input; /** * - * @author francis + * @author Francis Dong * */ public class AggregateResource { diff --git a/src/main/java/we/fizz/AggregateResult.java b/src/main/java/we/fizz/AggregateResult.java index 72e4f50..c41c9b7 100644 --- a/src/main/java/we/fizz/AggregateResult.java +++ b/src/main/java/we/fizz/AggregateResult.java @@ -17,20 +17,18 @@ package we.fizz; -import java.util.Map; - import org.springframework.util.MultiValueMap; /** * - * @author francis + * @author Francis Dong * */ public class AggregateResult { private MultiValueMap headers; - private Map body; + private Object body; private StepContext stepContext; @@ -42,11 +40,11 @@ public class AggregateResult { this.headers = headers; } - public Map getBody() { + public Object getBody() { return body; } - public void setBody(Map body) { + public void setBody(Object body) { this.body = body; } diff --git a/src/main/java/we/fizz/AggregateService.java b/src/main/java/we/fizz/AggregateService.java index 13ed0b3..c664c14 100644 --- a/src/main/java/we/fizz/AggregateService.java +++ b/src/main/java/we/fizz/AggregateService.java @@ -99,7 +99,12 @@ public class AggregateService { ServerHttpResponse clientResp = exchange.getResponse(); String traceId = WebUtils.getTraceId(exchange); LogService.setBizId(traceId); - String js = JSON.toJSONString(ar.getBody()); + String js = null; + if(ar.getBody() instanceof String) { + js = (String) ar.getBody(); + }else { + js = JSON.toJSONString(ar.getBody()); + } log.debug("aggregate response body: {}", js); if (ar.getHeaders() != null && !ar.getHeaders().isEmpty()) { ar.getHeaders().remove("Content-Length"); diff --git a/src/main/java/we/fizz/ConfigLoader.java b/src/main/java/we/fizz/ConfigLoader.java index db67f7c..99c8625 100644 --- a/src/main/java/we/fizz/ConfigLoader.java +++ b/src/main/java/we/fizz/ConfigLoader.java @@ -22,10 +22,10 @@ import com.alibaba.fastjson.JSONArray; import com.alibaba.nacos.api.config.annotation.NacosValue; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; import we.config.AppConfigProperties; -import we.fizz.input.ClientInputConfig; -import we.fizz.input.Input; -import we.fizz.input.InputType; +import we.fizz.input.*; import org.apache.commons.io.FileUtils; import org.noear.snack.ONode; @@ -36,6 +36,10 @@ import org.springframework.data.redis.core.ReactiveStringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import we.fizz.input.extension.grpc.GrpcInput; +import we.fizz.input.extension.dubbo.DubboInput; +import we.fizz.input.extension.mysql.MySQLInput; +import we.fizz.input.extension.request.RequestInput; import javax.annotation.PostConstruct; import javax.annotation.Resource; @@ -46,7 +50,8 @@ import static we.util.Constants.Symbol.FORWARD_SLASH; import java.io.File; import java.io.IOException; import java.io.Serializable; -import java.nio.charset.Charset; +import java.lang.ref.SoftReference; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -55,13 +60,31 @@ import java.util.concurrent.ConcurrentHashMap; /** * - * @author francis + * @author Francis Dong * @author zhongjie * */ @Component public class ConfigLoader { + /** + * legacy aggregate formal path prefix + */ + private static final String LEGACY_FORMAL_PATH_PREFIX = "/proxy"; + /** + * legacy aggregate test path prefix + */ + private static final String LEGACY_TEST_PATH_PREFIX = "/proxytest"; + /** + * aggregate test path prefix + */ + private static final String TEST_PATH_PREFIX = "/_proxytest"; + /** + * aggregate test path service name start index + */ + private static final int TEST_PATH_SERVICE_NAME_START_INDEX = TEST_PATH_PREFIX.length() + 1; + @Autowired + public ConfigurableApplicationContext appContext; private static final Logger LOGGER = LoggerFactory.getLogger(ConfigLoader.class); /** @@ -83,6 +106,9 @@ public class ConfigLoader { @Value("${fizz.aggregate.read-local-config-flag:false}") private Boolean readLocalConfigFlag; + private String formalPathPrefix; + private int formalPathServiceNameStartIndex; + public Input createInput(String configStr) throws IOException { ONode cfgNode = ONode.loadStr(configStr); @@ -115,14 +141,20 @@ public class ConfigLoader { public Pipeline createPipeline(String configStr) throws IOException { ONode cfgNode = ONode.loadStr(configStr); + + InputFactory.registerInput(RequestInput.TYPE, RequestInput.class); + InputFactory.registerInput(MySQLInput.TYPE, MySQLInput.class); + InputFactory.registerInput(GrpcInput.TYPE, GrpcInput.class); + InputFactory.registerInput(DubboInput.TYPE, DubboInput.class); Pipeline pipeline = new Pipeline(); + pipeline.setApplicationContext(appContext); List> stepConfigs = cfgNode.select("$.stepConfigs").toObject(List.class); for (Map stepConfig : stepConfigs) { // set the specified env URL this.handleRequestURL(stepConfig); - - Step step = new Step.Builder().read(stepConfig); + SoftReference weakPipeline = new SoftReference(pipeline); + Step step = new Step.Builder().read(stepConfig, weakPipeline); step.setName((String) stepConfig.get("name")); if (stepConfig.get("stop") != null) { step.setStop((Boolean) stepConfig.get("stop")); @@ -175,6 +207,11 @@ public class ConfigLoader { @PostConstruct public synchronized void init() throws Exception { + if (formalPathPrefix == null) { + formalPathPrefix = appContext.getEnvironment().getProperty("gateway.prefix", "/proxy"); + formalPathServiceNameStartIndex = formalPathPrefix.length() + 1; + } + if (aggregateResources == null) { aggregateResources = new ConcurrentHashMap<>(1024); resourceKey2ConfigInfoMap = new ConcurrentHashMap<>(1024); @@ -190,7 +227,7 @@ public class ConfigLoader { if (!file.exists()) { throw new IOException("File not found"); } - String configStr = FileUtils.readFileToString(file, Charset.forName("UTF-8")); + String configStr = FileUtils.readFileToString(file, StandardCharsets.UTF_8); this.addConfig(configStr); } } @@ -214,13 +251,42 @@ public class ConfigLoader { } ONode cfgNode = ONode.loadStr(configStr); + + boolean needReGenConfigStr = false; + // in the future aggregate config will add this field and remove the prefix '/proxy'|'/proxytest' of path + boolean existAggrVersion = cfgNode.contains("aggrVersion"); + String method = cfgNode.select("$.method").getString(); String path = cfgNode.select("$.path").getString(); + + if (!existAggrVersion) { + if (path.startsWith(LEGACY_TEST_PATH_PREFIX)) { + // legacy test path, remove prefix '/proxytest' + path = path.replaceFirst(LEGACY_TEST_PATH_PREFIX, TEST_PATH_PREFIX); + needReGenConfigStr = true; + } else if (path.startsWith(LEGACY_FORMAL_PATH_PREFIX)) { + // legacy formal path, remove prefix '/proxy' + path = path.replace(LEGACY_FORMAL_PATH_PREFIX, ""); + needReGenConfigStr = true; + } + } + + if (!path.startsWith(TEST_PATH_PREFIX)) { + // formal path add the custom gateway prefix + path = String.format("%s%s", formalPathPrefix, path); + needReGenConfigStr = true; + } + String resourceKey = method.toUpperCase() + ":" + path; String configId = cfgNode.select("$.id").getString(); String configName = cfgNode.select("$.name").getString(); long version = cfgNode.select("$.version").getLong(); + if (needReGenConfigStr) { + cfgNode.set("path", path); + configStr = cfgNode.toJson(); + } + LOGGER.debug("add aggregation config, key={} config={}", resourceKey, configStr); if (StringUtils.hasText(configId)) { String existResourceKey = aggregateId2ResourceKeyMap.get(configId); @@ -294,17 +360,12 @@ public class ConfigLoader { return configInfo; } - private static final String FORMAL_PATH_PREFIX = "/proxy/"; - private static final int FORMAL_PATH_SERVICE_NAME_START_INDEX = 7; - private static final String TEST_PATH_PREFIX = "/proxytest/"; - private static final int TEST_PATH_SERVICE_NAME_START_INDEX = 11; - private String extractServiceName(String path) { if (path != null) { - if (path.startsWith(FORMAL_PATH_PREFIX)) { - int endIndex = path.indexOf(FORWARD_SLASH, FORMAL_PATH_SERVICE_NAME_START_INDEX); - if (endIndex > FORMAL_PATH_SERVICE_NAME_START_INDEX) { - return path.substring(FORMAL_PATH_SERVICE_NAME_START_INDEX, endIndex); + if (path.startsWith(formalPathPrefix)) { + int endIndex = path.indexOf(FORWARD_SLASH, formalPathServiceNameStartIndex); + if (endIndex > formalPathServiceNameStartIndex) { + return path.substring(formalPathServiceNameStartIndex, endIndex); } } else if (path.startsWith(TEST_PATH_PREFIX)) { int endIndex = path.indexOf(FORWARD_SLASH, TEST_PATH_SERVICE_NAME_START_INDEX); diff --git a/src/main/java/we/fizz/Pipeline.java b/src/main/java/we/fizz/Pipeline.java index 6f4bdbd..e4b7245 100644 --- a/src/main/java/we/fizz/Pipeline.java +++ b/src/main/java/we/fizz/Pipeline.java @@ -25,6 +25,7 @@ import java.util.Map; import javax.script.ScriptException; +import org.springframework.context.ConfigurableApplicationContext; import we.schema.util.I18nUtils; import org.noear.snack.ONode; import org.slf4j.Logger; @@ -36,6 +37,7 @@ import com.alibaba.fastjson.JSON; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import we.constants.CommonConstants; import we.exception.ExecuteScriptException; import we.fizz.input.ClientInputConfig; import we.fizz.input.Input; @@ -43,6 +45,7 @@ import we.fizz.input.InputConfig; import we.fizz.input.PathMapping; import we.fizz.input.ScriptHelper; import we.flume.clients.log4j2appender.LogService; +import we.schema.util.PropertiesSupportUtils; import we.util.JacksonUtils; import we.util.JsonSchemaUtils; import we.util.MapUtil; @@ -50,11 +53,12 @@ import we.util.MapUtil; /** * * @author linwaiwai - * @author francis + * @author Francis Dong * @author zhongjie * */ public class Pipeline { + private ConfigurableApplicationContext applicationContext; private static final Logger LOGGER = LoggerFactory.getLogger(Pipeline.class); private LinkedList steps = new LinkedList(); private StepContext stepContext = new StepContext<>(); @@ -74,6 +78,7 @@ public class Pipeline { ClientInputConfig config = (ClientInputConfig)input.getConfig(); this.initialStepContext(clientInput); this.stepContext.setDebug(config.isDebug()); + this.stepContext.setApplicationContext(applicationContext); if(traceId != null) { this.stepContext.setTraceId(traceId); @@ -211,6 +216,7 @@ public class Pipeline { /** * 当validateResponse不为空表示验参失败,使用该配置响应数据 */ + @SuppressWarnings("unchecked") private AggregateResult doInputDataMapping(Input input, Map validateResponse) { AggregateResult aggResult = new AggregateResult(); Map> group = (Map>) stepContext.get("input"); @@ -234,40 +240,49 @@ public class Pipeline { ONode ctxNode = PathMapping.toONode(stepContext); // headers - response.put("headers", - PathMapping.transform(ctxNode, stepContext, - (Map) responseMapping.get("fixedHeaders"), - (Map) responseMapping.get("headers"))); + Map headers = PathMapping.transform(ctxNode, stepContext, + MapUtil.upperCaseKey((Map) responseMapping.get("fixedHeaders")), + MapUtil.upperCaseKey((Map) responseMapping.get("headers")), false); + if (headers.containsKey(CommonConstants.WILDCARD_TILDE) + && headers.get(CommonConstants.WILDCARD_TILDE) instanceof Map) { + response.put("headers", headers.get(CommonConstants.WILDCARD_TILDE)); + } else { + response.put("headers", headers); + } // body Map body = PathMapping.transform(ctxNode, stepContext, (Map) responseMapping.get("fixedBody"), (Map) responseMapping.get("body")); - - // script - if (responseMapping.get("script") != null) { - Map scriptCfg = (Map) responseMapping.get("script"); - try { - Object respBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); - if(respBody != null) { - body.putAll((Map) respBody); + if (body.containsKey(CommonConstants.WILDCARD_TILDE)) { + response.put("body", body.get(CommonConstants.WILDCARD_TILDE)); + } else { + // script + if (responseMapping.get("script") != null) { + Map scriptCfg = (Map) responseMapping.get("script"); + try { + Object respBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); + if(respBody != null) { + body.putAll((Map) respBody); + } + } catch (ScriptException e) { + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); + throw new ExecuteScriptException(e, stepContext, scriptCfg); } - } catch (ScriptException e) { - LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); - throw new ExecuteScriptException(e, stepContext, scriptCfg); } + response.put("body", body); } - response.put("body", body); } } - Map respBody = (Map) response.get("body"); + Object respBody = response.get("body"); // 测试模式返回StepContext - if(stepContext.returnContext()) { - respBody.put(stepContext.CONTEXT_FIELD, stepContext); + if (stepContext.returnContext() && respBody instanceof Map) { + Map t = (Map) respBody; + t.put(stepContext.CONTEXT_FIELD, stepContext); } - aggResult.setBody((Map) response.get("body")); + aggResult.setBody(response.get("body")); aggResult.setHeaders(MapUtil.toMultiValueMap((Map) response.get("headers"))); return aggResult; } @@ -283,7 +298,14 @@ public class Pipeline { Map headersDef = ((ClientInputConfig) config).getHeadersDef(); if (!CollectionUtils.isEmpty(headersDef)) { // 验证headers入参是否符合要求 - List errorList = JsonSchemaUtils.validateAllowValueStr(JSON.toJSONString(headersDef), JSON.toJSONString(clientInput.get("headers"))); + List errorList; + PropertiesSupportUtils.setContextSupportPropertyUpperCase(); + try { + errorList = JsonSchemaUtils.validateAllowValueStr(JSON.toJSONString(headersDef), JSON.toJSONString(clientInput.get("headers"))); + } finally { + PropertiesSupportUtils.removeContextSupportPropertyUpperCase(); + } + if (!CollectionUtils.isEmpty(errorList)) { return errorList; } @@ -362,4 +384,12 @@ public class Pipeline { } } } + + public void setApplicationContext(ConfigurableApplicationContext appContext) { + this.applicationContext = appContext; + } + + public ConfigurableApplicationContext getApplicationContext() { + return this.applicationContext; + } } diff --git a/src/main/java/we/fizz/Step.java b/src/main/java/we/fizz/Step.java index 93d0bd1..d589ba1 100644 --- a/src/main/java/we/fizz/Step.java +++ b/src/main/java/we/fizz/Step.java @@ -17,11 +17,13 @@ package we.fizz; +import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; @@ -41,12 +43,12 @@ import we.fizz.input.InputType; /** * * @author linwaiwai - * @author francis + * @author Francis Dong * */ public class Step { - - private String name; + private SoftReference weakPipeline; + private String name; // 是否在执行完当前step就返回 private boolean stop; @@ -55,9 +57,22 @@ public class Step { private Map requestConfigs = new HashMap(); + public SoftReference getWeakPipeline() { + return weakPipeline; + } + + public void setWeakPipeline(SoftReference weakPipeline) { + this.weakPipeline = weakPipeline; + } + + public ConfigurableApplicationContext getCurrentApplicationContext() { + return this.getWeakPipeline() != null ? this.getWeakPipeline().get().getApplicationContext(): null; + } + public static class Builder { - public Step read(Map config) { + public Step read(Map config, SoftReference weakPipeline) { Step step = new Step(); + step.setWeakPipeline(weakPipeline); List requests= (List) config.get("requests"); for(Map requestConfig: requests) { InputConfig inputConfig = InputFactory.createInputConfig(requestConfig); @@ -68,6 +83,11 @@ public class Step { } private StepContext stepContext; + + public StepContext getStepContext(){ + return this.stepContext; + } + private StepResponse lastStepResponse = null; private Map inputs = new HashMap(); public void beforeRun(StepContext stepContext2, StepResponse response ) { @@ -80,6 +100,7 @@ public class Step { InputConfig inputConfig = configs.get(configName); InputType type = inputConfig.getType(); Input input = InputFactory.createInput(type.toString()); + input.setWeakStep(new SoftReference(this)); input.setConfig(inputConfig); input.setName(configName); input.setStepResponse(stepResponse); diff --git a/src/main/java/we/fizz/StepContext.java b/src/main/java/we/fizz/StepContext.java index f0183eb..16f6515 100644 --- a/src/main/java/we/fizz/StepContext.java +++ b/src/main/java/we/fizz/StepContext.java @@ -23,38 +23,39 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.context.ConfigurableApplicationContext; import we.constants.CommonConstants; /** - * + * * @author linwaiwai - * @author francis + * @author Francis Dong * * @param * @param */ @SuppressWarnings("unchecked") public class StepContext extends ConcurrentHashMap { - + private ConfigurableApplicationContext applicationContext; public static final String ELAPSED_TIMES = "elapsedTimes"; public static final String DEBUG = "debug"; public static final String RETURN_CONTEXT = "returnContext"; // context field in response body public static final String CONTEXT_FIELD = "_context"; - + // exception info public static final String EXCEPTION_MESSAGE = "exceptionMessage"; public static final String EXCEPTION_STACKS = "exceptionStacks"; public static final String EXCEPTION_DATA = "exceptionData"; - + public void setDebug(Boolean debug) { this.put((K)DEBUG, (V)debug); } - + public String getTraceId() { return (String) this.get(CommonConstants.TRACE_ID); } - + public void setTraceId(String traceId) { this.put((K)CommonConstants.TRACE_ID, (V)traceId); } @@ -74,7 +75,7 @@ public class StepContext extends ConcurrentHashMap { public boolean returnContext() { return Boolean.valueOf((String)getInputReqHeader(RETURN_CONTEXT)); } - + /** * set exception information * @param cause exception @@ -97,7 +98,7 @@ public class StepContext extends ConcurrentHashMap { this.put((K) EXCEPTION_STACKS, (V) arr); } } - + public synchronized void addElapsedTime(String actionName, Long milliSeconds) { List> elapsedTimes = (List>) this.get(ELAPSED_TIMES); if (elapsedTimes == null) { @@ -129,7 +130,7 @@ public class StepContext extends ConcurrentHashMap { /** * 设置Step里调用接口的请求头 - * + * * @param stepName * @param requestName * @param headerName @@ -150,12 +151,15 @@ public class StepContext extends ConcurrentHashMap { headers = new HashMap<>(); req.put("headers", headers); } - headers.put(headerName, headerValue); + if (headerName == null || "".equals(headerName)) { + return; + } + headers.put(headerName.toUpperCase(), headerValue); } /** * 获取Step里调用接口的请求头 - * + * * @param stepName * @param requestName * @param headerName @@ -173,15 +177,18 @@ public class StepContext extends ConcurrentHashMap { if (headers == null) { return null; } - return headers.get(headerName); + if (headerName == null || "".equals(headerName)) { + return null; + } + return headers.get(headerName.toUpperCase()); } /** * 设置Step里调用接口的请求body - * + * * @param stepName * @param requestName - * @param fieldName + * @param key * @param value */ public void setStepReqBody(String stepName, String requestName, String key, Object value) { @@ -194,6 +201,9 @@ public class StepContext extends ConcurrentHashMap { req = new HashMap<>(); request.put("request", req); } + if (req.get("body") != null && !(req.get("body") instanceof Map)) { + return; + } Map body = (Map) req.get("body"); if (body == null) { body = new HashMap<>(); @@ -204,10 +214,10 @@ public class StepContext extends ConcurrentHashMap { /** * 获取Step里调用接口的请求body - * + * * @param stepName * @param requestName - * @param fieldName 字段名 + * @param fieldName */ public Object getStepReqBody(String stepName, String requestName, String fieldName) { Map request = getStepRequest(stepName, requestName); @@ -219,6 +229,9 @@ public class StepContext extends ConcurrentHashMap { req = new HashMap<>(); request.put("request", req); } + if (req.get("body") != null && !(req.get("body") instanceof Map)) { + return null; + } Map body = (Map) req.get("body"); if (body == null) { return null; @@ -228,7 +241,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取Step里调用接口的请求body - * + * * @param stepName * @param requestName */ @@ -244,7 +257,7 @@ public class StepContext extends ConcurrentHashMap { } return req.get("body"); } - + /** * 获取Step里调用的接口的URL参数 * @param stepName 步骤名【必填】 @@ -263,7 +276,7 @@ public class StepContext extends ConcurrentHashMap { } return req.get("params"); } - + /** * 获取Step里调用的接口的URL参数 * @param stepName 步骤名【必填】 @@ -274,10 +287,9 @@ public class StepContext extends ConcurrentHashMap { Map params = (Map) this.getStepReqParam(stepName, requestName); return params == null ? null : params.get(paramName); } - /** * 设置Step里调用接口响应头 - * + * * @param stepName * @param requestName * @param headerName @@ -298,12 +310,15 @@ public class StepContext extends ConcurrentHashMap { headers = new HashMap<>(); response.put("headers", headers); } - headers.put(headerName, headerValue); + if (headerName == null || "".equals(headerName)) { + return; + } + headers.put(headerName.toUpperCase(), headerValue); } /** * 获取Step里调用接口响应头 - * + * * @param stepName * @param requestName * @param headerName @@ -321,12 +336,15 @@ public class StepContext extends ConcurrentHashMap { if (headers == null) { return null; } - return headers.get(headerName); + if (headerName == null || "".equals(headerName)) { + return null; + } + return headers.get(headerName.toUpperCase()); } /** * 设置Step里调用接口的响应body - * + * * @param stepName * @param requestName * @param key @@ -342,6 +360,9 @@ public class StepContext extends ConcurrentHashMap { response = new HashMap<>(); request.put("response", response); } + if (response.get("body") != null && !(response.get("body") instanceof Map)) { + return; + } Map body = (Map) response.get("body"); if (body == null) { body = new HashMap<>(); @@ -352,7 +373,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取Step里调用接口的响应body - * + * * @param stepName * @param requestName * @param key @@ -366,6 +387,9 @@ public class StepContext extends ConcurrentHashMap { if (response == null) { return null; } + if (response.get("body") != null && !(response.get("body") instanceof Map)) { + return null; + } Map body = (Map) response.get("body"); if (body == null) { return null; @@ -375,7 +399,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取Step里调用接口的响应body - * + * * @param stepName * @param requestName */ @@ -393,7 +417,7 @@ public class StepContext extends ConcurrentHashMap { /** * 设置Step的结果 - * + * * @param stepName * @param key * @param value @@ -413,7 +437,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取Step的结果 - * + * * @param stepName * @param key */ @@ -431,7 +455,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取Step的结果 - * + * * @param stepName */ public Object getStepResult(String stepName) { @@ -444,7 +468,7 @@ public class StepContext extends ConcurrentHashMap { /** * 设置聚合接口的响应头 - * + * * @param headerName * @param headerValue */ @@ -467,7 +491,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取聚合接口的响应头 - * + * * @param headerName */ public Object getInputRespHeader(String headerName) { @@ -488,10 +512,13 @@ public class StepContext extends ConcurrentHashMap { /** * 获取聚合接口的请求头 - * + * * @param headerName */ public Object getInputReqHeader(String headerName) { + if (headerName == null || "".equals(headerName)) { + return null; + } Map input = (Map) this.get("input"); if (input == null) { return null; @@ -504,12 +531,12 @@ public class StepContext extends ConcurrentHashMap { if (headers == null) { return null; } - return headers.get(headerName); + return headers.get(headerName.toUpperCase()); } /** * 设置聚合接口的响应body - * + * * @param fieldName * @param value */ @@ -523,6 +550,9 @@ public class StepContext extends ConcurrentHashMap { response = new HashMap<>(); input.put("response", response); } + if (response.get("body") != null && !(response.get("body") instanceof Map)) { + return; + } Map body = (Map) response.get("body"); if (body == null) { body = new HashMap<>(); @@ -533,7 +563,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取聚合接口的响应body - * + * * @param fieldName */ public Object getInputRespBody(String fieldName) { @@ -545,6 +575,9 @@ public class StepContext extends ConcurrentHashMap { if (response == null) { return null; } + if (response.get("body") != null && !(response.get("body") instanceof Map)) { + return null; + } Map body = (Map) response.get("body"); if (body == null) { return null; @@ -554,7 +587,7 @@ public class StepContext extends ConcurrentHashMap { /** * 获取聚合接口的响应body - * + * */ public Object getInputRespBody() { Map input = (Map) this.get("input"); @@ -570,11 +603,16 @@ public class StepContext extends ConcurrentHashMap { /** * 获取聚合接口的请求body - * + * * @param fieldName */ + @SuppressWarnings("unused") public Object getInputReqBody(String fieldName) { - Map body = (Map) getInputReqAttr("body"); + Object respBody = getInputReqAttr("body"); + if (respBody != null && !(respBody instanceof Map)) { + return null; + } + Map body = (Map) respBody; if (body == null) { return null; } @@ -583,19 +621,19 @@ public class StepContext extends ConcurrentHashMap { /** * 获取聚合接口的请求body - * + * */ public Object getInputReqBody() { return getInputReqAttr("body"); } - + /** * 获取客户端URL请求参数(query string) */ public Object getInputReqParam() { return this.getInputReqAttr("params"); } - + /** * 获取客户端URL请求参数(query string) * @param paramName URL参数名 @@ -604,11 +642,11 @@ public class StepContext extends ConcurrentHashMap { Map params = (Map) this.getInputReqAttr("params"); return params == null ? null : paramName == null ? params : params.get(paramName); } - + /** * 获取聚合接口请求属性
* 可选属性:path,method,headers,params,body - * + * */ public Object getInputReqAttr(String key) { Map input = (Map) this.get("input"); @@ -622,4 +660,11 @@ public class StepContext extends ConcurrentHashMap { return request.get(key); } + public ConfigurableApplicationContext getApplicationContext(){ + return this.applicationContext; + } + + public void setApplicationContext(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } } diff --git a/src/main/java/we/fizz/exception/FizzException.java b/src/main/java/we/fizz/exception/FizzException.java new file mode 100644 index 0000000..e18085a --- /dev/null +++ b/src/main/java/we/fizz/exception/FizzException.java @@ -0,0 +1,10 @@ +package we.fizz.exception; + +public class FizzException extends Exception { + public FizzException(Throwable exception) { + super(exception); + } + public FizzException(String message) { + super(message); + } +} diff --git a/src/main/java/we/fizz/exception/FizzRuntimeException.java b/src/main/java/we/fizz/exception/FizzRuntimeException.java new file mode 100644 index 0000000..9205f0b --- /dev/null +++ b/src/main/java/we/fizz/exception/FizzRuntimeException.java @@ -0,0 +1,7 @@ +package we.fizz.exception; + +public class FizzRuntimeException extends RuntimeException { + public FizzRuntimeException (String message){ + super(message); + } +} diff --git a/src/main/java/we/fizz/input/ClientInputConfig.java b/src/main/java/we/fizz/input/ClientInputConfig.java index 68f5b87..61fff67 100644 --- a/src/main/java/we/fizz/input/ClientInputConfig.java +++ b/src/main/java/we/fizz/input/ClientInputConfig.java @@ -23,7 +23,7 @@ import java.util.Map; /** * * @author linwaiwai - * @author francis + * @author Francis Dong * */ public class ClientInputConfig extends InputConfig { @@ -41,6 +41,7 @@ public class ClientInputConfig extends InputConfig { @SuppressWarnings("unchecked") public ClientInputConfig(Map configBody) { + super(configBody); if(configBody.get("debug") != null) { this.debug = (boolean) configBody.get("debug"); } @@ -73,10 +74,11 @@ public class ClientInputConfig extends InputConfig { validateResponse = ((Map) configBody.get("validateResponse")); } } - + public ClientInputConfig() { - - } + super(null); + } + public boolean isDebug() { return debug; diff --git a/src/main/java/we/fizz/input/IInput.java b/src/main/java/we/fizz/input/IInput.java new file mode 100644 index 0000000..5709c0f --- /dev/null +++ b/src/main/java/we/fizz/input/IInput.java @@ -0,0 +1,29 @@ +package we.fizz.input; + +import org.springframework.context.ConfigurableApplicationContext; +import reactor.core.publisher.Mono; +import we.fizz.Step; +import we.fizz.StepContext; +import we.fizz.StepResponse; + +import java.lang.ref.SoftReference; +import java.util.Map; + +public interface IInput { + public static final InputType TYPE = null; + public static Class inputConfigClass() { + return null; + } + public String getName() ; + public boolean needRun(StepContext stepContext); + public void beforeRun(InputContext context); + public Mono run(); + + public StepResponse getStepResponse() ; + public void setStepResponse(StepResponse stepResponse); + public SoftReference getWeakStep(); + public void setWeakStep(SoftReference weakStep); + + public ConfigurableApplicationContext getCurrentApplicationContext(); + +} diff --git a/src/main/java/we/fizz/input/Input.java b/src/main/java/we/fizz/input/Input.java index 0904b68..a5c8ec1 100644 --- a/src/main/java/we/fizz/input/Input.java +++ b/src/main/java/we/fizz/input/Input.java @@ -16,10 +16,12 @@ */ package we.fizz.input; -import java.util.HashMap; +import java.lang.ref.SoftReference; import java.util.Map; +import org.springframework.context.ConfigurableApplicationContext; import reactor.core.publisher.Mono; +import we.fizz.Step; import we.fizz.StepContext; import we.fizz.StepResponse; @@ -34,7 +36,7 @@ public class Input { protected InputContext inputContext; protected StepResponse lastStepResponse = null; protected StepResponse stepResponse; - + private SoftReference weakStep; public void setConfig(InputConfig inputConfig) { config = inputConfig; } @@ -67,7 +69,7 @@ public class Input { } public void setName(String configName) { this.name = configName; - + } public StepResponse getStepResponse() { @@ -76,7 +78,21 @@ public class Input { public void setStepResponse(StepResponse stepResponse) { this.stepResponse = stepResponse; } - - - + + public SoftReference getWeakStep() { + return weakStep; + } + + public void setWeakStep(SoftReference weakStep) { + this.weakStep = weakStep; + } + + public ConfigurableApplicationContext getCurrentApplicationContext(){ + return this.getWeakStep() != null ? this.getWeakStep().get().getCurrentApplicationContext() : null; + } + + public static Class inputConfigClass (){ + return InputConfig.class; + } + } diff --git a/src/main/java/we/fizz/input/InputConfig.java b/src/main/java/we/fizz/input/InputConfig.java index 897efee..8ce6f91 100644 --- a/src/main/java/we/fizz/input/InputConfig.java +++ b/src/main/java/we/fizz/input/InputConfig.java @@ -17,6 +17,7 @@ package we.fizz.input; +import java.util.HashMap; import java.util.Map; /** @@ -28,6 +29,22 @@ public class InputConfig { private InputType type; protected Map dataMapping; + protected Map configMap; + + + private Map condition; + + public Map getCondition() { + return condition; + } + + public void setCondition(Map condition) { + this.condition = condition; + } + + public InputConfig(Map aConfigMap) { + configMap = aConfigMap; + } public InputType getType() { return type; @@ -45,4 +62,18 @@ public class InputConfig { this.dataMapping = dataMapping; } + private Map fallback = new HashMap(); + + public Map getFallback() { + return fallback; + } + + public void setFallback(Map fallback) { + this.fallback = fallback; + } + + public void parse(){ + + } + } diff --git a/src/main/java/we/fizz/input/InputFactory.java b/src/main/java/we/fizz/input/InputFactory.java index 733a6ca..5a335b7 100644 --- a/src/main/java/we/fizz/input/InputFactory.java +++ b/src/main/java/we/fizz/input/InputFactory.java @@ -17,6 +17,11 @@ package we.fizz.input; +import we.fizz.exception.FizzRuntimeException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; import java.util.Map; /** @@ -25,38 +30,54 @@ import java.util.Map; * */ public class InputFactory { - public static InputConfig createInputConfig(Map config) { + public static Map inputClasses = new HashMap(); + public static void registerInput(InputType type, Class inputClass){ + inputClasses.put(type, inputClass); + } + public static void unregisterInput(InputType type){ + inputClasses.remove(type); + } + public static InputConfig createInputConfig(Map config) { String type = (String) config.get("type"); InputType typeEnum = InputType.valueOf(type.toUpperCase()); InputConfig inputConfig = null; - switch(typeEnum) { - case REQUEST: - inputConfig = new RequestInputConfig(config); - - break; - case MYSQL: - inputConfig = new MySQLInputConfig(config); - break; + if (inputClasses.containsKey(typeEnum)){ + Class InputClass = inputClasses.get(typeEnum); + + try { + Method inputConfigClassMethod = InputClass.getMethod("inputConfigClass"); + Class InputConfigClass = (Class) inputConfigClassMethod.invoke(null); + Constructor constructor = null; + constructor = InputConfigClass.getDeclaredConstructor(Map.class); + constructor.setAccessible(true); + inputConfig = (InputConfig) constructor.newInstance(config); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new FizzRuntimeException(e.getMessage()); + } + inputConfig.setType(typeEnum); + inputConfig.setDataMapping((Map) config.get("dataMapping")); + inputConfig.parse(); + return inputConfig; } - inputConfig.setType(typeEnum); - inputConfig.setDataMapping((Map) config.get("dataMapping")); - - return inputConfig; + return null; } public static Input createInput(String type) { InputType typeEnum = InputType.valueOf(type.toUpperCase()); Input input = null; - switch(typeEnum) { - case REQUEST: - input = new RequestInput(); - break; - case MYSQL: - input = new MySQLInput(); - break; + if (inputClasses.containsKey(typeEnum)) { + Class InputClass = inputClasses.get(typeEnum); + Constructor constructor = null; + try { + constructor = InputClass.getDeclaredConstructor(); + constructor.setAccessible(true); + input = (Input) constructor.newInstance(); + return input; + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new FizzRuntimeException(e.getMessage()); + } } - - return input; + return null; } } diff --git a/src/main/java/we/fizz/input/InputType.java b/src/main/java/we/fizz/input/InputType.java index 752e8fe..5c7f36b 100644 --- a/src/main/java/we/fizz/input/InputType.java +++ b/src/main/java/we/fizz/input/InputType.java @@ -17,16 +17,29 @@ package we.fizz.input; +import java.util.HashMap; +import java.util.Map; + /** * * @author linwaiwai * */ -public enum InputType { - REQUEST("REQUEST"), - MYSQL("MYSQL"); - private final String type; - private InputType(String aType) { - this.type = aType; + +public class InputType { + + private final String type; + static private Map inputs = new HashMap(); + public InputType(String aType) { + this.type = aType; + inputs.put(aType, this); } + + public static InputType valueOf(String string) { + return inputs.get(string); + } + public String toString(){ + return type; + } + } \ No newline at end of file diff --git a/src/main/java/we/fizz/input/PathMapping.java b/src/main/java/we/fizz/input/PathMapping.java index 26004b2..07af7d2 100644 --- a/src/main/java/we/fizz/input/PathMapping.java +++ b/src/main/java/we/fizz/input/PathMapping.java @@ -32,7 +32,7 @@ import we.fizz.StepContext; /** * - * @author francis + * @author Francis Dong * */ public class PathMapping { @@ -46,7 +46,7 @@ public class PathMapping { } @SuppressWarnings({ "unchecked", "rawtypes" }) - public static void setByPath(ONode target, String path, Object obj) { + public static void setByPath(ONode target, String path, Object obj, boolean supportMultiLevels) { if (CommonConstants.WILDCARD_STAR.equals(path)) { if (obj instanceof ONode) { ONode node = (ONode) obj; @@ -58,6 +58,9 @@ public class PathMapping { } } else { String[] keys = path.split("\\."); + if (!supportMultiLevels) { + keys = new String[] { path }; + } ONode cur = target; for (int i = 0; i < keys.length - 1; i++) { cur = cur.get(keys[i]); @@ -96,12 +99,12 @@ public class PathMapping { } } - public static Map transformToMap(ONode ctxNode, Map rules) { - ONode target = transform(ctxNode, rules); + public static Map transformToMap(ONode ctxNode, Map rules, boolean supportMultiLevels) { + ONode target = transform(ctxNode, rules, supportMultiLevels); return target.toObject(Map.class); } - public static ONode transform(ONode ctxNode, Map rules) { + public static ONode transform(ONode ctxNode, Map rules, boolean supportMultiLevels) { ONode target = ONode.load(new HashMap()); if (rules.isEmpty()) { return target; @@ -131,8 +134,8 @@ public class PathMapping { String starEntryKey = null; for (Entry entry : rs.entrySet()) { - ONode val = ctxNode.select("$." + handlePath(entry.getValue())); - + String path = handlePath(entry.getValue()); + ONode val = select(ctxNode, path); Object obj = val; if (val != null && types.containsKey(entry.getKey())) { switch (types.get(entry.getKey())) { @@ -171,17 +174,38 @@ public class PathMapping { starValObj = obj; starEntryKey = entry.getKey(); }else { - setByPath(target, entry.getKey(), obj); + setByPath(target, entry.getKey(), obj, supportMultiLevels); } } if(starEntryKey != null) { - setByPath(target, starEntryKey, starValObj); + setByPath(target, starEntryKey, starValObj, supportMultiLevels); } return target; } - + + public static ONode select(ONode ctxNode, String path) { + ONode val = ctxNode.select("$." + path); + if (val != null && !val.isNull()) { + return val; + } + String[] arr = path.split("\\."); + if (arr.length == 6 && "headers".equals(arr[4]) && arr[5].endsWith("[0]")) { + ONode v = ctxNode.select("$." + path.substring(0, path.length() - 3)); + if (!v.isArray()) { + return v; + } + } + if (arr.length == 4 && "headers".equals(arr[2]) && arr[3].endsWith("[0]")) { + ONode v = ctxNode.select("$." + path.substring(0, path.length() - 3)); + if (!v.isArray()) { + return v; + } + } + return val; + } + public static Map getScriptRules(Map rules) { if (rules.isEmpty()) { return new HashMap<>(); @@ -261,6 +285,11 @@ public class PathMapping { break; } } + // upper case header name + if (list.size() > 5 && "headers".equals(list.get(4))) { + String headerName = list.get(5).toUpperCase(); + list.set(5, headerName); + } return String.join(".", list); }else if(path.startsWith("input")) { String[] arr = path.split("\\."); @@ -293,13 +322,18 @@ public class PathMapping { break; } } + // upper case header name + if (list.size() > 3 && "headers".equals(list.get(2))) { + String headerName = list.get(3).toUpperCase(); + list.set(3, headerName); + } return String.join(".", list); }else { return path; } } - + /** * 数据转换 * @@ -311,16 +345,40 @@ public class PathMapping { */ public static Map transform(ONode ctxNode, StepContext stepContext, Map fixed, Map mappingRules) { + return transform(ctxNode, stepContext, fixed, mappingRules, true); + } + + /** + * 数据转换 + * + * @param ctxNode + * @param stepContext + * @param fixed optional + * @param mappingRules optional + * @return + */ + public static Map transform(ONode ctxNode, StepContext stepContext, + Map fixed, Map mappingRules, boolean supportMultiLevels) { + if (fixed != null && fixed.containsKey(CommonConstants.WILDCARD_TILDE)) { + Object val = fixed.get(CommonConstants.WILDCARD_TILDE); + fixed = new HashMap<>(); + fixed.put(CommonConstants.WILDCARD_TILDE, val); + } + if (mappingRules != null && mappingRules.containsKey(CommonConstants.WILDCARD_TILDE)) { + Object val = mappingRules.get(CommonConstants.WILDCARD_TILDE); + mappingRules = new HashMap<>(); + mappingRules.put(CommonConstants.WILDCARD_TILDE, val); + } Map result = new HashMap<>(); if (fixed != null) { - result.putAll((Map) fixed); + result.putAll((Map) convertPath(fixed, supportMultiLevels)); } if (mappingRules != null) { // 路径映射 - ONode target = PathMapping.transform(ctxNode, mappingRules); + ONode target = PathMapping.transform(ctxNode, mappingRules, supportMultiLevels); // 脚本转换 Map scriptRules = PathMapping.getScriptRules(mappingRules); - Map scriptResult = ScriptHelper.executeScripts(target, scriptRules, ctxNode, stepContext); + Map scriptResult = ScriptHelper.executeScripts(target, scriptRules, ctxNode, stepContext, supportMultiLevels); if (scriptResult != null && !scriptResult.isEmpty()) { result.putAll(scriptResult); } @@ -328,4 +386,28 @@ public class PathMapping { return result; } + public static Map convertPath(Map fixed, boolean supportMultiLevels) { + ONode target = ONode.load(new HashMap()); + if (fixed.isEmpty()) { + return target.toObject(Map.class); + } + + // wildcard star entry + Object starValObj = null; + String starEntryKey = null; + + for (Entry entry : fixed.entrySet()) { + if (CommonConstants.WILDCARD_STAR.equals(entry.getKey())) { + starValObj = entry.getValue(); + starEntryKey = entry.getKey(); + }else { + setByPath(target, entry.getKey(), entry.getValue(), supportMultiLevels); + } + } + if(starEntryKey != null) { + setByPath(target, starEntryKey, starValObj, supportMultiLevels); + } + + return target.toObject(Map.class); + } } diff --git a/src/main/java/we/fizz/input/RPCInput.java b/src/main/java/we/fizz/input/RPCInput.java new file mode 100644 index 0000000..64708c2 --- /dev/null +++ b/src/main/java/we/fizz/input/RPCInput.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input; +import java.util.HashMap; +import java.util.Map; + +import javax.script.ScriptException; + +import org.noear.snack.ONode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; + +import reactor.core.publisher.Mono; +import we.exception.ExecuteScriptException; +import we.fizz.StepContext; +import we.flume.clients.log4j2appender.LogService; +import we.util.JacksonUtils; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +public class RPCInput extends Input { + protected static final Logger LOGGER = LoggerFactory.getLogger(RPCInput.class.getName()); + protected static final String FALLBACK_MODE_STOP = "stop"; + protected static final String FALLBACK_MODE_CONTINUE = "continue"; + protected Map request = new HashMap<>(); + protected Map response = new HashMap<>(); + + protected void doRequestMapping(InputConfig aConfig, InputContext inputContext) { + + } + + protected void doOnResponseSuccess(RPCResponse cr, long elapsedMillis) { + + } + protected Mono bodyToMono(RPCResponse cr){ + return cr.getBodyMono(); + } + + protected void doOnBodyError(Throwable ex, long elapsedMillis) { + } + + protected void doOnBodySuccess(Object resp, long elapsedMillis) { + } + + protected void doResponseMapping(InputConfig aConfig, InputContext inputContext, Object responseBody) { + } + + @Override + @SuppressWarnings("unchecked") + public boolean needRun(StepContext stepContext) { + Map condition = ((InputConfig) config).getCondition(); + if (CollectionUtils.isEmpty(condition)) { + // 没有配置condition,直接运行 + return Boolean.TRUE; + } + + ONode ctxNode = PathMapping.toONode(stepContext); + try { + Boolean needRun = ScriptHelper.execute(condition, ctxNode, stepContext, Boolean.class); + return needRun != null ? needRun : Boolean.TRUE; + } catch (ScriptException e) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(condition), e); + throw new ExecuteScriptException(e, stepContext, condition); + } + } + + protected String prefix; + + @Override + public Mono run() { + long t1 = System.currentTimeMillis(); + this.doRequestMapping(config, inputContext); + inputContext.getStepContext().addElapsedTime(stepResponse.getStepName() + "-" + this.name + "-RequestMapping", + System.currentTimeMillis() - t1); + + prefix = stepResponse.getStepName() + "-" + "调用接口"; + long start = System.currentTimeMillis(); + Mono rpcResponse = this.getClientSpecFromContext(config, inputContext); + Mono body = rpcResponse.flatMap(cr->{ + return Mono.just(cr).doOnError(throwable -> cleanup(cr)); + }).doOnSuccess(cr -> { + long elapsedMillis = System.currentTimeMillis() - start; + this.doOnResponseSuccess(cr, elapsedMillis); + + }).flatMap(cr -> { return this.bodyToMono(cr); }).doOnSuccess(resp -> { + long elapsedMillis = System.currentTimeMillis() - start; + this.doOnBodySuccess(resp, elapsedMillis); + }).doOnError(ex -> { + long elapsedMillis = System.currentTimeMillis() - start; + this.doOnBodyError(ex, elapsedMillis); + }); + + // fallback handler + InputConfig reqConfig = (InputConfig) config; + if (reqConfig.getFallback() != null) { + Map fallback = reqConfig.getFallback(); + String mode = fallback.get("mode"); + if (FALLBACK_MODE_STOP.equals(mode)) { + body = body.onErrorStop(); + } else if (FALLBACK_MODE_CONTINUE.equals(mode)) { + body = body.onErrorResume(ex -> { + return Mono.just(fallback.get("defaultResult")); + }); + } else { + body = body.onErrorStop(); + } + } + + return body.flatMap(item -> { + Map result = new HashMap(); + result.put("data", item); + result.put("request", this); + + long t3 = System.currentTimeMillis(); + this.doResponseMapping(config, inputContext, item); + inputContext.getStepContext().addElapsedTime( + stepResponse.getStepName() + "-" + this.name + "-ResponseMapping", System.currentTimeMillis() - t3); + + return Mono.just(result); + }); + } + + private void cleanup(RPCResponse clientResponse) { + + } + + protected Mono getClientSpecFromContext(InputConfig aConfig, InputContext inputContext) { + return null; + } + +} diff --git a/src/main/java/we/fizz/input/RPCResponse.java b/src/main/java/we/fizz/input/RPCResponse.java new file mode 100644 index 0000000..f015d61 --- /dev/null +++ b/src/main/java/we/fizz/input/RPCResponse.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input; + +import org.springframework.util.MultiValueMap; + +import reactor.core.publisher.Mono; + +/** + * + * @author linwaiwai + * + */ +public class RPCResponse { + private MultiValueMap headers; + private Mono bodyMono; + + public MultiValueMap getHeaders() { + return headers; + } + + public void setHeaders(MultiValueMap headers) { + this.headers = headers; + } + + public Mono getBodyMono() { + return bodyMono; + } + + public void setBodyMono(Mono bodyMono) { + this.bodyMono = bodyMono; + } +} diff --git a/src/main/java/we/fizz/input/RequestInput.java b/src/main/java/we/fizz/input/RequestInput.java deleted file mode 100644 index 7ca89cd..0000000 --- a/src/main/java/we/fizz/input/RequestInput.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright (C) 2020 the original author or authors. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package we.fizz.input; - -import java.util.HashMap; -import java.util.Map; - -import javax.script.ScriptException; - -import org.noear.snack.ONode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.reactive.function.client.ClientResponse; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import com.alibaba.fastjson.JSON; - -import reactor.core.publisher.Mono; -import we.FizzAppContext; -import we.constants.CommonConstants; -import we.exception.ExecuteScriptException; -import we.fizz.StepContext; -import we.fizz.StepResponse; -import we.flume.clients.log4j2appender.LogService; -import we.proxy.FizzWebClient; -import we.util.JacksonUtils; -import we.util.MapUtil; - -/** - * - * @author linwaiwai - * @author francis - * - */ -@SuppressWarnings("unchecked") -public class RequestInput extends Input { - - private static final Logger LOGGER = LoggerFactory.getLogger(RequestInput.class); - private InputType type; - protected Map dataMapping; - protected Map request = new HashMap<>(); - protected Map response = new HashMap<>(); - - private static final String FALLBACK_MODE_STOP = "stop"; - private static final String FALLBACK_MODE_CONTINUE = "continue"; - - private static final String CONTENT_TYPE_JSON = "application/json"; - private static final String CONTENT_TYPE_XML = "application/xml"; - private static final String CONTENT_TYPE_JS = "application/javascript"; - private static final String CONTENT_TYPE_HTML = "text/html"; - private static final String CONTENT_TYPE_TEXT = "text/plain"; - - private static final String CONTENT_TYPE = "content-type"; - - public InputType getType() { - return type; - } - - public void setType(InputType typeEnum) { - this.type = typeEnum; - } - - public Map getDataMapping() { - return dataMapping; - } - - public void setDataMapping(Map dataMapping) { - this.dataMapping = dataMapping; - } - - private void doRequestMapping(InputConfig aConfig, InputContext inputContext) { - RequestInputConfig config = (RequestInputConfig) aConfig; - - // 把请求信息放入stepContext - Map group = new HashMap<>(); - group.put("request", request); - group.put("response", response); - this.stepResponse.getRequests().put(name, group); - - HttpMethod method = HttpMethod.valueOf(config.getMethod().toUpperCase()); - request.put("method", method); - - Map params = new HashMap<>(); - params.putAll(MapUtil.toHashMap(config.getQueryParams())); - request.put("params", params); - - // 数据转换 - if (inputContext != null && inputContext.getStepContext() != null) { - StepContext stepContext = inputContext.getStepContext(); - Map dataMapping = this.getConfig().getDataMapping(); - if (dataMapping != null) { - Map requestMapping = (Map) dataMapping.get("request"); - if (requestMapping != null && !StringUtils.isEmpty(requestMapping)) { - ONode ctxNode = PathMapping.toONode(stepContext); - - // headers - request.put("headers", - PathMapping.transform(ctxNode, stepContext, - (Map) requestMapping.get("fixedHeaders"), - (Map) requestMapping.get("headers"))); - - // params - params.putAll(PathMapping.transform(ctxNode, stepContext, - (Map) requestMapping.get("fixedParams"), - (Map) requestMapping.get("params"))); - request.put("params", params); - - // body - Map body = PathMapping.transform(ctxNode, stepContext, - (Map) requestMapping.get("fixedBody"), - (Map) requestMapping.get("body")); - - - // script - if (requestMapping.get("script") != null) { - Map scriptCfg = (Map) requestMapping.get("script"); - try { - Object reqBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); - if (reqBody != null) { - body.putAll((Map) reqBody); - } - } catch (ScriptException e) { - LogService.setBizId(inputContext.getStepContext().getTraceId()); - LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); - throw new ExecuteScriptException(e, stepContext, scriptCfg); - } - } - request.put("body", body); - } - } - } - - UriComponents uriComponents = UriComponentsBuilder.fromUriString(config.getBaseUrl() + config.getPath()) - .queryParams(MapUtil.toMultiValueMap(params)).build(); - request.put("url", uriComponents.toUriString()); - } - - private void doResponseMapping(InputConfig aConfig, InputContext inputContext, Object responseBody) { - RequestInputConfig config = (RequestInputConfig) aConfig; - response.put("body", responseBody); - // 数据转换 - if (inputContext != null && inputContext.getStepContext() != null) { - StepContext stepContext = inputContext.getStepContext(); - Map dataMapping = this.getConfig().getDataMapping(); - if (dataMapping != null) { - Map responseMapping = (Map) dataMapping.get("response"); - if (responseMapping != null && !StringUtils.isEmpty(responseMapping)) { - ONode ctxNode = PathMapping.toONode(stepContext); - - // headers - Map fixedHeaders = (Map) responseMapping.get("fixedHeaders"); - Map headerMapping = (Map) responseMapping.get("headers"); - if ((fixedHeaders != null && !fixedHeaders.isEmpty()) - || (headerMapping != null && !headerMapping.isEmpty())) { - Map headers = new HashMap<>(); - headers.putAll(PathMapping.transform(ctxNode, stepContext, fixedHeaders, headerMapping)); - response.put("headers", headers); - } - - // body - Map fixedBody = (Map) responseMapping.get("fixedBody"); - Map bodyMapping = (Map) responseMapping.get("body"); - Map scriptCfg = (Map) responseMapping.get("script"); - if ((fixedBody != null && !fixedBody.isEmpty()) || (bodyMapping != null && !bodyMapping.isEmpty()) - || (scriptCfg != null && scriptCfg.get("type") != null - && scriptCfg.get("source") != null)) { - // body - Map body = new HashMap<>(); - body.putAll(PathMapping.transform(ctxNode, stepContext, fixedBody, bodyMapping)); - - // script - if (scriptCfg != null && scriptCfg.get("type") != null && scriptCfg.get("source") != null) { - try { - Object respBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); - if (respBody != null) { - body.putAll((Map) respBody); - } - } catch (ScriptException e) { - LogService.setBizId(inputContext.getStepContext().getTraceId()); - LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); - throw new ExecuteScriptException(e, stepContext, scriptCfg); - } - } - response.put("body", body); - } - } - } else { - response.put("body", responseBody); - } - } - } - - private Mono getClientSpecFromContext(InputConfig aConfig, InputContext inputContext) { - RequestInputConfig config = (RequestInputConfig) aConfig; - - int timeout = config.getTimeout() < 1 ? 3000 : config.getTimeout() > 10000 ? 10000 : config.getTimeout(); - - HttpMethod method = HttpMethod.valueOf(config.getMethod()); - String url = (String) request.get("url"); - - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = new HashMap<>(); - } - if (!headers.containsKey("Content-Type")) { - // defalut content-type - headers.put("Content-Type", "application/json; charset=UTF-8"); - } - headers.put(CommonConstants.HEADER_TRACE_ID, inputContext.getStepContext().getTraceId()); - - HttpMethod aggrMethod = HttpMethod.valueOf(inputContext.getStepContext().getInputReqAttr("method").toString()); - String aggrPath = (String)inputContext.getStepContext().getInputReqAttr("path"); - String aggrService = aggrPath.split("\\/")[2]; - - FizzWebClient client = FizzAppContext.appContext.getBean(FizzWebClient.class); - return client.aggrSend(aggrService, aggrMethod, aggrPath, null, method, url, - MapUtil.toHttpHeaders(headers), request.get("body"), (long)timeout); - } - - private Map getResponses(Map stepContext2) { - // TODO Auto-generated method stub - return null; - } - - @Override - @SuppressWarnings("unchecked") - public boolean needRun(StepContext stepContext) { - Map condition = ((RequestInputConfig) config).getCondition(); - if (CollectionUtils.isEmpty(condition)) { - // 没有配置condition,直接运行 - return Boolean.TRUE; - } - - ONode ctxNode = PathMapping.toONode(stepContext); - try { - Boolean needRun = ScriptHelper.execute(condition, ctxNode, stepContext, Boolean.class); - return needRun != null ? needRun : Boolean.TRUE; - } catch (ScriptException e) { - LogService.setBizId(inputContext.getStepContext().getTraceId()); - LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(condition), e); - throw new ExecuteScriptException(e, stepContext, condition); - } - } - - @Override - public Mono run() { - long t1 = System.currentTimeMillis(); - this.doRequestMapping(config, inputContext); - inputContext.getStepContext().addElapsedTime(stepResponse.getStepName() + "-" + this.name + "-RequestMapping", - System.currentTimeMillis() - t1); - - Map tmpMap = new HashMap<>(); - - String prefix = stepResponse.getStepName() + "-" + "调用接口"; - long start = System.currentTimeMillis(); - Mono clientResponse = this.getClientSpecFromContext(config, inputContext); - Mono body = clientResponse.flatMap(cr->{ - return Mono.just(cr).doOnError(throwable -> cleanup(cr)); - }).flatMap(cr -> { - long elapsedMillis = System.currentTimeMillis() - start; - HttpHeaders httpHeaders = cr.headers().asHttpHeaders(); - Map headers = new HashMap<>(); - httpHeaders.forEach((key, value) -> { - if (value.size() > 1) { - headers.put(key, value); - } else { - headers.put(key, httpHeaders.getFirst(key)); - } - }); - tmpMap.put(CONTENT_TYPE, httpHeaders.getFirst(CONTENT_TYPE)); - headers.put("elapsedTime", elapsedMillis + "ms"); - this.response.put("headers", headers); - inputContext.getStepContext().addElapsedTime(prefix + request.get("url"), elapsedMillis); - - return cr.bodyToMono(String.class); - }).doOnSuccess(resp -> { - long elapsedMillis = System.currentTimeMillis() - start; - if(inputContext.getStepContext().isDebug()) { - LogService.setBizId(inputContext.getStepContext().getTraceId()); - LOGGER.info("{} 耗时:{}ms URL={}, reqHeader={} req={} resp={}", prefix, elapsedMillis, request.get("url"), - JSON.toJSONString(this.request.get("headers")), - JSON.toJSONString(this.request.get("body")), resp); - } - }).doOnError(ex -> { - LogService.setBizId(inputContext.getStepContext().getTraceId()); - LOGGER.warn("failed to call {}", request.get("url"), ex); - long elapsedMillis = System.currentTimeMillis() - start; - inputContext.getStepContext().addElapsedTime( - stepResponse.getStepName() + "-" + "调用接口 failed " + request.get("url"), elapsedMillis); - }); - - // fallback handler - RequestInputConfig reqConfig = (RequestInputConfig) config; - if (reqConfig.getFallback() != null) { - Map fallback = reqConfig.getFallback(); - String mode = fallback.get("mode"); - if (FALLBACK_MODE_STOP.equals(mode)) { - body = body.onErrorStop(); - } else if (FALLBACK_MODE_CONTINUE.equals(mode)) { - body = body.onErrorResume(ex -> { - return Mono.just(fallback.get("defaultResult")); - }); - } else { - body = body.onErrorStop(); - } - } - - return body.flatMap(item -> { - Map result = new HashMap(); - result.put("data", item); - result.put("request", this); - - long t3 = System.currentTimeMillis(); - this.doResponseMapping(config, inputContext, parseBody((String) tmpMap.get(CONTENT_TYPE), item)); - inputContext.getStepContext().addElapsedTime( - stepResponse.getStepName() + "-" + this.name + "-ResponseMapping", System.currentTimeMillis() - t3); - - return Mono.just(result); - }); - } - - // Parse response body according to content-type header - public Object parseBody(String contentType, String responseBody) { - String[] cts = contentType.split(";"); - Object body = null; - for (int i = 0; i < cts.length; i++) { - String ct = cts[i].toLowerCase(); - switch (ct) { - case CONTENT_TYPE_JSON: - body = JSON.parse(responseBody); - break; - case CONTENT_TYPE_TEXT: - // parse text as json if start with "{" and end with "}" or start with "[" and - // end with "]" - if ((responseBody.startsWith("{") && responseBody.endsWith("}")) - || (responseBody.startsWith("[") && responseBody.endsWith("]"))) { - try { - body = JSON.parse(responseBody); - } catch (Exception e) { - body = responseBody; - } - } else { - body = responseBody; - } - break; - case CONTENT_TYPE_XML: - body = responseBody; - break; - case CONTENT_TYPE_HTML: - body = responseBody; - break; - case CONTENT_TYPE_JS: - body = responseBody; - break; - } - if (body != null) { - break; - } - } - if (body == null) { - body = responseBody; - } - return body; - } - - private void cleanup(ClientResponse clientResponse) { - if (clientResponse != null) { - clientResponse.bodyToMono(Void.class).subscribe(); - } - } - -} diff --git a/src/main/java/we/fizz/input/ScriptHelper.java b/src/main/java/we/fizz/input/ScriptHelper.java index 545ae46..0bd11dd 100644 --- a/src/main/java/we/fizz/input/ScriptHelper.java +++ b/src/main/java/we/fizz/input/ScriptHelper.java @@ -42,7 +42,7 @@ import we.util.ScriptUtils; /** * - * @author francis + * @author Francis Dong * */ public class ScriptHelper { @@ -96,13 +96,13 @@ public class ScriptHelper { } public static Map executeScripts(ONode target, Map scriptRules, ONode ctxNode, - StepContext stepContext) { - return executeScripts(target, scriptRules, ctxNode, stepContext, Object.class); + StepContext stepContext, boolean supportMultiLevels) { + return executeScripts(target, scriptRules, ctxNode, stepContext, Object.class, supportMultiLevels); } @SuppressWarnings("unchecked") public static Map executeScripts(ONode target, Map scriptRules, ONode ctxNode, - StepContext stepContext, Class clazz) { + StepContext stepContext, Class clazz, boolean supportMultiLevels) { if(target == null) { target = ONode.load(new HashMap()); } @@ -117,7 +117,7 @@ public class ScriptHelper { starValObj = execute(scriptCfg, ctxNode, stepContext, clazz); starEntryKey = entry.getKey(); }else { - PathMapping.setByPath(target, entry.getKey(), execute(scriptCfg, ctxNode, stepContext, clazz)); + PathMapping.setByPath(target, entry.getKey(), execute(scriptCfg, ctxNode, stepContext, clazz), supportMultiLevels); } } catch (ScriptException e) { LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); @@ -125,7 +125,7 @@ public class ScriptHelper { } } if(starEntryKey != null) { - PathMapping.setByPath(target, starEntryKey, starValObj); + PathMapping.setByPath(target, starEntryKey, starValObj, supportMultiLevels); } } return target.toObject(Map.class); diff --git a/src/main/java/we/fizz/input/extension/dubbo/DubboInput.java b/src/main/java/we/fizz/input/extension/dubbo/DubboInput.java new file mode 100644 index 0000000..36f1fd0 --- /dev/null +++ b/src/main/java/we/fizz/input/extension/dubbo/DubboInput.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input.extension.dubbo; + +import java.util.HashMap; +import java.util.Map; + +import javax.script.ScriptException; + +import org.noear.snack.ONode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.StringUtils; + +import reactor.core.publisher.Mono; +import we.constants.CommonConstants; +import we.exception.ExecuteScriptException; +import we.fizz.StepContext; +import we.fizz.input.InputConfig; +import we.fizz.input.InputContext; +import we.fizz.input.InputType; +import we.fizz.input.PathMapping; +import we.fizz.input.RPCInput; +import we.fizz.input.RPCResponse; +import we.fizz.input.ScriptHelper; +import we.flume.clients.log4j2appender.LogService; +import we.proxy.dubbo.ApacheDubboGenericService; +import we.proxy.dubbo.DubboInterfaceDeclaration; +import we.util.JacksonUtils; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +public class DubboInput extends RPCInput { + static public InputType TYPE = new InputType("DUBBO"); + private static final Logger LOGGER = LoggerFactory.getLogger(DubboInput.class); + + @SuppressWarnings("unchecked") + @Override + protected Mono getClientSpecFromContext(InputConfig aConfig, InputContext inputContext) { + DubboInputConfig config = (DubboInputConfig) aConfig; + + int timeout = config.getTimeout() < 1 ? 3000 : config.getTimeout() > 10000 ? 10000 : config.getTimeout(); + Map attachments = (Map) request.get("attachments"); + ConfigurableApplicationContext applicationContext = this.getCurrentApplicationContext(); + Map body = (Map) request.get("body"); + + ApacheDubboGenericService proxy = applicationContext.getBean(ApacheDubboGenericService.class); + DubboInterfaceDeclaration declaration = new DubboInterfaceDeclaration(); + declaration.setServiceName(config.getServiceName()); + declaration.setVersion(config.getVersion()); + declaration.setGroup(config.getGroup()); + declaration.setMethod(config.getMethod()); + declaration.setParameterTypes(config.getParamTypes()); + declaration.setTimeout(timeout); + HashMap contextAttachment = null; + if (attachments == null) { + contextAttachment = new HashMap(); + } else { + contextAttachment = new HashMap(attachments); + } + if (inputContext.getStepContext() != null && inputContext.getStepContext().getTraceId() != null) { + contextAttachment.put(CommonConstants.HEADER_TRACE_ID, inputContext.getStepContext().getTraceId()); + } + + Mono proxyResponse = proxy.send(body, declaration, contextAttachment); + return proxyResponse.flatMap(cr -> { + DubboRPCResponse response = new DubboRPCResponse(); + response.setBodyMono(Mono.just(cr)); + return Mono.just(response); + }); + } + + protected void doRequestMapping(InputConfig aConfig, InputContext inputContext) { + DubboInputConfig config = (DubboInputConfig) aConfig; + + // 把请求信息放入stepContext + Map group = new HashMap<>(); + group.put("request", request); + group.put("response", response); + this.stepResponse.getRequests().put(name, group); + + request.put("serviceName", config.getServiceName()); + request.put("version", config.getVersion()); + request.put("group", config.getGroup()); + request.put("method", config.getMethod()); + request.put("paramTypes", config.getParamTypes()); + + // 数据转换 + if (inputContext != null && inputContext.getStepContext() != null) { + StepContext stepContext = inputContext.getStepContext(); + Map dataMapping = this.getConfig().getDataMapping(); + if (dataMapping != null) { + Map requestMapping = (Map) dataMapping.get("request"); + if (requestMapping != null && !StringUtils.isEmpty(requestMapping)) { + ONode ctxNode = PathMapping.toONode(stepContext); + + // attachments + Map attachments = PathMapping.transform(ctxNode, stepContext, + (Map) requestMapping.get("fixedHeaders"), + (Map) requestMapping.get("headers")); + if (attachments.containsKey(CommonConstants.WILDCARD_TILDE) + && attachments.get(CommonConstants.WILDCARD_TILDE) instanceof Map) { + request.put("attachments", attachments.get(CommonConstants.WILDCARD_TILDE)); + } else { + request.put("attachments", attachments); + } + + // body + Map body = PathMapping.transform(ctxNode, stepContext, + (Map) requestMapping.get("fixedBody"), + (Map) requestMapping.get("body")); + if (body.containsKey(CommonConstants.WILDCARD_TILDE)) { + request.put("body", body.get(CommonConstants.WILDCARD_TILDE)); + } else { + // script + if (requestMapping.get("script") != null) { + Map scriptCfg = (Map) requestMapping.get("script"); + try { + Object reqBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); + if (reqBody != null) { + body.putAll((Map) reqBody); + } + } catch (ScriptException e) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); + throw new ExecuteScriptException(e, stepContext, scriptCfg); + } + } + request.put("body", body); + } + } + } + } + } + + protected void doOnResponseSuccess(RPCResponse cr, long elapsedMillis) { + inputContext.getStepContext().addElapsedTime(this.getApiName(), elapsedMillis); + } + + protected Mono bodyToMono(RPCResponse cr) { + return cr.getBodyMono(); + } + + protected void doOnBodyError(Throwable ex, long elapsedMillis) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("failed to call {}", this.getApiName(), ex); + inputContext.getStepContext().addElapsedTime(this.getApiName() + " failed ", elapsedMillis); + } + + protected void doOnBodySuccess(Object resp, long elapsedMillis) { + + } + + protected void doResponseMapping(InputConfig aConfig, InputContext inputContext, Object responseBody) { + DubboInputConfig config = (DubboInputConfig) aConfig; + response.put("body", responseBody); + + // 数据转换 + if (inputContext != null && inputContext.getStepContext() != null) { + StepContext stepContext = inputContext.getStepContext(); + Map dataMapping = this.getConfig().getDataMapping(); + if (dataMapping != null) { + Map responseMapping = (Map) dataMapping.get("response"); + if (responseMapping != null && !StringUtils.isEmpty(responseMapping)) { + ONode ctxNode = PathMapping.toONode(stepContext); + + // body + Map fixedBody = (Map) responseMapping.get("fixedBody"); + Map bodyMapping = (Map) responseMapping.get("body"); + Map scriptCfg = (Map) responseMapping.get("script"); + if ((fixedBody != null && !fixedBody.isEmpty()) || (bodyMapping != null && !bodyMapping.isEmpty()) + || (scriptCfg != null && scriptCfg.get("type") != null + && scriptCfg.get("source") != null)) { + // body + Map body = new HashMap<>(); + body.putAll(PathMapping.transform(ctxNode, stepContext, fixedBody, bodyMapping)); + if (body.containsKey(CommonConstants.WILDCARD_TILDE)) { + response.put("body", body.get(CommonConstants.WILDCARD_TILDE)); + } else { + // script + if (scriptCfg != null && scriptCfg.get("type") != null && scriptCfg.get("source") != null) { + try { + Object respBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); + if (respBody != null) { + body.putAll((Map) respBody); + } + } catch (ScriptException e) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); + throw new ExecuteScriptException(e, stepContext, scriptCfg); + } + } + response.put("body", body); + } + } + } + } else { + response.put("body", responseBody); + } + } + } + + public static Class inputConfigClass() { + return DubboInputConfig.class; + } + + private String getApiName() { + return prefix + " - " + request.get("serviceName") + " - " + request.get("method"); + } + +} diff --git a/src/main/java/we/fizz/input/extension/dubbo/DubboInputConfig.java b/src/main/java/we/fizz/input/extension/dubbo/DubboInputConfig.java new file mode 100644 index 0000000..0cc8f4e --- /dev/null +++ b/src/main/java/we/fizz/input/extension/dubbo/DubboInputConfig.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package we.fizz.input.extension.dubbo; + +import java.util.Map; + +import org.springframework.util.StringUtils; + +import we.fizz.exception.FizzRuntimeException; +import we.fizz.input.InputConfig; + +/** +* +* @author linwaiwai +* @author Francis Dong +* +*/ +public class DubboInputConfig extends InputConfig { + private String serviceName; + private String version; + private String group; + private String method; + private String paramTypes; + private int timeout; + + public DubboInputConfig(Map configMap) { + super(configMap); + } + + public void parse() { + String serviceName = (String) configMap.get("serviceName"); + if (StringUtils.isEmpty(serviceName)) { + throw new FizzRuntimeException("service name can not be blank"); + } + setServiceName(serviceName); + + String version = (String) configMap.get("version"); + if (!StringUtils.isEmpty(version)) { + setVersion(version); + } + + String group = (String) configMap.get("group"); + if (!StringUtils.isEmpty(group)) { + setGroup(group); + } + + String method = (String) configMap.get("method"); + if (StringUtils.isEmpty(method)) { + throw new FizzRuntimeException("method can not be blank"); + } + setMethod(method); + String paramTypes = (String) configMap.get("paramTypes"); + if (!StringUtils.isEmpty(paramTypes)) { + setParamTypes(paramTypes); + } + + if (configMap.get("timeout") != null) { + setTimeout(Integer.valueOf(configMap.get("timeout").toString())); + } + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getParamTypes() { + return paramTypes; + } + + public void setParamTypes(String paramTypes) { + this.paramTypes = paramTypes; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getServiceName() { + return serviceName; + } + + public String getMethod() { + return method; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + +} diff --git a/src/main/java/we/fizz/input/MySQLInput.java b/src/main/java/we/fizz/input/extension/dubbo/DubboRPCResponse.java similarity index 76% rename from src/main/java/we/fizz/input/MySQLInput.java rename to src/main/java/we/fizz/input/extension/dubbo/DubboRPCResponse.java index f482d0c..3b4e14e 100644 --- a/src/main/java/we/fizz/input/MySQLInput.java +++ b/src/main/java/we/fizz/input/extension/dubbo/DubboRPCResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 the original author or authors. + * Copyright (C) 2021 the original author or authors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,13 +15,14 @@ * along with this program. If not, see . */ -package we.fizz.input; +package we.fizz.input.extension.dubbo; + +import we.fizz.input.RPCResponse; /** - * - * @author linwaiwai - * - */ -public class MySQLInput extends Input { - +* +* @author linwaiwai +* +*/ +public class DubboRPCResponse extends RPCResponse { } diff --git a/src/main/java/we/fizz/input/extension/grpc/GRPCResponse.java b/src/main/java/we/fizz/input/extension/grpc/GRPCResponse.java new file mode 100644 index 0000000..27e4ca5 --- /dev/null +++ b/src/main/java/we/fizz/input/extension/grpc/GRPCResponse.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input.extension.grpc; + +import org.springframework.http.HttpStatus; +import we.fizz.input.RPCResponse; + +/** + * + * @author linwaiwai + * + */ +public class GRPCResponse extends RPCResponse { + private HttpStatus statusCode; + public void setStatus(HttpStatus statusCode) { + this.statusCode = statusCode; + } + + public HttpStatus getStatusCode() { + return statusCode; + } +} diff --git a/src/main/java/we/fizz/input/extension/grpc/GrpcInput.java b/src/main/java/we/fizz/input/extension/grpc/GrpcInput.java new file mode 100644 index 0000000..d3eca4a --- /dev/null +++ b/src/main/java/we/fizz/input/extension/grpc/GrpcInput.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input.extension.grpc; + +import com.alibaba.fastjson.JSON; + +import org.noear.snack.ONode; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.StringUtils; + +import reactor.core.publisher.Mono; +import we.constants.CommonConstants; +import we.exception.ExecuteScriptException; +import we.fizz.StepContext; +import we.fizz.input.*; +import we.flume.clients.log4j2appender.LogService; +import we.proxy.grpc.GrpcGenericService; +import we.proxy.grpc.GrpcInstanceService; +import we.proxy.grpc.GrpcInterfaceDeclaration; +import we.util.JacksonUtils; + +import java.util.HashMap; +import java.util.Map; + +import javax.script.ScriptException; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +public class GrpcInput extends RPCInput implements IInput { + static public InputType TYPE = new InputType("GRPC"); + + @SuppressWarnings("unchecked") + @Override + protected Mono getClientSpecFromContext(InputConfig aConfig, InputContext inputContext) { + GrpcInputConfig config = (GrpcInputConfig) aConfig; + + int timeout = config.getTimeout() < 1 ? 3000 : config.getTimeout() > 10000 ? 10000 : config.getTimeout(); + Map attachments = (Map) request.get("attachments"); + ConfigurableApplicationContext applicationContext = this.getCurrentApplicationContext(); + Map body = (Map) request.get("body"); + String endpoint = (String) request.get("endpoint"); + + GrpcGenericService proxy = applicationContext.getBean(GrpcGenericService.class); + GrpcInterfaceDeclaration declaration = new GrpcInterfaceDeclaration(); + declaration.setEndpoint(endpoint); + declaration.setServiceName(config.getServiceName()); + declaration.setMethod(config.getMethod()); + declaration.setTimeout(timeout); + HashMap contextAttachment = null; + if (attachments == null) { + contextAttachment = new HashMap(); + } else { + contextAttachment = new HashMap(attachments); + } + if (inputContext.getStepContext() != null && inputContext.getStepContext().getTraceId() != null) { + contextAttachment.put(CommonConstants.HEADER_TRACE_ID, inputContext.getStepContext().getTraceId()); + } + + Mono proxyResponse = proxy.send(JSON.toJSONString(body), declaration, contextAttachment); + return proxyResponse.flatMap(cr -> { + GRPCResponse response = new GRPCResponse(); + response.setBodyMono(Mono.just(cr)); + return Mono.just(response); + }); + } + + @SuppressWarnings("unchecked") + protected void doRequestMapping(InputConfig aConfig, InputContext inputContext) { + GrpcInputConfig config = (GrpcInputConfig) aConfig; + + // 把请求信息放入stepContext + Map group = new HashMap<>(); + group.put("request", request); + group.put("response", response); + this.stepResponse.getRequests().put(name, group); + + request.put("serviceName", config.getServiceName()); + request.put("method", config.getMethod()); + GrpcInstanceService grpcInstanceService = this.getCurrentApplicationContext() + .getBean(GrpcInstanceService.class); + request.put("endpoint", grpcInstanceService.getInstanceRoundRobin(config.getServiceName())); + + // 数据转换 + if (inputContext != null && inputContext.getStepContext() != null) { + StepContext stepContext = inputContext.getStepContext(); + Map dataMapping = this.getConfig().getDataMapping(); + if (dataMapping != null) { + Map requestMapping = (Map) dataMapping.get("request"); + if (requestMapping != null && !StringUtils.isEmpty(requestMapping)) { + ONode ctxNode = PathMapping.toONode(stepContext); + + // attachments + Map attachments = PathMapping.transform(ctxNode, stepContext, + (Map) requestMapping.get("fixedHeaders"), + (Map) requestMapping.get("headers")); + if (attachments.containsKey(CommonConstants.WILDCARD_TILDE) + && attachments.get(CommonConstants.WILDCARD_TILDE) instanceof Map) { + request.put("attachments", attachments.get(CommonConstants.WILDCARD_TILDE)); + } else { + request.put("attachments", attachments); + } + + // body + Map body = PathMapping.transform(ctxNode, stepContext, + (Map) requestMapping.get("fixedBody"), + (Map) requestMapping.get("body")); + if (body.containsKey(CommonConstants.WILDCARD_TILDE)) { + request.put("body", body.get(CommonConstants.WILDCARD_TILDE)); + } else { + // script + if (requestMapping.get("script") != null) { + Map scriptCfg = (Map) requestMapping.get("script"); + try { + Object reqBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); + if (reqBody != null) { + body.putAll((Map) reqBody); + } + } catch (ScriptException e) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); + throw new ExecuteScriptException(e, stepContext, scriptCfg); + } + } + request.put("body", body); + } + } + } + } + } + + protected void doOnResponseSuccess(RPCResponse cr, long elapsedMillis) { + inputContext.getStepContext().addElapsedTime(this.getApiName(), elapsedMillis); + } + + protected Mono bodyToMono(RPCResponse cr) { + return cr.getBodyMono(); + } + + protected void doOnBodyError(Throwable ex, long elapsedMillis) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("failed to call {}", this.getApiName(), ex); + inputContext.getStepContext().addElapsedTime(this.getApiName() + " failed ", elapsedMillis); + } + + protected void doOnBodySuccess(Object resp, long elapsedMillis) { + + } + + @SuppressWarnings("unchecked") + protected void doResponseMapping(InputConfig aConfig, InputContext inputContext, Object responseBody) { +// GrpcInputConfig config = (GrpcInputConfig) aConfig; + response.put("body", responseBody); + + // 数据转换 + if (inputContext != null && inputContext.getStepContext() != null) { + StepContext stepContext = inputContext.getStepContext(); + Map dataMapping = this.getConfig().getDataMapping(); + if (dataMapping != null) { + Map responseMapping = (Map) dataMapping.get("response"); + if (responseMapping != null && !StringUtils.isEmpty(responseMapping)) { + ONode ctxNode = PathMapping.toONode(stepContext); + + // body + Map fixedBody = (Map) responseMapping.get("fixedBody"); + Map bodyMapping = (Map) responseMapping.get("body"); + Map scriptCfg = (Map) responseMapping.get("script"); + if ((fixedBody != null && !fixedBody.isEmpty()) || (bodyMapping != null && !bodyMapping.isEmpty()) + || (scriptCfg != null && scriptCfg.get("type") != null + && scriptCfg.get("source") != null)) { + // body + Map body = new HashMap<>(); + body.putAll(PathMapping.transform(ctxNode, stepContext, fixedBody, bodyMapping)); + if (body.containsKey(CommonConstants.WILDCARD_TILDE)) { + response.put("body", body.get(CommonConstants.WILDCARD_TILDE)); + } else { + // script + if (scriptCfg != null && scriptCfg.get("type") != null && scriptCfg.get("source") != null) { + try { + Object respBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); + if (respBody != null) { + body.putAll((Map) respBody); + } + } catch (ScriptException e) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), + e); + throw new ExecuteScriptException(e, stepContext, scriptCfg); + } + } + response.put("body", body); + } + } + } + } else { + response.put("body", responseBody); + } + } + } + + private String getApiName() { + return prefix + " - " + request.get("serviceName") + " - " + request.get("method"); + } + + public static Class inputConfigClass() { + return GrpcInputConfig.class; + } +} diff --git a/src/main/java/we/fizz/input/extension/grpc/GrpcInputConfig.java b/src/main/java/we/fizz/input/extension/grpc/GrpcInputConfig.java new file mode 100644 index 0000000..9586d6b --- /dev/null +++ b/src/main/java/we/fizz/input/extension/grpc/GrpcInputConfig.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input.extension.grpc; + +import we.fizz.exception.FizzRuntimeException; +import we.fizz.input.InputConfig; + +import java.util.Map; + +import org.springframework.util.StringUtils; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +public class GrpcInputConfig extends InputConfig { + + private int timeout; + private String serviceName; + private String method; + + public GrpcInputConfig(Map configMap) { + super(configMap); + } + + public void parse() { + String serviceName = (String) configMap.get("serviceName"); + if (StringUtils.isEmpty(serviceName)) { + throw new FizzRuntimeException("service name can not be blank"); + } + setServiceName(serviceName); + + String method = (String) configMap.get("method"); + if (StringUtils.isEmpty(method)) { + throw new FizzRuntimeException("method can not be blank"); + } + setMethod(method); + + if (configMap.get("timeout") != null) { + setTimeout(Integer.valueOf(configMap.get("timeout").toString())); + } + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } +} diff --git a/src/main/java/we/fizz/input/extension/mysql/MySQLInput.java b/src/main/java/we/fizz/input/extension/mysql/MySQLInput.java new file mode 100644 index 0000000..bb64ff8 --- /dev/null +++ b/src/main/java/we/fizz/input/extension/mysql/MySQLInput.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package we.fizz.input.extension.mysql; + +import we.fizz.input.IInput; +import we.fizz.input.Input; +import we.fizz.input.InputType; + + + + + +/** + * + * @author linwaiwai + * + */ + +public class MySQLInput extends Input implements IInput { + static public InputType TYPE = new InputType("MYSQL"); + public static Class inputConfigClass (){ + return MySQLInputConfig.class; + } + +} diff --git a/src/main/java/we/fizz/input/MySQLInputConfig.java b/src/main/java/we/fizz/input/extension/mysql/MySQLInputConfig.java similarity index 90% rename from src/main/java/we/fizz/input/MySQLInputConfig.java rename to src/main/java/we/fizz/input/extension/mysql/MySQLInputConfig.java index 2ba2319..129a925 100644 --- a/src/main/java/we/fizz/input/MySQLInputConfig.java +++ b/src/main/java/we/fizz/input/extension/mysql/MySQLInputConfig.java @@ -15,7 +15,9 @@ * along with this program. If not, see . */ -package we.fizz.input; +package we.fizz.input.extension.mysql; + +import we.fizz.input.InputConfig; import java.util.Map; @@ -27,7 +29,7 @@ import java.util.Map; public class MySQLInputConfig extends InputConfig { public MySQLInputConfig(Map configBody) { - // TODO Auto-generated constructor stub + super(configBody); } } diff --git a/src/main/java/we/fizz/input/extension/request/RequestInput.java b/src/main/java/we/fizz/input/extension/request/RequestInput.java new file mode 100644 index 0000000..35c9961 --- /dev/null +++ b/src/main/java/we/fizz/input/extension/request/RequestInput.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2020 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input.extension.request; + +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import javax.script.ScriptException; + +import org.noear.snack.ONode; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import com.alibaba.fastjson.JSON; + +import reactor.core.publisher.Mono; +import we.config.SystemConfig; +import we.constants.CommonConstants; +import we.exception.ExecuteScriptException; +import we.fizz.StepContext; +import we.fizz.StepResponse; +import we.fizz.input.*; +import we.flume.clients.log4j2appender.LogService; +import we.proxy.FizzWebClient; +import we.util.JacksonUtils; +import we.util.MapUtil; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +@SuppressWarnings("unchecked") +public class RequestInput extends RPCInput implements IInput{ + + private static final Logger LOGGER = LoggerFactory.getLogger(RequestInput.class); + static public InputType TYPE = new InputType("REQUEST"); + private InputType type; + protected Map dataMapping; + + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String CONTENT_TYPE_XML = "application/xml"; + private static final String CONTENT_TYPE_JS = "application/javascript"; + private static final String CONTENT_TYPE_HTML = "text/html"; + private static final String CONTENT_TYPE_TEXT = "text/plain"; + + private static final String CONTENT_TYPE = "content-type"; + + private String respContentType; + + public InputType getType() { + return type; + } + + public void setType(InputType typeEnum) { + this.type = typeEnum; + } + + public Map getDataMapping() { + return dataMapping; + } + + public void setDataMapping(Map dataMapping) { + this.dataMapping = dataMapping; + } + + protected void doRequestMapping(InputConfig aConfig, InputContext inputContext) { + RequestInputConfig config = (RequestInputConfig) aConfig; + + // 把请求信息放入stepContext + Map group = new HashMap<>(); + group.put("request", request); + group.put("response", response); + this.stepResponse.getRequests().put(name, group); + + HttpMethod method = HttpMethod.valueOf(config.getMethod().toUpperCase()); + request.put("method", method); + + Map params = new HashMap<>(); + params.putAll(MapUtil.toHashMap(config.getQueryParams())); + request.put("params", params); + + // 数据转换 + if (inputContext != null && inputContext.getStepContext() != null) { + StepContext stepContext = inputContext.getStepContext(); + Map dataMapping = this.getConfig().getDataMapping(); + if (dataMapping != null) { + Map requestMapping = (Map) dataMapping.get("request"); + if (requestMapping != null && !StringUtils.isEmpty(requestMapping)) { + ONode ctxNode = PathMapping.toONode(stepContext); + + // headers + Map headers = PathMapping.transform(ctxNode, stepContext, + MapUtil.upperCaseKey((Map) requestMapping.get("fixedHeaders")), + MapUtil.upperCaseKey((Map) requestMapping.get("headers")), false); + if (headers.containsKey(CommonConstants.WILDCARD_TILDE) + && headers.get(CommonConstants.WILDCARD_TILDE) instanceof Map) { + request.put("headers", headers.get(CommonConstants.WILDCARD_TILDE)); + } else { + request.put("headers", headers); + } + + // params + params.putAll(PathMapping.transform(ctxNode, stepContext, + (Map) requestMapping.get("fixedParams"), + (Map) requestMapping.get("params"), false)); + if (params.containsKey(CommonConstants.WILDCARD_TILDE) + && params.get(CommonConstants.WILDCARD_TILDE) instanceof Map) { + request.put("params", params.get(CommonConstants.WILDCARD_TILDE)); + } else { + request.put("params", params); + } + + // body + Map body = PathMapping.transform(ctxNode, stepContext, + (Map) requestMapping.get("fixedBody"), + (Map) requestMapping.get("body")); + if (body.containsKey(CommonConstants.WILDCARD_TILDE)) { + request.put("body", body.get(CommonConstants.WILDCARD_TILDE)); + } else { + // script + if (requestMapping.get("script") != null) { + Map scriptCfg = (Map) requestMapping.get("script"); + try { + Object reqBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); + if (reqBody != null) { + body.putAll((Map) reqBody); + } + } catch (ScriptException e) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); + throw new ExecuteScriptException(e, stepContext, scriptCfg); + } + } + request.put("body", body); + } + } + } + } + + UriComponents uriComponents = UriComponentsBuilder.fromUriString(config.getBaseUrl() + config.getPath()) + .queryParams(MapUtil.toMultiValueMap(params)).build(); + request.put("url", uriComponents.toUriString()); + } + + @Override + public void doResponseMapping(InputConfig aConfig, InputContext inputContext, Object responseBody) { + + RequestInputConfig config = (RequestInputConfig) aConfig; + response.put("body", this.parseBody(this.respContentType, (String)responseBody)); + + // 数据转换 + if (inputContext != null && inputContext.getStepContext() != null) { + StepContext stepContext = inputContext.getStepContext(); + Map dataMapping = this.getConfig().getDataMapping(); + if (dataMapping != null) { + Map responseMapping = (Map) dataMapping.get("response"); + if (responseMapping != null && !StringUtils.isEmpty(responseMapping)) { + ONode ctxNode = PathMapping.toONode(stepContext); + + // headers + Map fixedHeaders = MapUtil.upperCaseKey((Map) responseMapping.get("fixedHeaders")); + Map headerMapping = MapUtil.upperCaseKey((Map) responseMapping.get("headers")); + if ((fixedHeaders != null && !fixedHeaders.isEmpty()) + || (headerMapping != null && !headerMapping.isEmpty())) { + Map headers = new HashMap<>(); + headers.putAll(PathMapping.transform(ctxNode, stepContext, fixedHeaders, headerMapping, false)); + if (headers.containsKey(CommonConstants.WILDCARD_TILDE) + && headers.get(CommonConstants.WILDCARD_TILDE) instanceof Map) { + response.put("headers", headers.get(CommonConstants.WILDCARD_TILDE)); + } else { + response.put("headers", headers); + } + } + + // body + Map fixedBody = (Map) responseMapping.get("fixedBody"); + Map bodyMapping = (Map) responseMapping.get("body"); + Map scriptCfg = (Map) responseMapping.get("script"); + if ((fixedBody != null && !fixedBody.isEmpty()) || (bodyMapping != null && !bodyMapping.isEmpty()) + || (scriptCfg != null && scriptCfg.get("type") != null + && scriptCfg.get("source") != null)) { + // body + Map body = new HashMap<>(); + body.putAll(PathMapping.transform(ctxNode, stepContext, fixedBody, bodyMapping)); + if (body.containsKey(CommonConstants.WILDCARD_TILDE)) { + response.put("body", body.get(CommonConstants.WILDCARD_TILDE)); + } else { + // script + if (scriptCfg != null && scriptCfg.get("type") != null && scriptCfg.get("source") != null) { + try { + Object respBody = ScriptHelper.execute(scriptCfg, ctxNode, stepContext); + if (respBody != null) { + body.putAll((Map) respBody); + } + } catch (ScriptException e) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("execute script failed, {}", JacksonUtils.writeValueAsString(scriptCfg), e); + throw new ExecuteScriptException(e, stepContext, scriptCfg); + } + } + response.put("body", body); + } + } + } + } else { + response.put("body", responseBody); + } + } + } + + @Override + protected Mono getClientSpecFromContext(InputConfig aConfig, InputContext inputContext) { + RequestInputConfig config = (RequestInputConfig) aConfig; + + int timeout = config.getTimeout() < 1 ? 3000 : config.getTimeout() > 10000 ? 10000 : config.getTimeout(); + + HttpMethod method = HttpMethod.valueOf(config.getMethod()); + String url = (String) request.get("url"); + String body = JSON.toJSONString(request.get("body")); + + Map hds = (Map) request.get("headers"); + if (hds == null) { + hds = new HashMap<>(); + } + HttpHeaders headers = MapUtil.toHttpHeaders(hds); + + if (!headers.containsKey(CommonConstants.HEADER_CONTENT_TYPE)) { + // default content-type + headers.add(CommonConstants.HEADER_CONTENT_TYPE, CommonConstants.CONTENT_TYPE_JSON); + } + + // add default headers + SystemConfig systemConfig = this.getCurrentApplicationContext().getBean(SystemConfig.class); + for (String hdr : systemConfig.proxySetHeaders) { + if(inputContext.getStepContext().getInputReqHeader(hdr) != null) { + headers.addIfAbsent(hdr, (String) inputContext.getStepContext().getInputReqHeader(hdr)); + } + } + + headers.remove(CommonConstants.HEADER_CONTENT_LENGTH); + headers.add(CommonConstants.HEADER_TRACE_ID, inputContext.getStepContext().getTraceId()); + + HttpMethod aggrMethod = HttpMethod.valueOf(inputContext.getStepContext().getInputReqAttr("method").toString()); + String aggrPath = (String)inputContext.getStepContext().getInputReqAttr("path"); + String aggrService = aggrPath.split("\\/")[2]; + +// FizzWebClient client = FizzAppContext.appContext.getBean(FizzWebClient.class); + FizzWebClient client = this.getCurrentApplicationContext().getBean(FizzWebClient.class); + Mono clientResponse = client.aggrSend(aggrService, aggrMethod, aggrPath, null, method, url, + headers, body, (long)timeout); + return clientResponse.flatMap(cr->{ + RequestRPCResponse response = new RequestRPCResponse(); + response.setHeaders(cr.headers().asHttpHeaders()); + response.setBodyMono(cr.bodyToMono(String.class)); + response.setStatus(cr.statusCode()); + return Mono.just(response); + }); + + + } + + private Map getResponses(Map stepContext2) { + // TODO Auto-generated method stub + return null; + } + + protected void doOnResponseSuccess(RPCResponse cr, long elapsedMillis) { + HttpHeaders httpHeaders = (HttpHeaders) cr.getHeaders(); + Map headers = new HashMap<>(); + httpHeaders.forEach((key, value) -> { + if (value.size() > 1) { + headers.put(key.toUpperCase(), value); + } else { + headers.put(key.toUpperCase(), httpHeaders.getFirst(key)); + } + }); + headers.put("ELAPSEDTIME", elapsedMillis + "ms"); + this.response.put("headers", headers); + this.respContentType = httpHeaders.getFirst(CONTENT_TYPE); + inputContext.getStepContext().addElapsedTime(prefix + request.get("url"), + elapsedMillis); + } + protected Mono bodyToMono(ClientResponse cr){ + return cr.bodyToMono(String.class); + } + + protected void doOnBodyError(Throwable ex, long elapsedMillis) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.warn("failed to call {}", request.get("url"), ex); + inputContext.getStepContext().addElapsedTime( + stepResponse.getStepName() + "-" + "调用接口 failed " + request.get("url"), elapsedMillis); + } + + // Parse response body according to content-type header + public Object parseBody(String contentType, String responseBody) { + String[] cts = contentType.split(";"); + Object body = null; + for (int i = 0; i < cts.length; i++) { + String ct = cts[i].toLowerCase(); + switch (ct) { + case CONTENT_TYPE_JSON: + body = JSON.parse(responseBody); + break; + case CONTENT_TYPE_TEXT: + // parse text as json if start with "{" and end with "}" or start with "[" and + // end with "]" + if ((responseBody.startsWith("{") && responseBody.endsWith("}")) + || (responseBody.startsWith("[") && responseBody.endsWith("]"))) { + try { + body = JSON.parse(responseBody); + } catch (Exception e) { + body = responseBody; + } + } else { + body = responseBody; + } + break; + case CONTENT_TYPE_XML: + body = responseBody; + break; + case CONTENT_TYPE_HTML: + body = responseBody; + break; + case CONTENT_TYPE_JS: + body = responseBody; + break; + } + if (body != null) { + break; + } + } + if (body == null) { + body = responseBody; + } + return body; + } + + protected void doOnBodySuccess(Object resp, long elapsedMillis) { + if(inputContext.getStepContext().isDebug()) { + LogService.setBizId(inputContext.getStepContext().getTraceId()); + LOGGER.info("{} 耗时:{}ms URL={}, reqHeader={} req={} resp={}", prefix, elapsedMillis, request.get("url"), + JSON.toJSONString(this.request.get("headers")), + JSON.toJSONString(this.request.get("body")), resp); + } + } + + private void cleanup(ClientResponse clientResponse) { + if (clientResponse != null) { + clientResponse.bodyToMono(Void.class).subscribe(); + } + } + + public static Class inputConfigClass (){ + return RequestInputConfig.class; + } + +} diff --git a/src/main/java/we/fizz/input/RequestInputConfig.java b/src/main/java/we/fizz/input/extension/request/RequestInputConfig.java similarity index 80% rename from src/main/java/we/fizz/input/RequestInputConfig.java rename to src/main/java/we/fizz/input/extension/request/RequestInputConfig.java index a473599..d7f723a 100644 --- a/src/main/java/we/fizz/input/RequestInputConfig.java +++ b/src/main/java/we/fizz/input/extension/request/RequestInputConfig.java @@ -15,31 +15,32 @@ * along with this program. If not, see . */ -package we.fizz.input; +package we.fizz.input.extension.request; import java.net.MalformedURLException; import java.net.URL; -import java.util.HashMap; import java.util.Map; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; +import we.fizz.input.InputConfig; + + /** * * @author linwaiwai - * @author francis + * @author Francis Dong * */ -public class RequestInputConfig extends InputConfig{ +public class RequestInputConfig extends InputConfig { private URL url ; private String method ; private int timeout = 3; - private Map fallback = new HashMap(); - private Map condition; - + public RequestInputConfig(Map configBody) { + super(configBody); String url = (String) configBody.get("url"); if(StringUtils.isEmpty(url)) { throw new RuntimeException("Request URL can not be blank"); @@ -54,7 +55,8 @@ public class RequestInputConfig extends InputConfig{ timeout = Integer.valueOf(configBody.get("timeout").toString()); } if (configBody.get("fallback") != null) { - fallback = (Map)configBody.get("fallback"); + Map fallback = (Map)configBody.get("fallback"); + setFallback(fallback); } if (configBody.get("condition") != null) { setCondition((Map)configBody.get("condition")); @@ -110,20 +112,4 @@ public class RequestInputConfig extends InputConfig{ this.timeout = timeout; } - public Map getFallback() { - return fallback; - } - - public void setFallback(Map fallback) { - this.fallback = fallback; - } - - public Map getCondition() { - return condition; - } - - public void setCondition(Map condition) { - this.condition = condition; - } - } diff --git a/src/main/java/we/fizz/input/extension/request/RequestRPCResponse.java b/src/main/java/we/fizz/input/extension/request/RequestRPCResponse.java new file mode 100644 index 0000000..f762a20 --- /dev/null +++ b/src/main/java/we/fizz/input/extension/request/RequestRPCResponse.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.fizz.input.extension.request; + +import org.springframework.http.HttpStatus; +import we.fizz.input.RPCResponse; + +/** + * + * @author linwaiwai + * + */ +public class RequestRPCResponse extends RPCResponse { + private HttpStatus statusCode; + public void setStatus(HttpStatus statusCode) { + this.statusCode = statusCode; + } + + public HttpStatus getStatusCode() { + return statusCode; + } +} diff --git a/src/main/java/we/plugin/auth/ApiConfigService.java b/src/main/java/we/plugin/auth/ApiConfigService.java index b902963..0ef41b8 100644 --- a/src/main/java/we/plugin/auth/ApiConfigService.java +++ b/src/main/java/we/plugin/auth/ApiConfigService.java @@ -32,6 +32,7 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import we.config.AggregateRedisConfig; +import we.config.SystemConfig; import we.flume.clients.log4j2appender.LogService; import we.util.*; @@ -49,10 +50,6 @@ public class ApiConfigService { private static final Logger log = LoggerFactory.getLogger(ApiConfigService.class); - private static final String signHeader = "fizz-sign"; - - private static final String timestampHeader = "fizz-ts"; - @NacosValue(value = "${fizz-api-config.key:fizz_api_config_route}", autoRefreshed = true) @Value("${fizz-api-config.key:fizz_api_config_route}") private String fizzApiConfig; @@ -81,6 +78,9 @@ public class ApiConfigService { @Resource private GatewayGroupService gatewayGroupService; + @Resource + private SystemConfig systemConfig; + @Autowired(required = false) private CustomAuth customAuth; @@ -175,6 +175,9 @@ public class ApiConfigService { log.info("no " + ac.service + " config to delete"); } else { sc.remove(ac); + if (sc.path2methodToApiConfigMapMap.isEmpty()) { + serviceConfigMap.remove(ac.service); + } apiConifg2appsService.remove(ac.id); } } else { @@ -247,6 +250,8 @@ public class ApiConfigService { if (ac.checkApp) { if (apiConifg2appsService.contains(ac.id, app)) { return ac; + } else if (log.isDebugEnabled()) { + log.debug(ac + " not contains app " + app); } } else { return ac; @@ -261,12 +266,11 @@ public class ApiConfigService { ServerHttpRequest req = exchange.getRequest(); HttpHeaders hdrs = req.getHeaders(); LogService.setBizId(req.getId()); - return canAccess(exchange, WebUtils.getAppId(exchange), WebUtils.getOriginIp(exchange), hdrs.getFirst(timestampHeader), hdrs.getFirst(signHeader), + return canAccess(exchange, WebUtils.getAppId(exchange), WebUtils.getOriginIp(exchange), getTimestamp(hdrs), getSign(hdrs), WebUtils.getClientService(exchange), req.getMethod(), WebUtils.getClientReqPath(exchange)); } - private Mono canAccess(ServerWebExchange exchange, String app, String ip, String timestamp, String sign, - String service, HttpMethod method, String path) { + private Mono canAccess(ServerWebExchange exchange, String app, String ip, String timestamp, String sign, String service, HttpMethod method, String path) { ServiceConfig sc = serviceConfigMap.get(service); if (sc == null) { @@ -326,7 +330,7 @@ public class ApiConfigService { } } - private static Mono authSign(ApiConfig ac, App a, String timestamp, String sign) { + private Mono authSign(ApiConfig ac, App a, String timestamp, String sign) { if (StringUtils.isAnyBlank(timestamp, sign)) { return logAndResult(a.app + " lack timestamp " + timestamp + " or sign " + sign, Access.NO_TIMESTAMP_OR_SIGN); } else if (validate(a.app, timestamp, a.secretkey, sign)) { @@ -336,13 +340,13 @@ public class ApiConfigService { } } - private static boolean validate(String app, String timestamp, String secretKey, String sign) { + private boolean validate(String app, String timestamp, String secretKey, String sign) { StringBuilder b = ThreadContext.getStringBuilder(); b.append(app).append(Constants.Symbol.UNDERLINE).append(timestamp).append(Constants.Symbol.UNDERLINE).append(secretKey); return sign.equalsIgnoreCase(DigestUtils.md532(b.toString())); } - private static Mono authSecretkey(ApiConfig ac, App a, String sign) { + private Mono authSecretkey(ApiConfig ac, App a, String sign) { if (StringUtils.isBlank(sign)) { return logAndResult(a.app + " lack secretkey " + sign, Access.NO_SECRETKEY); } else if (a.secretkey.equals(sign)) { @@ -352,7 +356,7 @@ public class ApiConfigService { } } - private static Mono allow(String api, ApiConfig ac) { + private Mono allow(String api, ApiConfig ac) { if (ac.access == ApiConfig.ALLOW) { return Mono.just(ac); } else { @@ -360,8 +364,30 @@ public class ApiConfigService { } } - private static Mono logAndResult(String msg, Access access) { + private Mono logAndResult(String msg, Access access) { log.warn(msg); return Mono.just(access); } + + private String getTimestamp(HttpHeaders reqHdrs) { + List tsHdrs = systemConfig.timestampHeaders; + for (int i = 0; i < tsHdrs.size(); i++) { + String a = reqHdrs.getFirst(tsHdrs.get(i)); + if (a != null) { + return a; + } + } + return null; + } + + private String getSign(HttpHeaders reqHdrs) { + List signHdrs = systemConfig.signHeaders; + for (int i = 0; i < signHdrs.size(); i++) { + String a = reqHdrs.getFirst(signHdrs.get(i)); + if (a != null) { + return a; + } + } + return null; + } } diff --git a/src/main/java/we/plugin/auth/GatewayGroup2apiConfig.java b/src/main/java/we/plugin/auth/GatewayGroup2apiConfig.java index 4fc2890..cfa9ca8 100644 --- a/src/main/java/we/plugin/auth/GatewayGroup2apiConfig.java +++ b/src/main/java/we/plugin/auth/GatewayGroup2apiConfig.java @@ -79,6 +79,9 @@ public class GatewayGroup2apiConfig { Set acs = configMap.get(gg); if (acs != null) { acs.remove(ac); + if (acs.isEmpty()) { + configMap.remove(gg); + } } } } diff --git a/src/main/java/we/plugin/auth/ServiceConfig.java b/src/main/java/we/plugin/auth/ServiceConfig.java index ba2c98a..bfc7a86 100644 --- a/src/main/java/we/plugin/auth/ServiceConfig.java +++ b/src/main/java/we/plugin/auth/ServiceConfig.java @@ -85,6 +85,12 @@ public class ServiceConfig { } else { log.info(id + " remove " + ac); gatewayGroup2apiConfig.remove(ac); + if (gatewayGroup2apiConfig.getConfigMap().isEmpty()) { + method2apiConfigMap.remove(ac.method); + if (method2apiConfigMap.isEmpty()) { + path2methodToApiConfigMapMap.remove(ac.path); + } + } } } } diff --git a/src/main/java/we/proxy/CallbackService.java b/src/main/java/we/proxy/CallbackService.java index 45ca443..c6034fe 100644 --- a/src/main/java/we/proxy/CallbackService.java +++ b/src/main/java/we/proxy/CallbackService.java @@ -17,7 +17,6 @@ package we.proxy; -import com.alibaba.fastjson.JSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.buffer.DataBuffer; @@ -31,6 +30,7 @@ import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import we.config.SystemConfig; import we.constants.CommonConstants; import we.fizz.AggregateResult; import we.fizz.AggregateService; @@ -41,6 +41,7 @@ import we.plugin.auth.CallbackConfig; import we.plugin.auth.Receiver; import we.util.*; +import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; @@ -59,7 +60,7 @@ public class CallbackService { private static final String callback = "callback"; @Resource - private FizzWebClient fizzWebClient; + private FizzWebClient fizzWebClient; @Resource private AggregateService aggregateService; @@ -67,6 +68,16 @@ public class CallbackService { @Resource private ApiConfigService apiConfigService; + @Resource + private SystemConfig systemConfig; + + private String aggrConfigPrefix; + + @PostConstruct + public void postConstruct() { + aggrConfigPrefix = systemConfig.gatewayPrefix + '/'; + } + public Mono requestBackends(ServerWebExchange exchange, HttpHeaders headers, DataBuffer body, CallbackConfig cc, Map service2instMap) { ServerHttpRequest req = exchange.getRequest(); String reqId = req.getId(); @@ -197,9 +208,6 @@ public class CallbackService { return Mono.just(ReactiveResult.fail("no api config for " + req.path)); } CallbackConfig cc = ac.callbackConfig; - // if (req.headers.getContentType().getSubtype().equalsIgnoreCase("json")) { - // req.body = JSON.parseObject(req.body, String.class); - // } List> sends = new ArrayList<>(); Mono send; @@ -218,7 +226,7 @@ public class CallbackService { } } else { String traceId = CommonConstants.TRACE_ID_PREFIX + req.id; - send = aggregateService.request(traceId, "/proxy/", req.method.name(), r.service, r.path, null, req.headers, req.body) + send = aggregateService.request(traceId, aggrConfigPrefix, req.method.name(), r.service, r.path, null, req.headers, req.body) .onErrorResume( arError(req, r.service, r.path) ); sends.add(send); } @@ -232,7 +240,7 @@ public class CallbackService { .onErrorResume( crError(req, stp.service, stp.path) ); } else { String traceId = CommonConstants.TRACE_ID_PREFIX + req.id; - send = aggregateService.request(traceId, "/proxy/", req.method.name(), stp.service, stp.path, null, req.headers, req.body) + send = aggregateService.request(traceId, aggrConfigPrefix, req.method.name(), stp.service, stp.path, null, req.headers, req.body) .onErrorResume( arError(req, stp.service, stp.path) ); } sends.add(send); diff --git a/src/main/java/we/proxy/FizzWebClient.java b/src/main/java/we/proxy/FizzWebClient.java index 386ec77..69df193 100644 --- a/src/main/java/we/proxy/FizzWebClient.java +++ b/src/main/java/we/proxy/FizzWebClient.java @@ -18,6 +18,7 @@ package we.proxy; import com.alibaba.nacos.api.config.annotation.NacosValue; +import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +42,8 @@ import we.util.WebUtils; import javax.annotation.PostConstruct; import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ThreadLocalRandom; /** @@ -56,6 +59,8 @@ public class FizzWebClient { private static final String localhost = "localhost"; + private static final String host = "HOST"; + @Resource private DiscoveryClientUriSelector discoveryClientUriSelector; @@ -184,6 +189,7 @@ public class FizzWebClient { } ); } + setHostHeader(uri, hdrs); } ); @@ -220,6 +226,31 @@ public class FizzWebClient { // TODO 请求完成后,做metric, 以反哺后续的请求转发 } + private void setHostHeader(String uri, HttpHeaders headers) { + boolean domain = false; + int begin = uri.indexOf(Constants.Symbol.FORWARD_SLASH) + 2; + int end = uri.indexOf(Constants.Symbol.FORWARD_SLASH, begin); + for (int i = begin; i < end; i++) { + char c = uri.charAt(i); + if ( (47 < c && c < 58) || c == Constants.Symbol.DOT || c == Constants.Symbol.COLON ) { + } else { + domain = true; + break; + } + } + if (domain) { + List lst = new ArrayList<>(1); + lst.add(uri.substring(begin, end)); + headers.put(host, lst); + } + + // int begin = uri.indexOf(Constants.Symbol.FORWARD_SLASH) + 2; + // int end = uri.indexOf(Constants.Symbol.FORWARD_SLASH, begin); + // List lst = new ArrayList<>(1); + // lst.add(uri.substring(begin, end)); + // headers.put(host, lst); + } + public String extractServiceOrAddress(String uriOrSvc) { char c4 = uriOrSvc.charAt(4); int start = 7, end = uriOrSvc.length(); diff --git a/src/main/java/we/proxy/dubbo/ApacheDubboGenericService.java b/src/main/java/we/proxy/dubbo/ApacheDubboGenericService.java new file mode 100644 index 0000000..eca1270 --- /dev/null +++ b/src/main/java/we/proxy/dubbo/ApacheDubboGenericService.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.proxy.dubbo; + +import com.alibaba.nacos.api.config.annotation.NacosValue; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.config.RegistryConfig; +import org.apache.dubbo.config.utils.ReferenceConfigCache; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.service.GenericException; +import org.apache.dubbo.rpc.service.GenericService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import reactor.core.publisher.Mono; +import we.fizz.exception.FizzException; + +import javax.annotation.PostConstruct; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +@Service +public class ApacheDubboGenericService { + + @NacosValue(value = "${fizz-dubbo-client.address}") + @Value("${fizz-dubbo-client.address}") + private String zookeeperAddress = ""; + + @PostConstruct + public void afterPropertiesSet() { + + } + + public ReferenceConfig createReferenceConfig(String serviceName, String version, String group) { + ApplicationConfig applicationConfig = new ApplicationConfig(); + applicationConfig.setName("fizz_proxy"); + RegistryConfig registryConfig = new RegistryConfig(); + registryConfig.setAddress(zookeeperAddress); + ReferenceConfig referenceConfig = new ReferenceConfig<>(); + referenceConfig.setInterface(serviceName); + applicationConfig.setRegistry(registryConfig); + referenceConfig.setApplication(applicationConfig); + referenceConfig.setGeneric(true); + referenceConfig.setAsync(true); + referenceConfig.setTimeout(30000); + referenceConfig.setVersion(version); + referenceConfig.setGroup(group); + applicationConfig.setQosEnable(false); + return referenceConfig; + } + + /** + * Generic invoke. + * + * @param body the json string body + * @param interfaceDeclaration the interface declaration + * @return the object + * @throws FizzException the fizz exception + */ + @SuppressWarnings("unchecked") + public Mono send(final Map body, final DubboInterfaceDeclaration interfaceDeclaration, + HashMap attachments) { + + RpcContext.getContext().setAttachments(attachments); + ReferenceConfig reference = createReferenceConfig(interfaceDeclaration.getServiceName(), + interfaceDeclaration.getVersion(), interfaceDeclaration.getGroup()); + + ReferenceConfigCache cache = ReferenceConfigCache.getCache(); + GenericService genericService = cache.get(reference); + + Pair pair; + if (CollectionUtils.isEmpty(body)) { + pair = new ImmutablePair(new String[] {}, new Object[] {}); + } else { + pair = DubboUtils.parseDubboParam(body, interfaceDeclaration.getParameterTypes()); + } + + CompletableFuture future = null; + Object object = genericService.$invoke(interfaceDeclaration.getMethod(), pair.getLeft(), pair.getRight()); + if (object == null) { + future = RpcContext.getContext().getCompletableFuture(); + } else if (object instanceof CompletableFuture) { + future = (CompletableFuture) object; + } else { + future = CompletableFuture.completedFuture(object); + } + Mono result = Mono.fromFuture(future.thenApply(ret -> { + return ret; + })).onErrorMap(exception -> exception instanceof GenericException + ? new FizzException(((GenericException) exception).getExceptionMessage()) + : new FizzException(exception)); + + if (interfaceDeclaration.getTimeout() != null && interfaceDeclaration.getTimeout() > 0) { + return result.timeout(Duration.ofMillis(interfaceDeclaration.getTimeout().longValue())); + } + return result; + } + +} diff --git a/src/main/java/we/proxy/dubbo/DubboInterfaceDeclaration.java b/src/main/java/we/proxy/dubbo/DubboInterfaceDeclaration.java new file mode 100644 index 0000000..abaaf86 --- /dev/null +++ b/src/main/java/we/proxy/dubbo/DubboInterfaceDeclaration.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.proxy.dubbo; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +public class DubboInterfaceDeclaration { + private String parameterTypes; + private String method; + private String serviceName; + private String version; + private String group; + private int timeout; + + public DubboInterfaceDeclaration() { + } + + public String getParameterTypes() { + return parameterTypes; + } + + // call method name + public String getMethod() { + return method; + } + + // service name + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public void setMethod(String method) { + this.method = method; + } + + public void setParameterTypes(String parameterTypes) { + this.parameterTypes = parameterTypes; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public Integer getTimeout() { + return this.timeout; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + +} diff --git a/src/main/java/we/proxy/dubbo/DubboUtils.java b/src/main/java/we/proxy/dubbo/DubboUtils.java new file mode 100644 index 0000000..840b7dc --- /dev/null +++ b/src/main/java/we/proxy/dubbo/DubboUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 the original author or authors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package we.proxy.dubbo; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; + +/** + * + * @author linwaiwai + * @author Francis Dong + * + */ +public class DubboUtils { + + /* + * body json string + */ + public static Pair parseDubboParam(Map paramMap, final String parameterTypes) { + if (StringUtils.isBlank(parameterTypes)) { + return new ImmutablePair<>(null, null); + } + String[] parameter = StringUtils.split(parameterTypes, ','); + if (parameter.length == 1 && !isBaseType(parameter[0])) { + return new ImmutablePair<>(parameter, new Object[] { paramMap.get("p1") }); + } + List list = new LinkedList<>(); + for (int i = 0; i < parameter.length; i++) { + list.add(paramMap.get("p" + (i + 1))); + } + Object[] objects = list.toArray(); + return new ImmutablePair<>(parameter, objects); + } + + private static boolean isBaseType(String type) { + return type.startsWith("java") || type.startsWith("[Ljava"); + } +} diff --git a/src/main/java/we/proxy/grpc/GrpcGenericService.java b/src/main/java/we/proxy/grpc/GrpcGenericService.java new file mode 100644 index 0000000..6d50e2f --- /dev/null +++ b/src/main/java/we/proxy/grpc/GrpcGenericService.java @@ -0,0 +1,70 @@ +package we.proxy.grpc; + +import com.google.common.net.HostAndPort; +import com.google.common.util.concurrent.ListenableFuture; +import io.grpc.CallOptions; +import io.grpc.ManagedChannel; +import org.apache.dubbo.rpc.service.GenericException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import we.fizz.exception.FizzException; +import static io.grpc.CallOptions.DEFAULT; +import static java.util.Collections.singletonList; + +import we.proxy.grpc.client.CallResults; +import we.proxy.grpc.client.GrpcProxyClient; +import we.proxy.grpc.client.core.GrpcMethodDefinition; +import we.proxy.grpc.client.utils.ChannelFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static we.proxy.grpc.client.utils.GrpcReflectionUtils.parseToMethodDefinition; + +@Service +public class GrpcGenericService { + + @Autowired + private GrpcProxyClient grpcProxyClient; + + /** + * Generic invoke. + * + * @param payload the json string body + * @param grpcInterfaceDeclaration the interface declaration + * @return the mono object + * @throws FizzException the fizz runtime exception + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Mono send(final String payload, final GrpcInterfaceDeclaration grpcInterfaceDeclaration, + HashMap attachments) { + GrpcMethodDefinition methodDefinition = parseToMethodDefinition( + grpcInterfaceDeclaration.getServiceName() + "." + grpcInterfaceDeclaration.getMethod()); + HostAndPort endPoint = HostAndPort.fromString(grpcInterfaceDeclaration.getEndpoint()); + if (endPoint == null) { + throw new RuntimeException("can't find target endpoint"); + } + Map metaHeaderMap = attachments; + ManagedChannel channel = null; + try { + channel = ChannelFactory.create(endPoint, metaHeaderMap); + CallOptions calloptions = DEFAULT; + calloptions.withDeadlineAfter(grpcInterfaceDeclaration.getTimeout(), TimeUnit.MILLISECONDS); + + CallResults callResults = new CallResults(); + ListenableFuture future = grpcProxyClient.invokeMethodAsync(methodDefinition, channel, DEFAULT, + singletonList(payload), callResults); + return Mono.fromFuture(new ListenableFutureAdapter(future).getCompletableFuture().thenApply(ret -> { + return callResults.asJSON(); + })).onErrorMap(exception -> exception instanceof GenericException + ? new FizzException(((GenericException) exception).getExceptionMessage()) + : new FizzException((Throwable) exception)); + } finally { + if (channel != null) { + channel.shutdown(); + } + } + } +} diff --git a/src/main/java/we/proxy/grpc/GrpcInstanceService.java b/src/main/java/we/proxy/grpc/GrpcInstanceService.java new file mode 100644 index 0000000..0f83961 --- /dev/null +++ b/src/main/java/we/proxy/grpc/GrpcInstanceService.java @@ -0,0 +1,32 @@ +package we.proxy.grpc; + +import java.util.List; + +/** + * gRPC instance service interface + * + * @author zhongjie + */ +public interface GrpcInstanceService { + /** + * random get an instance + * + * @param service service name + * @return instance, {@code null} if instance not-exist + */ + String getInstanceRandom(String service); + /** + * round-robin get an instance + * + * @param service service name + * @return instance, {@code null} if instance not-exist + */ + String getInstanceRoundRobin(String service); + /** + * get all instances + * + * @param service service name + * @return instance, {@code null} if instance not-exist + */ + List getAllInstance(String service); +} diff --git a/src/main/java/we/proxy/grpc/GrpcInstanceServiceImpl.java b/src/main/java/we/proxy/grpc/GrpcInstanceServiceImpl.java new file mode 100644 index 0000000..f31530d --- /dev/null +++ b/src/main/java/we/proxy/grpc/GrpcInstanceServiceImpl.java @@ -0,0 +1,217 @@ +package we.proxy.grpc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.ReactiveStringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import we.config.AggregateRedisConfig; +import we.flume.clients.log4j2appender.LogService; +import we.util.Constants; +import we.util.JacksonUtils; +import we.util.ReactorUtils; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * gRPC instance service implementation, get all config from redis cache when init and listen on redis channel for change + * + * @author zhongjie + */ +@Service +public class GrpcInstanceServiceImpl implements GrpcInstanceService { + private static final Logger LOGGER = LoggerFactory.getLogger(GrpcInstanceServiceImpl.class); + /** + * redis rpc service change channel + */ + private static final String RPC_SERVICE_CHANNEL = "fizz_rpc_service_channel"; + /** + * redis rpc service info hash key + */ + private static final String RPC_SERVICE_HASH_KEY = "fizz_rpc_service"; + + private static Map> serviceToInstancesMap = new ConcurrentHashMap<>(32); + private static Map idToRpcServiceMap = new ConcurrentHashMap<>(32); + private static Map serviceToCountMap = new ConcurrentHashMap<>(32); + + @Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE) + private ReactiveStringRedisTemplate redisTemplate; + + @PostConstruct + public void init() throws Throwable { + final Throwable[] throwable = new Throwable[1]; + Throwable error = Mono.just(Objects.requireNonNull(redisTemplate.opsForHash().entries(RPC_SERVICE_HASH_KEY) + .defaultIfEmpty(new AbstractMap.SimpleEntry<>(ReactorUtils.OBJ, ReactorUtils.OBJ)).onErrorStop().doOnError(t -> LOGGER.info(null, t)) + .concatMap(e -> { + Object k = e.getKey(); + if (k == ReactorUtils.OBJ) { + return Flux.just(e); + } + Object v = e.getValue(); + LOGGER.info(k.toString() + Constants.Symbol.COLON + v.toString(), LogService.BIZ_ID, k.toString()); + String json = (String) v; + try { + RpcService rpcService = JacksonUtils.readValue(json, RpcService.class); + this.updateLocalCache(rpcService); + return Flux.just(e); + } catch (Throwable t) { + throwable[0] = t; + LOGGER.info(json, t); + return Flux.error(t); + } + }).blockLast())).flatMap( + e -> { + if (throwable[0] != null) { + return Mono.error(throwable[0]); + } + return lsnRpcServiceChange(); + } + ).block(); + if (error != ReactorUtils.EMPTY_THROWABLE) { + assert error != null; + throw error; + } + } + + @Override + public String getInstanceRandom(String service) { + List instanceList = serviceToInstancesMap.get(service); + if (CollectionUtils.isEmpty(instanceList)) { + return null; + } + return instanceList.get(ThreadLocalRandom.current().nextInt(instanceList.size())); + } + + @Override + public String getInstanceRoundRobin(String service) { + List instanceList = serviceToInstancesMap.get(service); + if (CollectionUtils.isEmpty(instanceList)) { + return null; + } + long currentCount = serviceToCountMap.computeIfAbsent(service, it -> new AtomicLong()).getAndIncrement(); + return instanceList.get((int)currentCount % instanceList.size()); + } + + @Override + public List getAllInstance(String service) { + return serviceToInstancesMap.get(service); + } + + private Mono lsnRpcServiceChange() { + final Throwable[] throwable = new Throwable[1]; + final boolean[] b = {false}; + redisTemplate.listenToChannel(RPC_SERVICE_CHANNEL).doOnError(t -> { + throwable[0] = t; + b[0] = false; + LOGGER.error("lsn " + RPC_SERVICE_CHANNEL, t); + }).doOnSubscribe( + s -> { + b[0] = true; + LOGGER.info("success to lsn on " + RPC_SERVICE_CHANNEL); + } + ).doOnNext(msg -> { + String json = msg.getMessage(); + LOGGER.info(json, LogService.BIZ_ID, "rpc" + System.currentTimeMillis()); + try { + RpcService rpcService = JacksonUtils.readValue(json, RpcService.class); + this.updateLocalCache(rpcService); + } catch (Throwable t) { + LOGGER.info(json, t); + } + }).subscribe(); + Throwable t = throwable[0]; + while (!b[0]) { + if (t != null) { + return Mono.error(t); + } else { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + return Mono.error(e); + } + } + } + return Mono.just(ReactorUtils.EMPTY_THROWABLE); + } + + private void updateLocalCache(RpcService rpcService) { + if (rpcService.getIsDeleted() == RpcService.DELETED) { + RpcService removedRpcService = idToRpcServiceMap.remove(rpcService.id); + LOGGER.info("remove {}", removedRpcService); + if (removedRpcService != null) { + serviceToInstancesMap.remove(removedRpcService.getService()); + serviceToCountMap.remove(removedRpcService.getService()); + } + } else { + RpcService existRpcService = idToRpcServiceMap.get(rpcService.id); + idToRpcServiceMap.put(rpcService.id, rpcService); + if (existRpcService == null) { + LOGGER.info("add {}", rpcService); + } else { + LOGGER.info("update {} with {}", existRpcService, rpcService); + serviceToInstancesMap.remove(existRpcService.getService()); + serviceToCountMap.remove(existRpcService.getService()); + } + serviceToInstancesMap.put(rpcService.service, rpcService.instance == null ? Collections.emptyList() : + Arrays.asList(rpcService.getInstance().split(","))); + } + } + + private static class RpcService { + private static final int DELETED = 1; + private Long id; + private Integer isDeleted; + private String service; + private String instance; + + @Override + public String toString() { + return JacksonUtils.writeValueAsString(this); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getIsDeleted() { + return isDeleted; + } + + public void setIsDeleted(Integer isDeleted) { + this.isDeleted = isDeleted; + } + + public String getService() { + return service; + } + + public void setService(String service) { + this.service = service; + } + + public String getInstance() { + return instance; + } + + public void setInstance(String instance) { + this.instance = instance; + } + } +} diff --git a/src/main/java/we/proxy/grpc/GrpcInterfaceDeclaration.java b/src/main/java/we/proxy/grpc/GrpcInterfaceDeclaration.java new file mode 100644 index 0000000..1bac694 --- /dev/null +++ b/src/main/java/we/proxy/grpc/GrpcInterfaceDeclaration.java @@ -0,0 +1,44 @@ +package we.proxy.grpc; + +public class GrpcInterfaceDeclaration { + private String method; + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + private String endpoint; + private String serviceName; + private int timeout; + + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getMethod() { + return method; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public int getTimeout() { + return timeout; + } + + +} diff --git a/src/main/java/we/proxy/grpc/ListenableFutureAdapter.java b/src/main/java/we/proxy/grpc/ListenableFutureAdapter.java new file mode 100644 index 0000000..0a6bdd0 --- /dev/null +++ b/src/main/java/we/proxy/grpc/ListenableFutureAdapter.java @@ -0,0 +1,47 @@ +package we.proxy.grpc; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.concurrent.CompletableFuture; + +public class ListenableFutureAdapter { + + private final ListenableFuture listenableFuture; + private final CompletableFuture completableFuture; + + public ListenableFutureAdapter(ListenableFuture listenableFuture) { + this.listenableFuture = listenableFuture; + this.completableFuture = new CompletableFuture() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean cancelled = listenableFuture.cancel(mayInterruptIfRunning); + super.cancel(cancelled); + return cancelled; + } + }; + + Futures.addCallback(this.listenableFuture, new FutureCallback() { + @Override + public void onSuccess(T result) { + completableFuture.complete(result); + } + + @Override + public void onFailure(Throwable ex) { + completableFuture.completeExceptionally(ex); + } + }); + } + + public CompletableFuture getCompletableFuture() { + return completableFuture; + } + + public static final CompletableFuture toCompletable(ListenableFuture listenableFuture) { + ListenableFutureAdapter listenableFutureAdapter = new ListenableFutureAdapter<>(listenableFuture); + return listenableFutureAdapter.getCompletableFuture(); + } + +} \ No newline at end of file diff --git a/src/main/java/we/proxy/grpc/client/CallParams.java b/src/main/java/we/proxy/grpc/client/CallParams.java new file mode 100644 index 0000000..0b4bcc8 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/CallParams.java @@ -0,0 +1,48 @@ +/* +MIT License + +Copyright (c) 2018 liuzhengyang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package we.proxy.grpc.client; + +import java.util.List; + +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.DynamicMessage; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.stub.StreamObserver; +import lombok.Builder; +import lombok.Getter; + +/** + * @author zhangjikai + */ +@Builder +@Getter +public class CallParams { + private MethodDescriptor methodDescriptor; + private Channel channel; + private CallOptions callOptions; + private List requests; + private StreamObserver responseObserver; +} diff --git a/src/main/java/we/proxy/grpc/client/CallResults.java b/src/main/java/we/proxy/grpc/client/CallResults.java new file mode 100644 index 0000000..e168939 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/CallResults.java @@ -0,0 +1,57 @@ +/* +MIT License + +Copyright (c) 2018 liuzhengyang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package we.proxy.grpc.client; + +import static java.util.stream.Collectors.toList; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.fastjson.JSON; + +/** + * @author zhangjikai + */ +public class CallResults { + private List results; + + public CallResults() { + this.results = new ArrayList<>(); + } + + public void add(String jsonText) { + results.add(jsonText); + } + + public List asList() { + return results; + } + + public Object asJSON() { + if (results.size() == 1) { + return JSON.parseObject(results.get(0)); + } + return results.stream().map(JSON::parseObject).collect(toList()); + } +} diff --git a/src/main/java/we/proxy/grpc/client/GrpcClient.java b/src/main/java/we/proxy/grpc/client/GrpcClient.java new file mode 100644 index 0000000..2b7aa7e --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/GrpcClient.java @@ -0,0 +1,117 @@ +/* +MIT License + +Copyright (c) 2018 liuzhengyang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package we.proxy.grpc.client; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.stub.ClientCalls.asyncBidiStreamingCall; +import static io.grpc.stub.ClientCalls.asyncClientStreamingCall; +import static io.grpc.stub.ClientCalls.asyncServerStreamingCall; +import static io.grpc.stub.ClientCalls.asyncUnaryCall; +//import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; +import static we.proxy.grpc.client.utils.GrpcReflectionUtils.fetchFullMethodName; +import static we.proxy.grpc.client.utils.GrpcReflectionUtils.fetchMethodType; + +import java.util.List; + +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.DynamicMessage; + +import io.grpc.ClientCall; +import io.grpc.MethodDescriptor.MethodType; + +import io.grpc.stub.StreamObserver; +import we.proxy.grpc.client.core.CompositeStreamObserver; +import we.proxy.grpc.client.core.DoneObserver; +import we.proxy.grpc.client.core.DynamicMessageMarshaller; + +/** + * @author zhangjikai + */ +public class GrpcClient { + + private static final Logger logger = LoggerFactory.getLogger(GrpcClient.class); + + @Nullable + public ListenableFuture call(CallParams callParams) { + checkParams(callParams); + MethodType methodType = fetchMethodType(callParams.getMethodDescriptor()); + List requests = callParams.getRequests(); + StreamObserver responseObserver = callParams.getResponseObserver(); + DoneObserver doneObserver = new DoneObserver<>(); + StreamObserver compositeObserver = CompositeStreamObserver.of(responseObserver, doneObserver); + StreamObserver requestObserver; + switch (methodType) { + case UNARY: + asyncUnaryCall(createCall(callParams), requests.get(0), compositeObserver); + return doneObserver.getCompletionFuture(); + case SERVER_STREAMING: + asyncServerStreamingCall(createCall(callParams), requests.get(0), compositeObserver); + return doneObserver.getCompletionFuture(); + case CLIENT_STREAMING: + requestObserver = asyncClientStreamingCall(createCall(callParams), compositeObserver); + requests.forEach(responseObserver::onNext); + requestObserver.onCompleted(); + return doneObserver.getCompletionFuture(); + case BIDI_STREAMING: + requestObserver = asyncBidiStreamingCall(createCall(callParams), compositeObserver); + requests.forEach(responseObserver::onNext); + requestObserver.onCompleted(); + return doneObserver.getCompletionFuture(); + default: + logger.info("Unknown methodType:{}", methodType); + return null; + } + } + + private void checkParams(CallParams callParams) { + checkNotNull(callParams); + checkNotNull(callParams.getMethodDescriptor()); + checkNotNull(callParams.getChannel()); + checkNotNull(callParams.getCallOptions()); + checkArgument(isNotEmpty(callParams.getRequests())); + checkNotNull(callParams.getResponseObserver()); + } + + private ClientCall createCall(CallParams callParams) { + return callParams.getChannel().newCall(createGrpcMethodDescriptor(callParams.getMethodDescriptor()), + callParams.getCallOptions()); + } + + private io.grpc.MethodDescriptor createGrpcMethodDescriptor(MethodDescriptor descriptor) { + return io.grpc.MethodDescriptor.newBuilder() + .setType(fetchMethodType(descriptor)) + .setFullMethodName(fetchFullMethodName(descriptor)) + .setRequestMarshaller(new DynamicMessageMarshaller(descriptor.getInputType())) + .setResponseMarshaller(new DynamicMessageMarshaller(descriptor.getOutputType())) + .build(); + } +} diff --git a/src/main/java/we/proxy/grpc/client/GrpcProxyClient.java b/src/main/java/we/proxy/grpc/client/GrpcProxyClient.java new file mode 100644 index 0000000..3b39a1c --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/GrpcProxyClient.java @@ -0,0 +1,88 @@ +/* +MIT License + +Copyright (c) 2018 liuzhengyang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package we.proxy.grpc.client; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.util.JsonFormat.TypeRegistry; + +import io.grpc.CallOptions; +import io.grpc.Channel; + +import io.grpc.stub.StreamObserver; +import org.springframework.stereotype.Service; +import we.proxy.grpc.client.core.GrpcMethodDefinition; +import we.proxy.grpc.client.core.ServiceResolver; +import we.proxy.grpc.client.utils.GrpcReflectionUtils; +import we.proxy.grpc.client.utils.MessageWriter; +@Service +/** + * @author zhangjikai + * Created on 2018-12-01 + */ +public class GrpcProxyClient { + + private GrpcClient grpcClient = new GrpcClient(); + + public CallResults invokeMethod(GrpcMethodDefinition definition, Channel channel, CallOptions callOptions, + List requestJsonTexts) { + CallResults results = new CallResults(); + try { + this.invokeMethodAsync( definition, channel, callOptions, requestJsonTexts, results).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Caught exception while waiting for rpc", e); + } + return results; + } + + public ListenableFuture invokeMethodAsync(GrpcMethodDefinition definition, Channel channel, CallOptions callOptions, + List requestJsonTexts, CallResults results) { + FileDescriptorSet fileDescriptorSet = GrpcReflectionUtils.resolveService(channel, definition.getFullServiceName()); + if (fileDescriptorSet == null) { + return null; + } + ServiceResolver serviceResolver = ServiceResolver.fromFileDescriptorSet(fileDescriptorSet); + MethodDescriptor methodDescriptor = serviceResolver.resolveServiceMethod(definition); + TypeRegistry registry = TypeRegistry.newBuilder().add(serviceResolver.listMessageTypes()).build(); + List requestMessages = GrpcReflectionUtils.parseToMessages(registry, methodDescriptor.getInputType(), + requestJsonTexts); +// CallResults results = new CallResults(); + StreamObserver streamObserver = MessageWriter.newInstance(registry, results); + CallParams callParams = CallParams.builder() + .methodDescriptor(methodDescriptor) + .channel(channel) + .callOptions(callOptions) + .requests(requestMessages) + .responseObserver(streamObserver) + .build(); + + return grpcClient.call(callParams); + + } +} diff --git a/src/main/java/we/proxy/grpc/client/core/CompositeStreamObserver.java b/src/main/java/we/proxy/grpc/client/core/CompositeStreamObserver.java new file mode 100644 index 0000000..8b5cb47 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/core/CompositeStreamObserver.java @@ -0,0 +1,87 @@ +/* +Copyright (c) 2016, gRPC Ecosystem +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of polyglot nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package we.proxy.grpc.client.core; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; + +import io.grpc.stub.StreamObserver; + +/** + * A {@link StreamObserver} which groups multiple observers and executes them all. + */ +public class CompositeStreamObserver implements StreamObserver { + private static final Logger logger = LoggerFactory.getLogger(CompositeStreamObserver.class); + private final ImmutableList> observers; + + @SafeVarargs + public static CompositeStreamObserver of(StreamObserver... observers) { + return new CompositeStreamObserver<>(ImmutableList.copyOf(observers)); + } + + private CompositeStreamObserver(ImmutableList> observers) { + this.observers = observers; + } + + @Override + public void onCompleted() { + for (StreamObserver observer : observers) { + try { + observer.onCompleted(); + } catch (Throwable t) { + logger.error("Exception in composite onComplete, moving on", t); + } + } + } + + @Override + public void onError(Throwable t) { + for (StreamObserver observer : observers) { + try { + observer.onError(t); + } catch (Throwable s) { + logger.error("Exception in composite onError, moving on", s); + } + } + } + + @Override + public void onNext(T value) { + for (StreamObserver observer : observers) { + try { + observer.onNext(value); + } catch (Throwable t) { + logger.error("Exception in composite onNext, moving on", t); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/we/proxy/grpc/client/core/DoneObserver.java b/src/main/java/we/proxy/grpc/client/core/DoneObserver.java new file mode 100644 index 0000000..2c1f066 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/core/DoneObserver.java @@ -0,0 +1,69 @@ +/* +Copyright (c) 2016, gRPC Ecosystem +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of polyglot nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package we.proxy.grpc.client.core; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import io.grpc.stub.StreamObserver; + +/** + * A {@link StreamObserver} holding a future which completes when the rpc terminates. + */ +public class DoneObserver implements StreamObserver { + private final SettableFuture doneFuture; + + public DoneObserver() { + this.doneFuture = SettableFuture.create(); + } + + @Override + public synchronized void onCompleted() { + doneFuture.set(null); + } + + @Override + public synchronized void onError(Throwable t) { + doneFuture.setException(t); + } + + @Override + public void onNext(T next) { + // Do nothing. + } + + /** + * Returns a future which completes when the rpc finishes. The returned future fails if the rpc + * fails. + */ + public ListenableFuture getCompletionFuture() { + return doneFuture; + } +} \ No newline at end of file diff --git a/src/main/java/we/proxy/grpc/client/core/DynamicMessageMarshaller.java b/src/main/java/we/proxy/grpc/client/core/DynamicMessageMarshaller.java new file mode 100644 index 0000000..fec1d90 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/core/DynamicMessageMarshaller.java @@ -0,0 +1,66 @@ +/* +Copyright (c) 2016, gRPC Ecosystem +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of polyglot nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package we.proxy.grpc.client.core; + +import java.io.IOException; +import java.io.InputStream; + +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.ExtensionRegistryLite; + +import io.grpc.MethodDescriptor.Marshaller; + +/** + * A {@link Marshaller} for dynamic messages. + */ +public class DynamicMessageMarshaller implements Marshaller { + private final Descriptor messageDescriptor; + + public DynamicMessageMarshaller(Descriptor messageDescriptor) { + this.messageDescriptor = messageDescriptor; + } + + @Override + public DynamicMessage parse(InputStream inputStream) { + try { + return DynamicMessage.newBuilder(messageDescriptor) + .mergeFrom(inputStream, ExtensionRegistryLite.getEmptyRegistry()) + .build(); + } catch (IOException e) { + throw new RuntimeException("Unable to merge from the supplied input stream", e); + } + } + + @Override + public InputStream stream(DynamicMessage abstractMessage) { + return abstractMessage.toByteString().newInput(); + } +} \ No newline at end of file diff --git a/src/main/java/we/proxy/grpc/client/core/GrpcMethodDefinition.java b/src/main/java/we/proxy/grpc/client/core/GrpcMethodDefinition.java new file mode 100644 index 0000000..bb62b63 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/core/GrpcMethodDefinition.java @@ -0,0 +1,60 @@ +/* +MIT License + +Copyright (c) 2018 liuzhengyang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package we.proxy.grpc.client.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + + + +/** + * @author zhangjikai + * Created on 2018-12-16 + */ + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GrpcMethodDefinition { + private String packageName; + private String serviceName; + private String methodName; + + public String getFullServiceName() { + if (isNotBlank(packageName)) { + return packageName + "." + serviceName; + } + return serviceName; + } + + public String getFullMethodName() { + if (isNotBlank(packageName)) { + return packageName + "." + serviceName + "/" + methodName; + } + return serviceName + "/" + methodName; + } +} diff --git a/src/main/java/we/proxy/grpc/client/core/ServerReflectionClient.java b/src/main/java/we/proxy/grpc/client/core/ServerReflectionClient.java new file mode 100644 index 0000000..74d700a --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/core/ServerReflectionClient.java @@ -0,0 +1,256 @@ +/* +Copyright (c) 2016, gRPC Ecosystem +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of polyglot nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package we.proxy.grpc.client.core; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.protobuf.ByteString; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.InvalidProtocolBufferException; + +import io.grpc.Channel; +import io.grpc.reflection.v1alpha.ListServiceResponse; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.reflection.v1alpha.ServerReflectionResponse.MessageResponseCase; +import io.grpc.stub.StreamObserver; + +public class ServerReflectionClient { + private static final Logger logger = LoggerFactory.getLogger(ServerReflectionClient.class); + private static final long LIST_RPC_DEADLINE_MS = 10_000; + private static final long LOOKUP_RPC_DEADLINE_MS = 10_000; + private static final ServerReflectionRequest LIST_SERVICES_REQUEST = + ServerReflectionRequest.newBuilder() + .setListServices("") // Not sure what this is for, appears to be ignored. + .build(); + + private final Channel channel; + + /** + * Returns a new reflection client using the supplied channel. + */ + public static ServerReflectionClient create(Channel channel) { + return new ServerReflectionClient(channel); + } + + private ServerReflectionClient(Channel channel) { + this.channel = channel; + } + + /** + * Asks the remote server to list its services and completes when the server responds. + */ + public ListenableFuture> listServices() { + ListServicesHandler rpcHandler = new ListServicesHandler(); + StreamObserver requestStream = ServerReflectionGrpc.newStub(channel) + .withDeadlineAfter(LIST_RPC_DEADLINE_MS, TimeUnit.MILLISECONDS) + .serverReflectionInfo(rpcHandler); + return rpcHandler.start(requestStream); + } + + /** + * Returns a {@link FileDescriptorSet} containing all the transitive dependencies of the supplied + * service, as provided by the remote server. + */ + public ListenableFuture lookupService(String serviceName) { + LookupServiceHandler rpcHandler = new LookupServiceHandler(serviceName); + StreamObserver requestStream = ServerReflectionGrpc.newStub(channel) + .withDeadlineAfter(LOOKUP_RPC_DEADLINE_MS, TimeUnit.MILLISECONDS) + .serverReflectionInfo(rpcHandler); + return rpcHandler.start(requestStream); + } + + /** + * Handles the rpc life cycle of a single list operation. + */ + private static class ListServicesHandler implements StreamObserver { + private final SettableFuture> resultFuture; + private StreamObserver requestStream; + + private ListServicesHandler() { + resultFuture = SettableFuture.create(); + } + + ListenableFuture> start( + StreamObserver requestStream) { + this.requestStream = requestStream; + requestStream.onNext(LIST_SERVICES_REQUEST); + return resultFuture; + } + + @Override + public void onNext(ServerReflectionResponse serverReflectionResponse) { + MessageResponseCase responseCase = serverReflectionResponse.getMessageResponseCase(); + switch (responseCase) { + case LIST_SERVICES_RESPONSE: + handleListServiceResponse(serverReflectionResponse.getListServicesResponse()); + break; + default: + logger.warn("Got unknown reflection response type: " + responseCase); + break; + } + } + + @Override + public void onError(Throwable t) { + resultFuture.setException(new RuntimeException("Error in server reflection rpc while listing services", t)); + } + + @Override + public void onCompleted() { + if (!resultFuture.isDone()) { + logger.error("Unexpected completion of server reflection rpc while listing services"); + resultFuture.setException(new RuntimeException("Unexpected end of rpc")); + } + } + + private void handleListServiceResponse(ListServiceResponse response) { + ImmutableList.Builder servicesBuilder = ImmutableList.builder(); + response.getServiceList().forEach(service -> servicesBuilder.add(service.getName())); + resultFuture.set(servicesBuilder.build()); + requestStream.onCompleted(); + } + } + + /** + * Handles the rpc life cycle of a single lookup operation. + */ + private static class LookupServiceHandler implements StreamObserver { + private final SettableFuture resultFuture; + private final String serviceName; + private final HashSet requestedDescriptors; + private final HashMap resolvedDescriptors; + private StreamObserver requestStream; + + // Used to notice when we've received all the files we've asked for and we can end the rpc. + private int outstandingRequests; + + private LookupServiceHandler(String serviceName) { + this.serviceName = serviceName; + this.resultFuture = SettableFuture.create(); + this.resolvedDescriptors = new HashMap<>(); + this.requestedDescriptors = new HashSet<>(); + this.outstandingRequests = 0; + } + + ListenableFuture start( + StreamObserver requestStream) { + this.requestStream = requestStream; + requestStream.onNext(requestForSymbol(serviceName)); + ++outstandingRequests; + return resultFuture; + } + + @Override + public void onNext(ServerReflectionResponse response) { + MessageResponseCase responseCase = response.getMessageResponseCase(); + switch (responseCase) { + case FILE_DESCRIPTOR_RESPONSE: + ImmutableSet descriptors = + parseDescriptors(response.getFileDescriptorResponse().getFileDescriptorProtoList()); + descriptors.forEach(d -> resolvedDescriptors.put(d.getName(), d)); + descriptors.forEach(this::processDependencies); + break; + default: + logger.warn("Got unknown reflection response type: " + responseCase); + break; + } + } + + @Override + public void onError(Throwable t) { + resultFuture.setException(new RuntimeException("Reflection lookup rpc failed for: " + serviceName, t)); + } + + @Override + public void onCompleted() { + if (!resultFuture.isDone()) { + logger.error("Unexpected completion of the server reflection rpc"); + resultFuture.setException(new RuntimeException("Unexpected end of rpc")); + } + } + + private ImmutableSet parseDescriptors(List descriptorBytes) { + ImmutableSet.Builder resultBuilder = ImmutableSet.builder(); + for (ByteString fileDescriptorBytes : descriptorBytes) { + try { + resultBuilder.add(FileDescriptorProto.parseFrom(fileDescriptorBytes)); + } catch (InvalidProtocolBufferException e) { + logger.warn("Failed to parse bytes as file descriptor proto"); + } + } + return resultBuilder.build(); + } + + private void processDependencies(FileDescriptorProto fileDescriptor) { + logger.debug("Processing deps of descriptor: " + fileDescriptor.getName()); + fileDescriptor.getDependencyList().forEach(dep -> { + if (!resolvedDescriptors.containsKey(dep) && !requestedDescriptors.contains(dep)) { + requestedDescriptors.add(dep); + ++outstandingRequests; + requestStream.onNext(requestForDescriptor(dep)); + } + }); + + --outstandingRequests; + if (outstandingRequests == 0) { + logger.debug("Retrieved service definition for [{}] by reflection", serviceName); + resultFuture.set(FileDescriptorSet.newBuilder() + .addAllFile(resolvedDescriptors.values()) + .build()); + requestStream.onCompleted(); + } + } + + private static ServerReflectionRequest requestForDescriptor(String name) { + return ServerReflectionRequest.newBuilder() + .setFileByFilename(name) + .build(); + } + + private static ServerReflectionRequest requestForSymbol(String symbol) { + return ServerReflectionRequest.newBuilder() + .setFileContainingSymbol(symbol) + .build(); + } + } +} diff --git a/src/main/java/we/proxy/grpc/client/core/ServiceResolver.java b/src/main/java/we/proxy/grpc/client/core/ServiceResolver.java new file mode 100644 index 0000000..94a7525 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/core/ServiceResolver.java @@ -0,0 +1,179 @@ +/* +Copyright (c) 2016, gRPC Ecosystem +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of polyglot nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package we.proxy.grpc.client.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.DescriptorValidationException; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; + + +/** + * A locator used to read proto file descriptors and extract method definitions. + */ +public class ServiceResolver { + private static final Logger logger = LoggerFactory.getLogger(ServiceResolver.class); + private final ImmutableList fileDescriptors; + + /** + * Creates a resolver which searches the supplied {@link FileDescriptorSet}. + */ + public static ServiceResolver fromFileDescriptorSet(FileDescriptorSet descriptorSet) { + ImmutableMap descriptorProtoIndex = + computeDescriptorProtoIndex(descriptorSet); + Map descriptorCache = new HashMap<>(); + + ImmutableList.Builder result = ImmutableList.builder(); + for (FileDescriptorProto descriptorProto : descriptorSet.getFileList()) { + try { + result.add(descriptorFromProto(descriptorProto, descriptorProtoIndex, descriptorCache)); + } catch (DescriptorValidationException e) { + logger.warn("Skipped descriptor " + descriptorProto.getName() + " due to error", e); + } + } + return new ServiceResolver(result.build()); + } + + private ServiceResolver(Iterable fileDescriptors) { + this.fileDescriptors = ImmutableList.copyOf(fileDescriptors); + } + + /** + * Lists all of the services found in the file descriptors + */ + public Iterable listServices() { + ArrayList serviceDescriptors = new ArrayList(); + for (FileDescriptor fileDescriptor : fileDescriptors) { + serviceDescriptors.addAll(fileDescriptor.getServices()); + } + return serviceDescriptors; + } + + /** + * Lists all the known message types. + */ + public ImmutableSet listMessageTypes() { + ImmutableSet.Builder resultBuilder = ImmutableSet.builder(); + fileDescriptors.forEach(d -> resultBuilder.addAll(d.getMessageTypes())); + return resultBuilder.build(); + } + + /** + * Returns the descriptor of a protobuf method with the supplied grpc method name. If the method + * cannot be found, this throws {@link IllegalArgumentException}. + */ + public MethodDescriptor resolveServiceMethod(GrpcMethodDefinition definition) { + + ServiceDescriptor service = findService(definition.getPackageName(), definition.getServiceName()); + MethodDescriptor method = service.findMethodByName(definition.getMethodName()); + if (method == null) { + throw new IllegalArgumentException( + "Unable to find method " + definition.getMethodName() + + " in service " + definition.getServiceName()); + } + return method; + } + + private ServiceDescriptor findService(String packageName, String serviceName) { + // TODO(dino): Consider creating an index. + for (FileDescriptor fileDescriptor : fileDescriptors) { + if (!fileDescriptor.getPackage().equals(packageName)) { + // Package does not match this file, ignore. + continue; + } + + ServiceDescriptor serviceDescriptor = fileDescriptor.findServiceByName(serviceName); + if (serviceDescriptor != null) { + return serviceDescriptor; + } + } + throw new IllegalArgumentException("Unable to find service with name: " + serviceName); + } + + /** + * Returns a map from descriptor proto name as found inside the descriptors to protos. + */ + private static ImmutableMap computeDescriptorProtoIndex( + FileDescriptorSet fileDescriptorSet) { + ImmutableMap.Builder resultBuilder = ImmutableMap.builder(); + for (FileDescriptorProto descriptorProto : fileDescriptorSet.getFileList()) { + resultBuilder.put(descriptorProto.getName(), descriptorProto); + } + return resultBuilder.build(); + } + + /** + * Recursively constructs file descriptors for all dependencies of the supplied proto and returns + * a {@link FileDescriptor} for the supplied proto itself. For maximal efficiency, reuse the + * descriptorCache argument across calls. + */ + private static FileDescriptor descriptorFromProto( + FileDescriptorProto descriptorProto, + ImmutableMap descriptorProtoIndex, + Map descriptorCache) throws DescriptorValidationException { + // First, check the cache. + String descriptorName = descriptorProto.getName(); + if (descriptorCache.containsKey(descriptorName)) { + return descriptorCache.get(descriptorName); + } + + // Then, fetch all the required dependencies recursively. + ImmutableList.Builder dependencies = ImmutableList.builder(); + for (String dependencyName : descriptorProto.getDependencyList()) { + if (!descriptorProtoIndex.containsKey(dependencyName)) { + throw new IllegalArgumentException("Could not find dependency: " + dependencyName); + } + FileDescriptorProto dependencyProto = descriptorProtoIndex.get(dependencyName); + dependencies.add(descriptorFromProto(dependencyProto, descriptorProtoIndex, descriptorCache)); + } + + // Finally, construct the actual descriptor. + FileDescriptor[] empty = new FileDescriptor[0]; + return FileDescriptor.buildFrom(descriptorProto, dependencies.build().toArray(empty)); + } + + public List getFileDescriptors() { + return fileDescriptors; + } +} diff --git a/src/main/java/we/proxy/grpc/client/utils/ChannelFactory.java b/src/main/java/we/proxy/grpc/client/utils/ChannelFactory.java new file mode 100644 index 0000000..3e59f1d --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/utils/ChannelFactory.java @@ -0,0 +1,57 @@ +package we.proxy.grpc.client.utils; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static java.util.Collections.emptyMap; + +import java.util.Map; + +import com.google.common.net.HostAndPort; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors.CheckedForwardingClientCall; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.MethodDescriptor; +import io.grpc.netty.NegotiationType; +import io.grpc.netty.NettyChannelBuilder; + +/** + * Knows how to construct grpc channels. + */ +public class ChannelFactory { + + public static ManagedChannel create(HostAndPort endpoint) { + return create(endpoint, emptyMap()); + } + + public static ManagedChannel create(HostAndPort endpoint, Map metaDataMap) { + return NettyChannelBuilder.forAddress(endpoint.getHostText(), endpoint.getPort()) + .negotiationType(NegotiationType.PLAINTEXT) + .intercept(metadataInterceptor(metaDataMap)) + .build(); + } + + private static ClientInterceptor metadataInterceptor(Map metaDataMap) { + return new ClientInterceptor() { + @Override + public ClientCall interceptCall( + final MethodDescriptor method, CallOptions callOptions, final Channel next) { + + return new CheckedForwardingClientCall(next.newCall(method, callOptions)) { + @Override + protected void checkedStart(Listener responseListener, Metadata headers) { + metaDataMap.forEach((k, v) -> { + Key mKey = Key.of(k, ASCII_STRING_MARSHALLER); + headers.put(mKey, String.valueOf(v)); + }); + delegate().start(responseListener, headers); + } + }; + } + }; + } +} diff --git a/src/main/java/we/proxy/grpc/client/utils/GrpcReflectionUtils.java b/src/main/java/we/proxy/grpc/client/utils/GrpcReflectionUtils.java new file mode 100644 index 0000000..778c70d --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/utils/GrpcReflectionUtils.java @@ -0,0 +1,133 @@ +package we.proxy.grpc.client.utils; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.grpc.MethodDescriptor.generateFullMethodName; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +//import static org.apache.commons.collections4.CollectionUtils.isEmpty; +import static org.apache.commons.lang3.ObjectUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import com.google.protobuf.util.JsonFormat.Parser; +import com.google.protobuf.util.JsonFormat.TypeRegistry; + +import io.grpc.Channel; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.Status; +import we.proxy.grpc.client.core.GrpcMethodDefinition; +import we.proxy.grpc.client.core.ServerReflectionClient; + +/** + * @author zhangjikai + */ +public class GrpcReflectionUtils { + private static final Logger logger = LoggerFactory.getLogger(GrpcReflectionUtils.class); + + public static List resolveServices(Channel channel) { + ServerReflectionClient serverReflectionClient = ServerReflectionClient.create(channel); + try { + List services = serverReflectionClient.listServices().get(); + if (isEmpty(services)) { + logger.info("Can't find services by channel {}", channel); + return emptyList(); + } + return services.stream().map(serviceName -> { + ListenableFuture future = serverReflectionClient.lookupService(serviceName); + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + logger.error("Get {} fileDescriptor occurs error", serviceName, e); + return null; + } + }).filter(Objects::nonNull).collect(toList()); + } catch (Throwable t) { + logger.error("Exception resolve service", t); + throw new RuntimeException(t); + } + } + + public static FileDescriptorSet resolveService(Channel channel, String serviceName) { + ServerReflectionClient reflectionClient = ServerReflectionClient.create(channel); + try { + List serviceNames = reflectionClient.listServices().get(); + if (!serviceNames.contains(serviceName)) { + throw Status.NOT_FOUND.withDescription( + String.format("Remote server does not have service %s. Services: %s", serviceName, serviceNames)) + .asRuntimeException(); + } + + return reflectionClient.lookupService(serviceName).get(); + } catch (InterruptedException | ExecutionException e) { + logger.error("Resolve services get error", e); + throw new RuntimeException(e); + } + } + + public static String fetchFullMethodName(MethodDescriptor methodDescriptor) { + String serviceName = methodDescriptor.getService().getFullName(); + String methodName = methodDescriptor.getName(); + return generateFullMethodName(serviceName, methodName); + } + + public static MethodType fetchMethodType(MethodDescriptor methodDescriptor) { + boolean clientStreaming = methodDescriptor.toProto().getClientStreaming(); + boolean serverStreaming = methodDescriptor.toProto().getServerStreaming(); + if (clientStreaming && serverStreaming) { + return MethodType.BIDI_STREAMING; + } else if (!clientStreaming && !serverStreaming) { + return MethodType.UNARY; + } else if (!clientStreaming) { + return MethodType.SERVER_STREAMING; + } else { + return MethodType.SERVER_STREAMING; + } + } + + public static List parseToMessages(TypeRegistry registry, Descriptor descriptor, + List jsonTexts) { + Parser parser = JsonFormat.parser().usingTypeRegistry(registry); + List messages = new ArrayList<>(); + try { + for (String jsonText : jsonTexts) { + DynamicMessage.Builder messageBuilder = DynamicMessage.newBuilder(descriptor); + parser.merge(jsonText, messageBuilder); + messages.add(messageBuilder.build()); + } + return messages; + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException("Unable to parse json text", e); + } + } + + public static GrpcMethodDefinition parseToMethodDefinition(String rawMethodName) { + checkArgument(isNotBlank(rawMethodName), "Raw method name can't be empty."); + int methodSplitPosition = rawMethodName.lastIndexOf("."); + checkArgument(methodSplitPosition != -1, "No package name and service name found."); + String methodName = rawMethodName.substring(methodSplitPosition + 1); + checkArgument(isNotBlank(methodName), "Method name can't be empty."); + String fullServiceName = rawMethodName.substring(0, methodSplitPosition); + int serviceSplitPosition = fullServiceName.lastIndexOf("."); + String serviceName = fullServiceName.substring(serviceSplitPosition + 1); + String packageName = ""; + if (serviceSplitPosition != -1) { + packageName = fullServiceName.substring(0, serviceSplitPosition); + } + checkArgument(isNotBlank(serviceName), "Service name can't be empty."); + return new GrpcMethodDefinition(packageName, serviceName, methodName); + } +} diff --git a/src/main/java/we/proxy/grpc/client/utils/MessageWriter.java b/src/main/java/we/proxy/grpc/client/utils/MessageWriter.java new file mode 100644 index 0000000..8184e59 --- /dev/null +++ b/src/main/java/we/proxy/grpc/client/utils/MessageWriter.java @@ -0,0 +1,54 @@ +package we.proxy.grpc.client.utils; + +import static com.google.protobuf.util.JsonFormat.TypeRegistry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import com.google.protobuf.util.JsonFormat.Printer; + +import io.grpc.stub.StreamObserver; +import we.proxy.grpc.client.CallResults; + +/** + * @author zhangjikai + */ +public class MessageWriter implements StreamObserver { + private static final Logger logger = LoggerFactory.getLogger(MessageWriter.class); + + private final Printer printer; + private final CallResults results; + + private MessageWriter(Printer printer, CallResults results) { + this.printer = printer; + this.results = results; + } + + public static MessageWriter newInstance(TypeRegistry registry, CallResults results){ + return new MessageWriter<>( + JsonFormat.printer().usingTypeRegistry(registry).includingDefaultValueFields(), + results); + } + + @Override + public void onNext(T value) { + try { + results.add(printer.print(value)); + } catch (InvalidProtocolBufferException e) { + logger.error("Skipping invalid response message", e); + } + } + + @Override + public void onError(Throwable t) { + logger.error("Messages write occur errors", t); + } + + @Override + public void onCompleted() { + logger.info("Messages write complete"); + } +} diff --git a/src/main/java/we/util/MapUtil.java b/src/main/java/we/util/MapUtil.java index 913a130..3b2d77a 100644 --- a/src/main/java/we/util/MapUtil.java +++ b/src/main/java/we/util/MapUtil.java @@ -30,7 +30,7 @@ import com.alibaba.fastjson.JSON; /** * - * @author francis + * @author Francis Dong * */ public class MapUtil { @@ -38,7 +38,7 @@ public class MapUtil { public static HttpHeaders toHttpHeaders(Map params) { HttpHeaders headers = new HttpHeaders(); - if (params.isEmpty()) { + if (params == null || params.isEmpty()) { return headers; } @@ -69,7 +69,7 @@ public class MapUtil { public static MultiValueMap toMultiValueMap(Map params) { MultiValueMap mvmap = new LinkedMultiValueMap<>(); - if (params.isEmpty()) { + if (params == null || params.isEmpty()) { return mvmap; } @@ -99,7 +99,7 @@ public class MapUtil { public static Map toHashMap(MultiValueMap params) { HashMap m = new HashMap<>(); - if (params.isEmpty()) { + if (params == null || params.isEmpty()) { return m; } @@ -116,6 +116,41 @@ public class MapUtil { return m; } + + public static Map headerToHashMap(HttpHeaders headers) { + HashMap m = new HashMap<>(); + + if (headers == null || headers.isEmpty()) { + return m; + } + + for (Entry> entry : headers.entrySet()) { + List val = entry.getValue(); + if (val != null && val.size() > 0) { + if (val.size() > 1) { + m.put(entry.getKey().toUpperCase(), val); + } else { + m.put(entry.getKey().toUpperCase(), val.get(0)); + } + } + } + + return m; + } + + public static Map upperCaseKey(Map m) { + HashMap rs = new HashMap<>(); + + if (m == null || m.isEmpty()) { + return rs; + } + + for (Entry entry : m.entrySet()) { + rs.put(entry.getKey().toUpperCase(), entry.getValue()); + } + + return rs; + } /** * Set value by path,support multiple levels,eg:a.b.c
diff --git a/src/main/java/we/util/WebUtils.java b/src/main/java/we/util/WebUtils.java index 7a0355a..f12769a 100644 --- a/src/main/java/we/util/WebUtils.java +++ b/src/main/java/we/util/WebUtils.java @@ -32,6 +32,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import we.config.SystemConfig; import we.constants.CommonConstants; import we.filter.FilterResult; import we.flume.clients.log4j2appender.LogService; @@ -44,6 +45,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author hongqiaowei @@ -51,47 +54,63 @@ import java.util.Set; public abstract class WebUtils { - private static final Logger log = LoggerFactory.getLogger(WebUtils.class); + private static final Logger log = LoggerFactory.getLogger(WebUtils.class); - private static final String clientService = "clientService"; + private static final String clientService = "clientService"; - public static final String BACKEND_SERVICE = "backendService"; + private static final String xForwardedFor = "X-FORWARDED-FOR"; - private static final String xForwardedFor = "X-FORWARDED-FOR"; + private static final String unknown = "unknown"; - private static final String unknown = "unknown"; + private static final String loopBack = "127.0.0.1"; - private static final String loopBack = "127.0.0.1"; + private static final String binaryAddress = "0:0:0:0:0:0:0:1"; - private static final String binaryAddress = "0:0:0:0:0:0:0:1"; + private static final String directResponse = "directResponse"; - private static final String directResponse = "directResponse"; + private static final String response = " response "; - private static final String response = " response "; + private static final String originIp = "originIp"; - private static final String originIp = "originIp"; + private static final String clientRequestPath = "clientRequestPath"; - public static final String APP_HEADER = "fizz-appid"; + private static final String clientRequestPathPrefix = "clientRequestPathPrefix"; - public static final String FILTER_CONTEXT = "filterContext"; + private static final String clientRequestQuery = "clientRequestQuery"; - public static final String APPEND_HEADERS = "appendHeaders"; + private static final String traceId = "traceId"; - public static final String PREV_FILTER_RESULT = "prevFilterResult"; + private static String gatewayPrefix = SystemConfig.DEFAULT_GATEWAY_PREFIX; - private static final String CLIENT_REQUEST_PATH = "clientRequestPath"; + private static List appHeaders = Stream.of("fizz-appid").collect(Collectors.toList()); - private static final String CLIENT_REQUEST_QUERY = "clientRequestQuery"; + private static final String app = "app"; - private static final String traceId = "traceId"; + public static final String BACKEND_SERVICE = "backendService"; - public static final String BACKEND_PATH = "backendPath"; + public static final String FILTER_CONTEXT = "filterContext"; - public static boolean logResponseBody = false; + public static final String APPEND_HEADERS = "appendHeaders"; - public static Set logHeaderSet = Collections.EMPTY_SET; + public static final String PREV_FILTER_RESULT = "prevFilterResult"; - public static final DataBuffer EMPTY_BODY = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false, true)).wrap(Constants.Symbol.EMPTY.getBytes()); + public static final String BACKEND_PATH = "backendPath"; + + public static boolean LOG_RESPONSE_BODY = false; + + public static Set LOG_HEADER_SET = Collections.EMPTY_SET; + + public static final DataBuffer EMPTY_BODY = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false, true)).wrap(Constants.Symbol.EMPTY.getBytes()); + + + + public static void setGatewayPrefix(String p) { + gatewayPrefix = p; + } + + public static void setAppHeaders(List hdrs) { + appHeaders = hdrs; + } public static String getHeaderValue(ServerWebExchange exchange, String header) { return exchange.getRequest().getHeaders().getFirst(header); @@ -102,38 +121,37 @@ public abstract class WebUtils { } public static String getAppId(ServerWebExchange exchange) { - return exchange.getAttribute(APP_HEADER); + String a = exchange.getAttribute(app); + if (a == null) { + HttpHeaders headers = exchange.getRequest().getHeaders(); + for (int i = 0; i < appHeaders.size(); i++) { + a = headers.getFirst(appHeaders.get(i)); + if (a != null) { + exchange.getAttributes().put(app, a); + break; + } + } + } + return a; } public static String getClientService(ServerWebExchange exchange) { String svc = exchange.getAttribute(clientService); if (svc == null) { String p = exchange.getRequest().getPath().value(); - int pl = p.length(); - if (pl < 15) { + int secFS = p.indexOf(Constants.Symbol.FORWARD_SLASH, 1); + if (StringUtils.isBlank(gatewayPrefix) || Constants.Symbol.FORWARD_SLASH_STR.equals(gatewayPrefix)) { + svc = p.substring(1, secFS); } else { - boolean b = false; - if (p.charAt(2) == 'r' && p.charAt(3) == 'o' && p.charAt(4) == 'x') { - b = true; - } - if (b) { - byte i = 9; - if (p.charAt(6) == 't') { - i = 13; - } - for (; i < pl; i++) { - if (p.charAt(i) == Constants.Symbol.FORWARD_SLASH) { - if (p.charAt(6) == 't') { - svc = p.substring(11, i); - } else { - svc = p.substring(7, i); - } - break; - } - } - exchange.getAttributes().put(clientService, svc); + String prefix = p.substring(0, secFS); + if (gatewayPrefix.equals(prefix) || SystemConfig.DEFAULT_GATEWAY_TEST_PREFIX.equals(prefix)) { + int trdFS = p.indexOf(Constants.Symbol.FORWARD_SLASH, secFS + 1); + svc = p.substring(secFS + 1, trdFS); + } else { + throw Utils.runtimeExceptionWithoutStack("wrong prefix " + prefix); } } + exchange.getAttributes().put(clientService, svc); } return svc; } @@ -263,13 +281,24 @@ public abstract class WebUtils { } public static String getClientReqPath(ServerWebExchange exchange) { - String path = exchange.getAttribute(CLIENT_REQUEST_PATH); - if (path == null) { - path = exchange.getRequest().getPath().value(); - path = path.substring(path.indexOf(Constants.Symbol.FORWARD_SLASH, 11), path.length()); - exchange.getAttributes().put(CLIENT_REQUEST_PATH, path); + String p = exchange.getAttribute(clientRequestPath); + if (p == null) { + p = exchange.getRequest().getPath().value(); + int secFS = p.indexOf(Constants.Symbol.FORWARD_SLASH, 1); + if (StringUtils.isBlank(gatewayPrefix) || Constants.Symbol.FORWARD_SLASH_STR.equals(gatewayPrefix)) { + p = p.substring(secFS); + } else { + String prefix = p.substring(0, secFS); + if (gatewayPrefix.equals(prefix) || SystemConfig.DEFAULT_GATEWAY_TEST_PREFIX.equals(prefix)) { + int trdFS = p.indexOf(Constants.Symbol.FORWARD_SLASH, secFS + 1); + p = p.substring(trdFS); + } else { + throw Utils.runtimeExceptionWithoutStack("wrong prefix " + prefix); + } + } + exchange.getAttributes().put(clientRequestPath, p); } - return path; + return p; } public static void setBackendPath(ServerWebExchange exchange, String path) { @@ -281,16 +310,27 @@ public abstract class WebUtils { } public static String getClientReqPathPrefix(ServerWebExchange exchange) { - String p = exchange.getRequest().getPath().value(); - byte i = 7; - if (p.charAt(6) == 't') { - i = 11; + String prefix = exchange.getAttribute(clientRequestPathPrefix); + if (prefix == null) { + if (StringUtils.isBlank(gatewayPrefix) || Constants.Symbol.FORWARD_SLASH_STR.equals(gatewayPrefix)) { + prefix = Constants.Symbol.FORWARD_SLASH_STR; + } else { + String path = exchange.getRequest().getPath().value(); + int secFS = path.indexOf(Constants.Symbol.FORWARD_SLASH, 1); + prefix = path.substring(0, secFS); + if (gatewayPrefix.equals(prefix) || SystemConfig.DEFAULT_GATEWAY_TEST_PREFIX.equals(prefix)) { + prefix = prefix + Constants.Symbol.FORWARD_SLASH; + } else { + throw Utils.runtimeExceptionWithoutStack("wrong prefix " + prefix); + } + } + exchange.getAttributes().put(clientRequestPathPrefix, prefix); } - return p.substring(0, i); + return prefix; } public static String getClientReqQuery(ServerWebExchange exchange) { - String qry = exchange.getAttribute(CLIENT_REQUEST_QUERY); + String qry = exchange.getAttribute(clientRequestQuery); if (qry != null && StringUtils.EMPTY.equals(qry)) { return null; } else { @@ -298,12 +338,12 @@ public abstract class WebUtils { URI uri = exchange.getRequest().getURI(); qry = uri.getQuery(); if (qry == null) { - exchange.getAttributes().put(CLIENT_REQUEST_QUERY, StringUtils.EMPTY); + exchange.getAttributes().put(clientRequestQuery, StringUtils.EMPTY); } else { if (StringUtils.indexOfAny(qry, Constants.Symbol.LEFT_BRACE, Constants.Symbol.FORWARD_SLASH, Constants.Symbol.HASH) > 0) { qry = uri.getRawQuery(); } - exchange.getAttributes().put(CLIENT_REQUEST_QUERY, qry); + exchange.getAttributes().put(clientRequestQuery, qry); } } return qry; @@ -372,7 +412,7 @@ public abstract class WebUtils { b.append(reqId).append(Constants.Symbol.SPACE).append(method).append(Constants.Symbol.SPACE).append(uri); if (headers != null) { final boolean[] f = {false}; - logHeaderSet.forEach( + LOG_HEADER_SET.forEach( h -> { String v = headers.getFirst(h); if (v != null) { @@ -392,7 +432,7 @@ public abstract class WebUtils { b.append(rid).append(response).append(clientResponse.statusCode()); HttpHeaders headers = clientResponse.headers().asHttpHeaders(); final boolean[] f = {false}; - logHeaderSet.forEach( + LOG_HEADER_SET.forEach( h -> { String v = headers.getFirst(h); if (v != null) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0722d74..ee97d06 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -77,8 +77,10 @@ aggr-webclient: name: aggr fizz-web-client: timeout: 20000 +fizz-dubbo-client: + address: zookeeper://127.0.0.1:2181 log: - headers: COOKIE,FIZZ-APPID,FIZZ-SECRETKEY,FIZZ-SIGN,FIZZ-TS,FIZZ-RSV + headers: COOKIE,FIZZ-APPID,FIZZ-SIGN,FIZZ-TS,FIZZ-RSV,HOST stat: # switch for push access stat data @@ -92,4 +94,10 @@ flowControl: true flow-stat-sched: cron: 2/10 * * * * ? dest: redis - queue: fizz_resource_access_stat \ No newline at end of file + queue: fizz_resource_access_stat + +gateway: + prefix: /proxy + aggr: + # set headers when calling the backend API + proxy_set_headers: host,X-Real-IP,X-Forwarded-Proto,X-Forwarded-For diff --git a/src/main/resources/js/common.js b/src/main/resources/js/common.js index 689ff00..9e52cb2 100644 --- a/src/main/resources/js/common.js +++ b/src/main/resources/js/common.js @@ -5,7 +5,10 @@ var common = { /* *********** private function begin *********** */ - // 获取上下文中客户端请求对象 + /** + * 获取上下文中客户端请求对象 + * @param {*} ctx 上下文 【必填】 + */ getInputReq: function (ctx){ if(!ctx || !ctx['input'] || !ctx['input']['request']){ return {}; @@ -13,7 +16,12 @@ var common = { return ctx['input']['request'] }, - // 获取上下文步骤中请求接口的请求对象 + /** + * 获取上下文步骤中请求接口的请求对象 + * @param {*} ctx 上下文 【必填】 + * @param {*} stepName 步骤名称 【必填】 + * @param {*} requestName 请求名称 【必填】 + */ getStepReq: function (ctx, stepName, requestName){ if(!ctx || !stepName || !requestName){ return {}; @@ -25,7 +33,12 @@ var common = { return ctx[stepName]['requests'][requestName]['request']; }, - // 获取上下文步骤中请求接口的响应对象 + /** + * 获取上下文步骤中请求接口的响应对象 + * @param {*} ctx 上下文 【必填】 + * @param {*} stepName 步骤名称 【必填】 + * @param {*} requestName 请求名称 【必填】 + */ getStepResp: function (ctx, stepName, requestName){ if(!ctx || !stepName || !requestName){ return {}; @@ -49,7 +62,7 @@ var common = { getInputReqHeader: function (ctx, headerName){ var req = this.getInputReq(ctx); var headers = req['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** @@ -82,7 +95,7 @@ var common = { getInputRespHeader: function (ctx, headerName){ var req = this.getInputReq(ctx); var headers = req['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** @@ -110,7 +123,7 @@ var common = { getStepReqHeader: function (ctx, stepName, requestName, headerName){ var req = this.getStepReq(ctx, stepName, requestName); var headers = req['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** @@ -149,7 +162,7 @@ var common = { getStepRespHeader: function (ctx, stepName, requestName, headerName){ var resp = this.getStepResp(ctx, stepName, requestName); var headers = resp['headers'] || {}; - return headerName ? headers[headerName] : headers; + return headerName ? headers[headerName.toUpperCase()] : headers; }, /** diff --git a/src/test/java/we/filter/FlowControlFilterTests.java b/src/test/java/we/filter/FlowControlFilterTests.java index b546200..f93a7e1 100644 --- a/src/test/java/we/filter/FlowControlFilterTests.java +++ b/src/test/java/we/filter/FlowControlFilterTests.java @@ -49,7 +49,7 @@ public class FlowControlFilterTests { Thread.sleep(3000); } - @Test + //@Test void flowControlFilterTest() throws NoSuchFieldException, InterruptedException { FlowControlFilter flowControlFilter = new FlowControlFilter(); diff --git a/src/test/java/we/fizz/group/DevTestGroup.java b/src/test/java/we/fizz/group/DevTestGroup.java new file mode 100644 index 0000000..c9c9468 --- /dev/null +++ b/src/test/java/we/fizz/group/DevTestGroup.java @@ -0,0 +1,4 @@ +package we.fizz.group; + +public class DevTestGroup { +} diff --git a/src/test/java/we/fizz/group/FastTestGroup.java b/src/test/java/we/fizz/group/FastTestGroup.java new file mode 100644 index 0000000..8d9abd1 --- /dev/null +++ b/src/test/java/we/fizz/group/FastTestGroup.java @@ -0,0 +1,4 @@ +package we.fizz.group; + +public class FastTestGroup { +} diff --git a/src/test/java/we/fizz/group/SlowTestGroup.java b/src/test/java/we/fizz/group/SlowTestGroup.java new file mode 100644 index 0000000..a99bbc6 --- /dev/null +++ b/src/test/java/we/fizz/group/SlowTestGroup.java @@ -0,0 +1,4 @@ +package we.fizz.group; + +public class SlowTestGroup { +} diff --git a/src/test/java/we/fizz/input/DubboInputMockTests.java b/src/test/java/we/fizz/input/DubboInputMockTests.java new file mode 100644 index 0000000..e050fda --- /dev/null +++ b/src/test/java/we/fizz/input/DubboInputMockTests.java @@ -0,0 +1,88 @@ +package we.fizz.input; + +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.rpc.service.GenericService; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ConfigurableApplicationContext; +import we.fizz.group.FastTestGroup; +import we.fizz.Step; +import we.fizz.StepContext; +import we.fizz.StepResponse; + +import we.fizz.input.extension.dubbo.DubboInput; +import we.fizz.input.extension.dubbo.DubboInputConfig; +import we.proxy.dubbo.ApacheDubboGenericService; +import we.proxy.dubbo.DubboInterfaceDeclaration; + +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +@Category(FastTestGroup.class) +public class DubboInputMockTests { + private static final String SERVICE_NAME = "com.fizzgate.test"; + private static final String METHOD_NAME = "method"; + private static final String[] LEFT = new String[]{}; + + private static final Object[] RIGHT = new Object[]{}; + + private ApacheDubboGenericService proxy; +// @Before +// public void setup(){ +// ApacheDubboGenericProxyTests test = new ApacheDubboGenericProxyTests(); +// proxy = test.getMockApachDubbo(); +// } + + @Test + public void test() { + DubboInterfaceDeclaration declaration = new DubboInterfaceDeclaration(); + declaration.setServiceName(SERVICE_NAME); + declaration.setMethod(METHOD_NAME); + declaration.setParameterTypes("java.lang.String, java.lang.String"); + declaration.setTimeout(3000); + + ReferenceConfig referenceConfig = mock(ReferenceConfig.class); + GenericService genericService = mock(GenericService.class); + when(referenceConfig.get()).thenReturn(genericService); + when(referenceConfig.getInterface()).thenReturn(SERVICE_NAME); + CompletableFuture future = new CompletableFuture<>(); + when(genericService.$invokeAsync(any(), any(), any())).thenReturn(future); + ApacheDubboGenericService apacheDubboProxyService = new ApacheDubboGenericService(); + ApacheDubboGenericService proxy = spy(apacheDubboProxyService); + when(proxy.createReferenceConfig(SERVICE_NAME, null, null)).thenReturn(referenceConfig); + + ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class); + when(applicationContext.getBean(ApacheDubboGenericService.class)).thenReturn(proxy); + + Step step = mock(Step.class); + when(step.getCurrentApplicationContext()).thenReturn(applicationContext); + + StepResponse stepResponse = new StepResponse(step, null, new HashMap>()); + DubboInputConfig config = mock(DubboInputConfig.class); + when(config.getServiceName()).thenReturn(SERVICE_NAME); + InputFactory.registerInput(DubboInput.TYPE, DubboInput.class); + DubboInput dubboInput = (DubboInput)InputFactory.createInput(DubboInput.TYPE.toString()); + + dubboInput.setName("input1"); + dubboInput.setWeakStep(new SoftReference<>(step)); + dubboInput.setStepResponse(stepResponse); + dubboInput.setConfig(config); + StepContext stepContext = mock(StepContext.class); + stepContext.put("step1", stepResponse); + InputContext context = new InputContext(stepContext, null); + dubboInput.beforeRun(context); + + dubboInput.run(); + + future.complete("success"); + + } +} diff --git a/src/test/java/we/fizz/input/DubboInputTests.java b/src/test/java/we/fizz/input/DubboInputTests.java new file mode 100644 index 0000000..251d62b --- /dev/null +++ b/src/test/java/we/fizz/input/DubboInputTests.java @@ -0,0 +1,44 @@ +package we.fizz.input; + +import org.junit.experimental.categories.Category; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import reactor.core.publisher.Mono; + +import we.fizz.group.DevTestGroup; +import we.fizz.input.extension.dubbo.DubboInput; +import we.fizz.input.extension.dubbo.DubboInputConfig; + +import java.util.HashMap; +import java.util.Map; + + +@ActiveProfiles("") +//@SpringBootTest +@Category(DevTestGroup.class) +class DubboInputTests { + private static final String SERVICE_NAME = "com.fizzgate.fizz.examples.dubbo.common.service.UserService"; + private static final String METHOD_NAME = "findAll"; + //@Test + public void test() { + + Map requestConfig = new HashMap(); + requestConfig.put("serviceName",SERVICE_NAME); + requestConfig.put("method",METHOD_NAME); +// requestConfig.put("parameterTypes", "java.lang.String"); + + DubboInputConfig inputConfig = new DubboInputConfig(requestConfig); + inputConfig.parse(); + DubboInput input = new DubboInput(); + input.setConfig(inputConfig); + input.beforeRun(null); + Monomono = input.run(); + Map result = mono.block(); + System.out.print(result); + Assertions.assertNotEquals(result, null, "no response"); + + } + +} \ No newline at end of file diff --git a/src/test/java/we/fizz/input/GrpcInputMockTests.java b/src/test/java/we/fizz/input/GrpcInputMockTests.java new file mode 100644 index 0000000..2a20480 --- /dev/null +++ b/src/test/java/we/fizz/input/GrpcInputMockTests.java @@ -0,0 +1,120 @@ +package we.fizz.input; + +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.util.ReflectionTestUtils; +import we.fizz.group.FastTestGroup; +import we.fizz.Step; +import we.fizz.StepContext; +import we.fizz.StepResponse; +import we.fizz.input.extension.grpc.GrpcInput; +import we.fizz.input.extension.grpc.GrpcInputConfig; +import we.proxy.grpc.GrpcGenericService; +import we.proxy.grpc.GrpcInterfaceDeclaration; +import we.proxy.grpc.client.GrpcProxyClient; +import we.proxy.grpc.client.utils.ChannelFactory; + +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.*; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +@Category(FastTestGroup.class) +public class GrpcInputMockTests { + private static final String URL ="localhost:8090"; + private static final String SERVICE_NAME = "com.fizzgate.test"; + private static final String METHOD_NAME = "method"; + private static final String[] LEFT = new String[]{}; + + private static final Object[] RIGHT = new Object[]{}; + + private GrpcGenericService proxy; +// @Before +// public void setup(){ +// ApacheDubboGenericProxyTests test = new ApacheDubboGenericProxyTests(); +// proxy = test.getMockApachDubbo(); +// } + + @Test + public void test() { + mockStatic(ChannelFactory.class); + GrpcInterfaceDeclaration declaration = new GrpcInterfaceDeclaration(); + declaration.setEndpoint(URL); + declaration.setServiceName(SERVICE_NAME); + declaration.setMethod(METHOD_NAME); + declaration.setTimeout(3000); + + GrpcProxyClient grpcProxyClient = mock(GrpcProxyClient.class); + + ListenableFuture future = new ListenableFuture() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public String get() throws InterruptedException, ExecutionException { + return "result"; + } + + @Override + public String get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return null; + } + + @Override + public void addListener(Runnable runnable, Executor executor) { + + } + }; + when(grpcProxyClient.invokeMethodAsync(any(), any(), any(), any(), any())).thenReturn((ListenableFuture) future); + + GrpcGenericService grpcGenericService = new GrpcGenericService(); + ReflectionTestUtils.setField(grpcGenericService, "grpcProxyClient", grpcProxyClient); + + ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class); + when(applicationContext.getBean(GrpcGenericService.class)).thenReturn(grpcGenericService); + + Step step = mock(Step.class); + when(step.getCurrentApplicationContext()).thenReturn(applicationContext); + + StepResponse stepResponse = new StepResponse(step, null, new HashMap>()); + GrpcInputConfig config = mock(GrpcInputConfig.class); + when(config.getServiceName()).thenReturn(SERVICE_NAME); + InputFactory.registerInput(GrpcInput.TYPE, GrpcInput.class); + GrpcInput grpcInput = (GrpcInput)InputFactory.createInput(GrpcInput.TYPE.toString()); + HashMap request = new HashMap (); + request.put("url",URL); + ReflectionTestUtils.setField(grpcInput, "request", request); + grpcInput.setName("input1"); + grpcInput.setWeakStep(new SoftReference<>(step)); + grpcInput.setStepResponse(stepResponse); + grpcInput.setConfig(config); + StepContext stepContext = mock(StepContext.class); + stepContext.put("step1", stepResponse); + InputContext context = new InputContext(stepContext, null); + grpcInput.beforeRun(context); + + grpcInput.run(); + + + } +} diff --git a/src/test/java/we/fizz/input/PathMappingTests.java b/src/test/java/we/fizz/input/PathMappingTests.java index 2868fae..a7bb652 100644 --- a/src/test/java/we/fizz/input/PathMappingTests.java +++ b/src/test/java/we/fizz/input/PathMappingTests.java @@ -29,8 +29,8 @@ class PathMappingTests { m.put("a", "1"); m.put("b", "1"); - PathMapping.setByPath(target, "data.id", "2"); - PathMapping.setByPath(target, "data", m); + PathMapping.setByPath(target, "data.id", "2", true); + PathMapping.setByPath(target, "data", m, false); assertEquals("1", target.get("data").get("a").getString()); assertEquals("1", target.get("data").get("b").getString()); @@ -39,11 +39,11 @@ class PathMappingTests { List list = new ArrayList<>(); list.add("YYYY"); - PathMapping.setByPath(target, "data.zzz", list); + PathMapping.setByPath(target, "data.zzz", list, true); List list2 = new ArrayList<>(); list2.add("XXXX"); - PathMapping.setByPath(target, "data.zzz", list2); + PathMapping.setByPath(target, "data.zzz", list2, true); List actualList = (List) target.get("data").get("zzz").toData(); @@ -53,16 +53,16 @@ class PathMappingTests { List list3 = new ArrayList<>(); list3.add("vvvv"); - PathMapping.setByPath(target, "data.ppp", list3); + PathMapping.setByPath(target, "data.ppp", list3, true); Map m3 = new HashMap<>(); m3.put("sss", "sss"); - PathMapping.setByPath(target, "data.ppp", m3); + PathMapping.setByPath(target, "data.ppp", m3, true); List list4 = new ArrayList<>(); list4.add("kkk"); - PathMapping.setByPath(target, "data.ppp", list4); + PathMapping.setByPath(target, "data.ppp", list4, true); List actualList2 = (List) target.get("data").get("ppp").toData(); assertTrue(actualList2.contains("kkk")); @@ -100,11 +100,60 @@ class PathMappingTests { pathMap.put("input.responseHeaders", "input.response.headers"); pathMap.put("input.responseBody", "input.response.body"); + pathMap.put("step1.request1.request.headers.Aa", "step1.requests.request1.request.headers.AA"); + pathMap.put("step1.request1.response.headers.aa", "step1.requests.request1.response.headers.AA"); + pathMap.put("input.requestHeaders.aa", "input.request.headers.AA"); + pathMap.put("input.responseHeaders.aA", "input.response.headers.AA"); + for (Entry entry : pathMap.entrySet()) { Assertions.assertEquals(entry.getValue(), PathMapping.handlePath(entry.getKey())); } } + @Test + void testSelect() { + Map m = new HashMap<>(); + ONode ctxNode = PathMapping.toONode(m); + PathMapping.setByPath(ctxNode, "step1.requests.request1.request.headers.abc", "1", true); + PathMapping.setByPath(ctxNode, "step1.requests.request1.request.headers.name1", "ken", true); + PathMapping.setByPath(ctxNode, "input.request.headers.abc", "1", true); + PathMapping.setByPath(ctxNode, "input.request.headers.name1", "ken", true); + List vals = new ArrayList<>(); + vals.add("Ken"); + vals.add("Kelly"); + PathMapping.setByPath(ctxNode, "step1.requests.request1.request.headers.name2", vals, true); + PathMapping.setByPath(ctxNode, "input.request.headers.name2", vals, true); + + + + ONode abc = PathMapping.select(ctxNode, "step1.requests.request1.request.headers.abc"); + ONode name1 = PathMapping.select(ctxNode, "step1.requests.request1.request.headers.name1"); + ONode inputAbc = PathMapping.select(ctxNode, "input.request.headers.abc"); + ONode inputAbcName1 = PathMapping.select(ctxNode, "input.request.headers.name1"); + ONode name2 = PathMapping.select(ctxNode, "step1.requests.request1.request.headers.name2"); + ONode inputAbcName2 = PathMapping.select(ctxNode, "input.request.headers.name2"); + assertEquals("1", (String)abc.toData()); + assertEquals("ken", (String)name1.toData()); + assertEquals("1", (String)inputAbc.toData()); + assertEquals("ken", (String)inputAbcName1.toData()); + assertEquals(2, ((List)name2.toData()).size()); + assertEquals(2, ((List)inputAbcName2.toData()).size()); + + abc = PathMapping.select(ctxNode, "step1.requests.request1.request.headers.abc[0]"); + name1 = PathMapping.select(ctxNode, "step1.requests.request1.request.headers.name1[0]"); + inputAbc = PathMapping.select(ctxNode, "input.request.headers.abc[0]"); + inputAbcName1 = PathMapping.select(ctxNode, "input.request.headers.name1[0]"); + name2 = PathMapping.select(ctxNode, "step1.requests.request1.request.headers.name2[0]"); + inputAbcName2 = PathMapping.select(ctxNode, "input.request.headers.name2[0]"); + assertEquals("1", (String)abc.toData()); + assertEquals("ken", (String)name1.toData()); + assertEquals("1", (String)inputAbc.toData()); + assertEquals("ken", (String)inputAbcName1.toData()); + assertEquals("Ken", (String)name2.toData()); + assertEquals("Ken", (String)inputAbcName2.toData()); + + } + } \ No newline at end of file diff --git a/src/test/java/we/fizz/input/RequestInputTests.java b/src/test/java/we/fizz/input/RequestInputTests.java index 96ffdc9..3f0271f 100644 --- a/src/test/java/we/fizz/input/RequestInputTests.java +++ b/src/test/java/we/fizz/input/RequestInputTests.java @@ -20,6 +20,7 @@ package we.fizz.input; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; +import we.fizz.input.extension.request.RequestInput; /** * diff --git a/src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceMockTests.java b/src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceMockTests.java new file mode 100644 index 0000000..bfafaa5 --- /dev/null +++ b/src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceMockTests.java @@ -0,0 +1,58 @@ +package we.fizz.input.proxy.dubbo; + +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.rpc.service.GenericService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import we.fizz.group.FastTestGroup; +import we.proxy.dubbo.ApacheDubboGenericService; +import we.proxy.dubbo.DubboInterfaceDeclaration; + +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) +@Category(FastTestGroup.class) +public class ApacheDubboGenericServiceMockTests { + private static final String SERVICE_NAME = "com.fizzgate.test"; + private static final String METHOD_NAME = "method"; + private static final String[] LEFT = new String[]{}; + + private static final Object[] RIGHT = new Object[]{}; + @Before + public void setup(){ + + } + + public ApacheDubboGenericService getMockApachDubbo(){ + ReferenceConfig referenceConfig = mock(ReferenceConfig.class); + GenericService genericService = mock(GenericService.class); + when(referenceConfig.get()).thenReturn(genericService); + when(referenceConfig.getInterface()).thenReturn(SERVICE_NAME); + ApacheDubboGenericService apacheDubboProxyService = mock(ApacheDubboGenericService.class); + when(apacheDubboProxyService.createReferenceConfig(SERVICE_NAME, null, null)).thenReturn(referenceConfig); + CompletableFuture future = new CompletableFuture<>(); + when(genericService.$invokeAsync(METHOD_NAME, LEFT, RIGHT)).thenReturn(future); + future.complete("success"); + return apacheDubboProxyService; + } + @Test + public void test() { + HashMap attachments = mock(HashMap.class); + DubboInterfaceDeclaration declaration = mock(DubboInterfaceDeclaration.class); + declaration.setServiceName(SERVICE_NAME); + declaration.setMethod(METHOD_NAME); + declaration.setParameterTypes("java.lang.String, java.lang.String"); + declaration.setTimeout(3000); + ApacheDubboGenericServiceMockTests test = new ApacheDubboGenericServiceMockTests(); + ApacheDubboGenericService apacheDubboProxyService = test.getMockApachDubbo(); + apacheDubboProxyService.send(null, declaration, attachments); + } + +} diff --git a/src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceTests.java b/src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceTests.java new file mode 100644 index 0000000..76ca08f --- /dev/null +++ b/src/test/java/we/fizz/input/proxy/dubbo/ApacheDubboGenericServiceTests.java @@ -0,0 +1,55 @@ +package we.fizz.input.proxy.dubbo; + +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.rpc.service.GenericService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import we.proxy.dubbo.ApacheDubboGenericService; +import we.proxy.dubbo.DubboInterfaceDeclaration; + +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) +public class ApacheDubboGenericServiceTests { + private static final String SERVICE_NAME = "com.fizzgate.test"; + private static final String METHOD_NAME = "method"; + private static final String[] LEFT = new String[]{}; + + private static final Object[] RIGHT = new Object[]{}; + @Before + public void setup(){ + + } + + public ApacheDubboGenericService getMockApachDubbo(){ + ReferenceConfig referenceConfig = mock(ReferenceConfig.class); + GenericService genericService = mock(GenericService.class); + when(referenceConfig.get()).thenReturn(genericService); + when(referenceConfig.getInterface()).thenReturn(SERVICE_NAME); + ApacheDubboGenericService apacheDubboProxyService = mock(ApacheDubboGenericService.class); + when(apacheDubboProxyService.createReferenceConfig(SERVICE_NAME, null, null)).thenReturn(referenceConfig); + CompletableFuture future = new CompletableFuture<>(); + when(genericService.$invokeAsync(METHOD_NAME, LEFT, RIGHT)).thenReturn(future); + future.complete("success"); + return apacheDubboProxyService; + } + @Test + public void test() { + HashMap attachments = mock(HashMap.class); + DubboInterfaceDeclaration declaration = mock(DubboInterfaceDeclaration.class); + declaration.setServiceName(SERVICE_NAME); + declaration.setMethod(METHOD_NAME); + declaration.setParameterTypes("java.lang.String, java.lang.String"); + declaration.setTimeout(3000); + ApacheDubboGenericServiceTests test = new ApacheDubboGenericServiceTests(); + ApacheDubboGenericService apacheDubboProxyService = test.getMockApachDubbo(); + apacheDubboProxyService.send(null, declaration, attachments); + } + +} diff --git a/src/test/java/we/util/WebUtilsTests.java b/src/test/java/we/util/WebUtilsTests.java index 230afa6..40a4204 100644 --- a/src/test/java/we/util/WebUtilsTests.java +++ b/src/test/java/we/util/WebUtilsTests.java @@ -1,18 +1,9 @@ package we.util; -import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; -import we.proxy.CallbackReplayReq; -import we.proxy.ServiceInstance; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -24,44 +15,30 @@ public class WebUtilsTests { @Test void getClientReqPathPrefixTest() throws JsonProcessingException { - MockServerHttpRequest mockRequest = MockServerHttpRequest.get("http://127.0.0.1:8600/proxytest/xservice/ybiz").build(); + WebUtils.setGatewayPrefix("/_proxytestx"); + MockServerHttpRequest mockRequest = MockServerHttpRequest.get("http://127.0.0.1:8600/_proxytest/xservice/ybiz?a=b").build(); MockServerWebExchange mockExchange = MockServerWebExchange.from(mockRequest); String clientService = WebUtils.getClientService(mockExchange); - assertEquals(clientService, "xservice"); + assertEquals("xservice", clientService); + String clientReqPath = WebUtils.getClientReqPath(mockExchange); + assertEquals("/ybiz", clientReqPath); String clientReqPathPrefix = WebUtils.getClientReqPathPrefix(mockExchange); - assertEquals(clientReqPathPrefix, "/proxytest/"); + assertEquals("/_proxytest/", clientReqPathPrefix); - MockServerHttpRequest mr = MockServerHttpRequest.get("http://127.0.0.1:8600/proxytest/test/ybiz").build(); - MockServerWebExchange me = MockServerWebExchange.from(mr); - String cs = WebUtils.getClientService(me); - // System.err.println(cs); - String crpp = WebUtils.getClientReqPathPrefix(me); - // System.err.println(crpp); + WebUtils.setGatewayPrefix("/prox"); + mockRequest = MockServerHttpRequest.get("http://127.0.0.1:8600/prox/test/ybiz").build(); + mockExchange = MockServerWebExchange.from(mockRequest); + clientService = WebUtils.getClientService(mockExchange); + assertEquals("test", clientService); + clientReqPath = WebUtils.getClientReqPath(mockExchange); + assertEquals("/ybiz", clientReqPath); - - // HttpHeaders httpHeaders = new HttpHeaders(); - // httpHeaders.add("h0", "v0"); - // List values = Arrays.asList("v11", "v12"); - // httpHeaders.addAll("h1", values); - // - // String s = JSON.toJSONString(JSON.toJSONString(httpHeaders)); - // System.err.println("s: " + s); - // Map> m = (Map>) JSON.parse(JSON.parse(s).toString()); - // System.err.println("m: " + m); - - - // ServiceInstance si1 = new ServiceInstance("127", 80); - // ServiceInstance si2 = new ServiceInstance("128", 90); - // Map receivers = new HashMap<>(); - // receivers.put("s1", si1); - // receivers.put("s2", si2); - // String receiversStr = JSON.toJSONString(JSON.toJSONString(receivers)); - // System.err.println("receivers: " + receiversStr); - // CallbackReplayReq req = new CallbackReplayReq(); - // req.setReceivers(receiversStr); - // System.err.println("s2 ip: " + req.receivers.get("s2").ip); - // - // String x = JSON.parseObject(receiversStr, String.class); - // System.err.println("x: " + x); + WebUtils.setGatewayPrefix(""); + mockRequest = MockServerHttpRequest.get("http://127.0.0.1:8600/aservice/ybiz1").build(); + mockExchange = MockServerWebExchange.from(mockRequest); + clientService = WebUtils.getClientService(mockExchange); + assertEquals("aservice", clientService); + clientReqPath = WebUtils.getClientReqPath(mockExchange); + assertEquals("/ybiz1", clientReqPath); } }