From 006fc666636f56d0de02dc1479fadacc64bcaf06 Mon Sep 17 00:00:00 2001 From: liuhy Date: Mon, 8 Jun 2026 17:09:32 +0800 Subject: [PATCH] Restore client-only modules for split repository The split client repository needs to carry MCP registration, beat registration, Spring Boot starters, and discovery registration without inheriting the main repository JDK upgrade constraints. This restores the client-side module surface on the JDK 8 release base, rebases dependencies onto Spring Boot 2/JDK 8 compatible lines, and expands CI coverage through JDK 25 after local compatibility verification. Constraint: Client artifacts must remain JDK 8 compatible while validating JDK 17, 21, 23, and 25 Constraint: The split repository does not define the release Maven profile, so CI uses ./mvnw -B clean test Rejected: Move the client back under the main repository | it would keep client compatibility tied to admin/bootstrap JDK upgrade timing Rejected: Keep JDK 23 and 25 as local-only checks | CI would not catch future toolchain regressions Confidence: medium Scope-risk: broad Directive: Do not upgrade client runtime dependencies without rerunning the JDK 8 compatibility build Tested: ./mvnw -DskipTests compile Tested: ./mvnw test Tested: JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home ./mvnw -B clean test Tested: JAVA_HOME=/Users/aias/Library/Java/JavaVirtualMachines/corretto-17.0.13/Contents/Home ./mvnw -B clean test -Prelease Tested: JAVA_HOME=/Users/aias/Library/Java/JavaVirtualMachines/openjdk-21.0.2/Contents/Home ./mvnw -B clean test -Prelease Tested: JAVA_HOME=/Users/aias/Library/Java/JavaVirtualMachines/openjdk-25.0.2/Contents/Home ./mvnw -B clean test Tested: JAVA_HOME=/tmp/shenyu-jdks/jdk-23.0.2+7/Contents/Home ./mvnw -B clean test Not-tested: End-to-end registration against matching admin/bootstrap runtime --- .github/workflows/ci.yml | 32 +- pom.xml | 18 +- progress.md | 51 +++ shenyu-client-core/pom.xml | 15 +- .../client/core/config/ShenyuConfig.java | 36 ++ .../client/core/constant/Constants.java | 15 + .../core/constant/InstanceTypeConstants.java | 32 ++ .../ShenyuClientRegisterEventPublisher.java | 2 + .../ShenyuClientMcpExecutorSubscriber.java | 52 +++ .../core/dto/DiscoveryUpstreamData.java | 130 ++++++ .../client/core/dto/InstanceBeatInfoDTO.java | 74 ++++ .../client/core/dto/McpToolsRegisterDTO.java | 92 +++++ .../shenyu/client/core/enums/RpcTypeEnum.java | 7 +- .../register/FailbackRegistryRepository.java | 40 ++ .../HttpClientRegisterRepository.java | 6 + .../register/InstanceRegisterListener.java | 100 +++++ .../ShenyuClientRegisterRepository.java | 9 + .../shenyu/client/core/type/DataType.java | 5 + .../client/core/utils/RequestMethodUtils.java | 192 +++++++++ .../client/core/utils/SystemInfoUtils.java | 47 +++ .../client/core/utils/VersionUtils.java | 127 ++++++ shenyu-client-mcp/pom.xml | 41 ++ .../shenyu-client-mcp-common/pom.xml | 56 +++ .../common/annotation/ShenyuMcpHeader.java | 38 ++ .../annotation/ShenyuMcpRequestConfig.java | 38 ++ .../mcp/common/annotation/ShenyuMcpTool.java | 78 ++++ .../common/annotation/ShenyuMcpToolParam.java | 41 ++ .../common/constants/OpenApiConstants.java | 124 ++++++ .../constants/RequestTemplateConstants.java | 55 +++ .../constants/ShenyuToolConfigConstants.java | 47 +++ .../common/dto/ShenyuMcpRequestConfig.java | 70 ++++ .../client/mcp/common/dto/ShenyuMcpTool.java | 138 +++++++ .../mcp/common/eunm/McpParameterType.java | 75 ++++ .../mcp/generator/McpOpenApiGenerator.java | 95 +++++ .../generator/McpRequestConfigGenerator.java | 100 +++++ .../McpToolsRegisterDTOGenerator.java | 82 ++++ .../mcp/utils/OpenApiConvertorUtil.java | 101 +++++ .../shenyu-client-mcp-register/pom.xml | 56 +++ .../client/mcp/McpServiceEventListener.java | 387 ++++++++++++++++++ shenyu-register-client-beat/pom.xml | 57 +++ .../client/beat/HeartbeatListener.java | 145 +++++++ .../beat/ShenyuBootstrapHeartBeatConfig.java | 56 +++ .../client/beat/HeartbeatListenerTest.java | 316 ++++++++++++++ shenyu-registry-api/pom.xml | 44 ++ .../api/ShenyuInstanceRegisterRepository.java | 91 ++++ .../registry/api/config/RegisterConfig.java | 262 ++++++++++++ .../registry/api/entity/InstanceEntity.java | 286 +++++++++++++ .../api/event/ChangedEventListener.java | 54 +++ .../api/path/InstancePathConstants.java | 71 ++++ .../api/config/RegisterConfigTest.java | 105 +++++ .../api/path/InstancePathConstantsTest.java | 49 +++ shenyu-spring-boot-starter-client/pom.xml | 53 +++ .../pom.xml | 46 +++ .../ShenyuApacheDubboClientConfiguration.java | 54 +++ .../main/resources/META-INF/spring.factories | 19 + .../main/resources/META-INF/spring.provides | 18 + ...ot.autoconfigure.AutoConfiguration.imports | 18 + ...nyuApacheDubboClientConfigurationTest.java | 60 +++ .../src/test/resources/application.properties | 33 ++ .../pom.xml | 40 ++ .../beat/HeartbeatListenerConfiguration.java | 61 +++ .../main/resources/META-INF/spring.factories | 19 + ...ot.autoconfigure.AutoConfiguration.imports | 18 + .../pom.xml | 43 ++ .../ShenyuClientCommonBeanConfiguration.java | 93 +++++ .../main/resources/META-INF/spring.factories | 19 + ...ot.autoconfigure.AutoConfiguration.imports | 18 + ...enyuClientCommonBeanConfigurationTest.java | 120 ++++++ .../pom.xml | 45 ++ .../grpc/ShenyuGrpcClientConfiguration.java | 102 +++++ .../ShenyuGrpcDiscoveryConfiguration.java | 93 +++++ .../main/resources/META-INF/spring.factories | 20 + .../main/resources/META-INF/spring.provides | 18 + ...ot.autoconfigure.AutoConfiguration.imports | 19 + .../ShenyuGrpcClientConfigurationTest.java | 98 +++++ .../server/ShenyuGrpcServerBuilderTest.java | 34 ++ .../pom.xml | 41 ++ .../mcp/ShenyuMcpClientConfiguration.java | 59 +++ .../pom.xml | 50 +++ .../sofa/ShenyuSofaClientConfiguration.java | 53 +++ .../main/resources/META-INF/spring.factories | 19 + .../main/resources/META-INF/spring.provides | 18 + ...ot.autoconfigure.AutoConfiguration.imports | 18 + .../ShenyuSofaClientConfigurationTest.java | 78 ++++ .../pom.xml | 46 +++ ...nyuSpringWebSocketClientConfiguration.java | 88 ++++ ...SpringWebSocketDiscoveryConfiguration.java | 93 +++++ .../main/resources/META-INF/spring.factories | 20 + .../main/resources/META-INF/spring.provides | 18 + ...ot.autoconfigure.AutoConfiguration.imports | 19 + ...pringWebSocketClientConfigurationTest.java | 72 ++++ .../pom.xml | 50 +++ .../ShenyuSpringMvcClientConfiguration.java | 107 +++++ ...ingMvcClientInfoRegisterConfiguration.java | 95 +++++ ...ShenyuSpringMvcDiscoveryConfiguration.java | 96 +++++ .../main/resources/META-INF/spring.factories | 21 + .../main/resources/META-INF/spring.provides | 18 + ...ot.autoconfigure.AutoConfiguration.imports | 20 + ...henyuSpringMvcClientConfigurationTest.java | 101 +++++ .../pom.xml | 41 ++ .../tars/ShenyuTarsClientConfiguration.java | 53 +++ .../main/resources/META-INF/spring.factories | 19 + .../main/resources/META-INF/spring.provides | 18 + ...ot.autoconfigure.AutoConfiguration.imports | 18 + .../ShenyuTarsClientConfigurationTest.java | 73 ++++ 105 files changed, 6793 insertions(+), 30 deletions(-) create mode 100644 progress.md create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/config/ShenyuConfig.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/InstanceTypeConstants.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/subcriber/ShenyuClientMcpExecutorSubscriber.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/DiscoveryUpstreamData.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/InstanceBeatInfoDTO.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/McpToolsRegisterDTO.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/InstanceRegisterListener.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/RequestMethodUtils.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/SystemInfoUtils.java create mode 100644 shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/VersionUtils.java create mode 100644 shenyu-client-mcp/pom.xml create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/pom.xml create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpHeader.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpRequestConfig.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpTool.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpToolParam.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/OpenApiConstants.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/RequestTemplateConstants.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/ShenyuToolConfigConstants.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpRequestConfig.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpTool.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/eunm/McpParameterType.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpOpenApiGenerator.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpToolsRegisterDTOGenerator.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/utils/OpenApiConvertorUtil.java create mode 100644 shenyu-client-mcp/shenyu-client-mcp-register/pom.xml create mode 100644 shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java create mode 100644 shenyu-register-client-beat/pom.xml create mode 100644 shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java create mode 100644 shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/ShenyuBootstrapHeartBeatConfig.java create mode 100644 shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java create mode 100644 shenyu-registry-api/pom.xml create mode 100644 shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/ShenyuInstanceRegisterRepository.java create mode 100644 shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/config/RegisterConfig.java create mode 100644 shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/entity/InstanceEntity.java create mode 100644 shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/event/ChangedEventListener.java create mode 100644 shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/path/InstancePathConstants.java create mode 100644 shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/config/RegisterConfigTest.java create mode 100644 shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/path/InstancePathConstantsTest.java create mode 100644 shenyu-spring-boot-starter-client/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.provides create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfigurationTest.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/resources/application.properties create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/test/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfigurationTest.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcDiscoveryConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfigurationTest.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/server/ShenyuGrpcServerBuilderTest.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/src/main/java/org/apache/shenyu/springboot/starter/client/mcp/ShenyuMcpClientConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.provides create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/test/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfigurationTest.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketDiscoveryConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.provides create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/test/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfigurationTest.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientInfoRegisterConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcDiscoveryConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.provides create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/test/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfigurationTest.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/pom.xml create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfiguration.java create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.factories create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.provides create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/test/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfigurationTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afb6c11..ccf9c3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,9 @@ on: jobs: build: strategy: + fail-fast: false matrix: - java: [ 8, 11, 17 ] + java: [ 8, 17, 21, 23, 25 ] os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} if: (github.repository == 'apache/shenyu-client-java') @@ -33,7 +34,7 @@ jobs: - name: Support longpaths if: ${{ matrix.os == 'windows-latest'}} run: git config --system core.longpaths true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true # - uses: dorny/paths-filter@v2 @@ -41,40 +42,26 @@ jobs: # with: # filters: '.github/filters.yml' # list-files: json - - name: Restore ShenYu Maven Repos -# if: steps.filter.outputs.changed == 'true' - id: restore-maven-cache - uses: actions/cache/restore@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 # if: steps.filter.outputs.changed == 'true' with: + distribution: temurin java-version: ${{ matrix.java }} + cache: maven - name: Build with Maven # if: steps.filter.outputs.changed == 'true' - run: ./mvnw -B clean test -Prelease - - uses: codecov/codecov-action@v1 + run: ./mvnw -B clean test + - uses: codecov/codecov-action@v4 with: token: 2760af6a-3405-4882-9e61-04c5176fecfa # if: steps.filter.outputs.changed == 'true' - - name: Save ShenYu Maven Repos -# if: steps.filter.outputs.changed == 'true' && steps.restore-maven-cache.outputs.cache-hit != 'true' - if: steps.restore-maven-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} check-license-header: name: check-license-header runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true - name: Check License Header @@ -92,4 +79,3 @@ jobs: - name: checking job status run: | [[ "${{ needs.build.result }}" == "success" ]] || exit -1 - diff --git a/pom.xml b/pom.xml index de025b8..bbae5f8 100644 --- a/pom.xml +++ b/pom.xml @@ -29,8 +29,10 @@ pom 2.7.0.1-jdk8-SNAPSHOT + shenyu-registry-api shenyu-client-core shenyu-client-http + shenyu-client-mcp shenyu-client-dubbo shenyu-client-sofa shenyu-client-tars @@ -39,6 +41,8 @@ shenyu-client-websocket shenyu-client-api-docs-annotations shenyu-client-autoconfig + shenyu-register-client-beat + shenyu-spring-boot-starter-client @@ -76,7 +80,7 @@ false 1.6.0 - 0.8.12 + 0.8.15 1.6.3 1.6 3.0.1 @@ -116,7 +120,9 @@ 4.9.3 1.78 3.5.15 + 1.18.10 4.0.3 + 2.2.21 @@ -197,6 +203,16 @@ bcprov-jdk18on ${bcprov-jdk18on.version} + + net.bytebuddy + byte-buddy + ${byte-buddy.version} + + + net.bytebuddy + byte-buddy-agent + ${byte-buddy.version} + diff --git a/progress.md b/progress.md new file mode 100644 index 0000000..70e2f6b --- /dev/null +++ b/progress.md @@ -0,0 +1,51 @@ +# Client Split Migration Progress + +## Baseline + +- Working branch: `split-client-jdk8-base` +- Base branch: `origin/2.7.0.1-jdk8-release` +- Goal: keep the split client repository JDK 8 compatible while preparing it to absorb client-only changes from the main ShenYu repository. + +## Done + +- Selected the existing `2.7.0.1-jdk8-release` branch as the migration base because it already preserves Java 8 source level and Apache release metadata. +- Added the MCP client module from the main ShenYu repository. +- Rebased MCP module POMs onto `shenyu-client-java` and removed Spring Boot 3 / springdoc starter coupling. +- Added MCP registration DTO, disruptor subscriber, RPC/data types, and HTTP register path support in `shenyu-client-core`. +- Added a Java 8 compatible `RequestMethodUtils` for MCP method and parameter discovery. +- Added the Spring Boot starter client modules from the main ShenYu repository. +- Rebased starter POMs onto this split client repo and rewired imports to `shenyu-client-core`. +- Restored the beat register client and beat Spring Boot starter. +- Restored the registry API module needed by instance registration. +- Restored Spring MVC, gRPC, and Spring WebSocket discovery instance auto-configuration. +- Added the minimal client-core DTO/config/registry support needed by beat and discovery without pulling in the full main-repo `shenyu-common` module. +- Updated GitHub Actions CI to run the Maven build on JDK 8, 17, 21, 23, and 25. +- Kept the SOFA starter test dependency on the JDK 8 compatible SOFA Boot 3.1.4 line. +- Upgraded JaCoCo to 0.8.15 so coverage instrumentation recognizes JDK 25 class files. +- Overrode Byte Buddy test dependencies to 1.18.10 so Mockito 3.x tests can run on JDK 21, 23, and 25 while preserving JDK 8 runtime compatibility. + +## Verification + +- `./mvnw -pl shenyu-client-mcp/shenyu-client-mcp-register -am -DskipTests compile` passed. +- `./mvnw -pl shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat -am -DskipTests compile` passed. +- `./mvnw -DskipTests compile` passed. +- `./mvnw -DskipTests test` passed. +- `./mvnw -pl shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc -am test` passed. +- `./mvnw test` passed across the full 30-module reactor. +- `JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./mvnw -B clean test -Prelease` passed across the full 30-module reactor. +- `JAVA_HOME=$(/usr/libexec/java_home -v 17) ./mvnw -B clean test -Prelease` passed across the full 30-module reactor. +- `JAVA_HOME=$(/usr/libexec/java_home -v 21) ./mvnw -B clean test -Prelease` passed across the full 30-module reactor. +- `JAVA_HOME=$(/usr/libexec/java_home -v 25) ./mvnw -B clean test` passed across the full 30-module reactor after the JaCoCo and Byte Buddy upgrades. +- Downloaded Temurin JDK 23.0.2+7 from Adoptium to `/tmp/shenyu-jdks`, verified SHA256 `749993e751f085c7ae713140066a90800075e4aeedfac50a5ed0c5457131c5a0`, and ran `JAVA_HOME=/tmp/shenyu-jdks/jdk-23.0.2+7/Contents/Home ./mvnw -B clean test` successfully across the full 30-module reactor. +- Re-ran `JAVA_HOME=$(/usr/libexec/java_home -v 1.8) ./mvnw -B clean test` successfully after the JaCoCo and Byte Buddy upgrades. +- The CI command now omits `-Prelease` because Maven reports that profile does not exist in this split repository. +- `.github/workflows/ci.yml` YAML parsing passed. +- Compatibility scan found no remaining old `org.apache.shenyu.common`, `org.apache.shenyu.register.common`, `org.apache.shenyu.register.client.http`, or `jakarta.*` imports in the restored beat/discovery code. +- Java 8 syntax scan found no `List.of`, `Map.of`, `Set.of`, or pattern-matching `instanceof` usage in the migrated MCP/starter modules. +- The full test run still logs existing background exceptions from original async tests under JDK 17, but Surefire reports reactor success. + +## Next Steps + +- Confirm the expanded GitHub Actions matrix on the remote runner after opening the pull request. +- Verify beat and discovery instance registration against matching admin/bootstrap runtime. +- Verify `/shenyu-client/register-mcp` against the matching admin side before treating MCP registration as end-to-end complete. diff --git a/shenyu-client-core/pom.xml b/shenyu-client-core/pom.xml index 0c008e9..e854e6a 100644 --- a/shenyu-client-core/pom.xml +++ b/shenyu-client-core/pom.xml @@ -31,11 +31,16 @@ shenyu-disruptor 2.6.1 - - - - - + + org.apache.shenyu + shenyu-spi + 2.6.1 + + + org.apache.shenyu + shenyu-registry-api + ${project.version} + com.github.ben-manes.caffeine diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/config/ShenyuConfig.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/config/ShenyuConfig.java new file mode 100644 index 0000000..fb66e04 --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/config/ShenyuConfig.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.config; + +import org.apache.shenyu.client.core.constant.Constants; + +/** + * Minimal client-side ShenYu configuration used by heartbeat reporting. + */ +public class ShenyuConfig { + + private String namespace = Constants.SYS_DEFAULT_NAMESPACE_ID; + + public String getNamespace() { + return namespace; + } + + public void setNamespace(final String namespace) { + this.namespace = namespace; + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/Constants.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/Constants.java index 8489011..d620c49 100644 --- a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/Constants.java +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/Constants.java @@ -269,6 +269,11 @@ public interface Constants { */ String HEARTBEAT = "heartbeat"; + /** + * The constant BEAT_URI_PATH. + */ + String BEAT_URI_PATH = "/instance/beat"; + /** * The constant header key of sign plugin version-2. */ @@ -623,6 +628,16 @@ public interface Constants { * When register by http, the uri path. */ String URI_PATH = "/shenyu-client/register-uri"; + + /** + * When register by http, the mcp tools path. + */ + String MCP_TOOLS_PATH = "/shenyu-client/register-mcp"; + + /** + * When register by http, the mcp tools type. + */ + String MCP_TOOLS_TYPE = "mcp"; /** * When register by http, the offline path. diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/InstanceTypeConstants.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/InstanceTypeConstants.java new file mode 100644 index 0000000..d3c5d4a --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/constant/InstanceTypeConstants.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.constant; + +/** + * Instance type constants. + */ +public final class InstanceTypeConstants { + + /** + * Bootstrap instance info type. + */ + public static final String BOOTSTRAP_INSTANCE_INFO = "bootstrapInstanceInfo"; + + private InstanceTypeConstants() { + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/ShenyuClientRegisterEventPublisher.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/ShenyuClientRegisterEventPublisher.java index b5eaea0..8743900 100644 --- a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/ShenyuClientRegisterEventPublisher.java +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/ShenyuClientRegisterEventPublisher.java @@ -19,6 +19,7 @@ import org.apache.shenyu.client.core.disruptor.executor.RegisterClientConsumerExecutor.RegisterClientExecutorFactory; import org.apache.shenyu.client.core.disruptor.subcriber.ShenyuClientApiDocExecutorSubscriber; +import org.apache.shenyu.client.core.disruptor.subcriber.ShenyuClientMcpExecutorSubscriber; import org.apache.shenyu.client.core.disruptor.subcriber.ShenyuClientMetadataExecutorSubscriber; import org.apache.shenyu.client.core.disruptor.subcriber.ShenyuClientURIExecutorSubscriber; import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; @@ -54,6 +55,7 @@ public void start(final ShenyuClientRegisterRepository shenyuClientRegisterRepos factory.addSubscribers(new ShenyuClientMetadataExecutorSubscriber(shenyuClientRegisterRepository)); factory.addSubscribers(new ShenyuClientURIExecutorSubscriber(shenyuClientRegisterRepository)); factory.addSubscribers(new ShenyuClientApiDocExecutorSubscriber(shenyuClientRegisterRepository)); + factory.addSubscribers(new ShenyuClientMcpExecutorSubscriber(shenyuClientRegisterRepository)); providerManage = new DisruptorProviderManage<>(factory); providerManage.startup(); } diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/subcriber/ShenyuClientMcpExecutorSubscriber.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/subcriber/ShenyuClientMcpExecutorSubscriber.java new file mode 100644 index 0000000..dd88746 --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/disruptor/subcriber/ShenyuClientMcpExecutorSubscriber.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.disruptor.subcriber; + +import org.apache.shenyu.client.core.dto.McpToolsRegisterDTO; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.subsriber.ExecutorTypeSubscriber; +import org.apache.shenyu.client.core.type.DataType; + +import java.util.Collection; + +/** + * The type Shenyu client mcp executor subscriber. + */ +public class ShenyuClientMcpExecutorSubscriber implements ExecutorTypeSubscriber { + + private final ShenyuClientRegisterRepository shenyuClientRegisterRepository; + + /** + * Instantiates a new Shenyu client mcp executor subscriber. + * + * @param shenyuClientRegisterRepository the shenyu client register repository + */ + public ShenyuClientMcpExecutorSubscriber(final ShenyuClientRegisterRepository shenyuClientRegisterRepository) { + this.shenyuClientRegisterRepository = shenyuClientRegisterRepository; + } + + @Override + public void executor(final Collection dataList) { + dataList.forEach(shenyuClientRegisterRepository::persistMcpTools); + } + + @Override + public DataType getType() { + return DataType.MCP_TOOLS; + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/DiscoveryUpstreamData.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/DiscoveryUpstreamData.java new file mode 100644 index 0000000..15ba3aa --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/DiscoveryUpstreamData.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.sql.Timestamp; + +/** + * Discovery upstream data used by client-side instance discovery registration. + */ +public class DiscoveryUpstreamData { + + private String id; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp dateCreated; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Timestamp dateUpdated; + + private String discoveryHandlerId; + + private String protocol; + + private String url; + + private int status; + + private int weight; + + private String props; + + private String namespaceId; + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public Timestamp getDateCreated() { + return dateCreated; + } + + public void setDateCreated(final Timestamp dateCreated) { + this.dateCreated = dateCreated; + } + + public Timestamp getDateUpdated() { + return dateUpdated; + } + + public void setDateUpdated(final Timestamp dateUpdated) { + this.dateUpdated = dateUpdated; + } + + public String getDiscoveryHandlerId() { + return discoveryHandlerId; + } + + public void setDiscoveryHandlerId(final String discoveryHandlerId) { + this.discoveryHandlerId = discoveryHandlerId; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(final String protocol) { + this.protocol = protocol; + } + + public String getUrl() { + return url; + } + + public void setUrl(final String url) { + this.url = url; + } + + public int getStatus() { + return status; + } + + public void setStatus(final int status) { + this.status = status; + } + + public int getWeight() { + return weight; + } + + public void setWeight(final int weight) { + this.weight = weight; + } + + public String getProps() { + return props; + } + + public void setProps(final String props) { + this.props = props; + } + + public String getNamespaceId() { + return namespaceId; + } + + public void setNamespaceId(final String namespaceId) { + this.namespaceId = namespaceId; + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/InstanceBeatInfoDTO.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/InstanceBeatInfoDTO.java new file mode 100644 index 0000000..1e6c11a --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/InstanceBeatInfoDTO.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.dto; + +/** + * Bootstrap heartbeat payload. + */ +public class InstanceBeatInfoDTO { + + private String instanceIp; + + private String instancePort; + + private String instanceType; + + private String instanceInfo; + + private String namespaceId; + + public String getInstanceIp() { + return instanceIp; + } + + public void setInstanceIp(final String instanceIp) { + this.instanceIp = instanceIp; + } + + public String getInstancePort() { + return instancePort; + } + + public void setInstancePort(final String instancePort) { + this.instancePort = instancePort; + } + + public String getInstanceType() { + return instanceType; + } + + public void setInstanceType(final String instanceType) { + this.instanceType = instanceType; + } + + public String getInstanceInfo() { + return instanceInfo; + } + + public void setInstanceInfo(final String instanceInfo) { + this.instanceInfo = instanceInfo; + } + + public String getNamespaceId() { + return namespaceId; + } + + public void setNamespaceId(final String namespaceId) { + this.namespaceId = namespaceId; + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/McpToolsRegisterDTO.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/McpToolsRegisterDTO.java new file mode 100644 index 0000000..56af176 --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/dto/McpToolsRegisterDTO.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.dto; + +import org.apache.shenyu.client.core.type.DataType; +import org.apache.shenyu.client.core.type.DataTypeParent; + +/** + * Mcp tools register dto. + */ +public class McpToolsRegisterDTO implements DataTypeParent { + + private MetaDataRegisterDTO metaDataRegisterDTO; + + private String namespaceId; + + private String mcpConfig; + + /** + * get meta data register dto. + * + * @return metaDataRegisterDTO + */ + public MetaDataRegisterDTO getMetaDataRegisterDTO() { + return metaDataRegisterDTO; + } + + /** + * set meta data register dto. + * + * @param metaDataRegisterDTO metaDataRegisterDTO + */ + public void setMetaDataRegisterDTO(final MetaDataRegisterDTO metaDataRegisterDTO) { + this.metaDataRegisterDTO = metaDataRegisterDTO; + } + + /** + * get mcp config. + * + * @return mcpConfig + */ + public String getMcpConfig() { + return mcpConfig; + } + + /** + * set mcp config. + * + * @param mcpConfig mcpConfig + */ + public void setMcpConfig(final String mcpConfig) { + this.mcpConfig = mcpConfig; + } + + /** + * get namespace id. + * + * @return namespaceId + */ + public String getNamespaceId() { + return namespaceId; + } + + /** + * set namespace id. + * + * @param namespaceId namespaceId + */ + public void setNamespaceId(final String namespaceId) { + this.namespaceId = namespaceId; + } + + @Override + public DataType getType() { + return DataType.MCP_TOOLS; + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/enums/RpcTypeEnum.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/enums/RpcTypeEnum.java index 3a44ac0..8fda93c 100644 --- a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/enums/RpcTypeEnum.java +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/enums/RpcTypeEnum.java @@ -69,7 +69,12 @@ public enum RpcTypeEnum { /** * ai. */ - AI("ai", true); + AI("ai", true), + + /** + * mcp. + */ + MCP("mcp", true); private final String name; diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/FailbackRegistryRepository.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/FailbackRegistryRepository.java index 28ee568..7301545 100644 --- a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/FailbackRegistryRepository.java +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/FailbackRegistryRepository.java @@ -19,6 +19,7 @@ import org.apache.shenyu.client.core.constant.Constants; import org.apache.shenyu.client.core.dto.ApiDocRegisterDTO; +import org.apache.shenyu.client.core.dto.McpToolsRegisterDTO; import org.apache.shenyu.client.core.dto.MetaDataRegisterDTO; import org.apache.shenyu.client.core.dto.URIRegisterDTO; import org.apache.shenyu.client.core.register.retry.FailureRegistryTask; @@ -96,6 +97,21 @@ public void persistApiDoc(final ApiDocRegisterDTO registerDTO) { } } + /** + * Persist mcp tools. + * + * @param registerDTO registerDTO + */ + @Override + public void persistMcpTools(final McpToolsRegisterDTO registerDTO) { + try { + this.doPersistMcpTools(registerDTO); + } catch (Exception ex) { + logger.warn("Failed to persistMcpTools {}, cause:{}", registerDTO, ex.getMessage()); + this.addFailureMcpToolsRegister(registerDTO); + } + } + /** * doPersistApiDoc. * @@ -103,6 +119,13 @@ public void persistApiDoc(final ApiDocRegisterDTO registerDTO) { */ protected abstract void doPersistApiDoc(ApiDocRegisterDTO apiDocRegisterDTO); + /** + * doPersistMcpTools. + * + * @param mcpToolsRegisterDTO mcpToolsRegisterDTO + */ + protected abstract void doPersistMcpTools(McpToolsRegisterDTO mcpToolsRegisterDTO); + /** * Add failure meta data register. * @@ -145,6 +168,20 @@ protected void addFailureApiDocRegister(final T t) { } } + /** + * Add failure mcp tools register. + * + * @param the type parameter + * @param t the t + */ + protected void addFailureMcpToolsRegister(final T t) { + if (t instanceof McpToolsRegisterDTO) { + McpToolsRegisterDTO dto = (McpToolsRegisterDTO) t; + String address = String.join(":", dto.getNamespaceId(), dto.getMcpConfig()); + addToFail(new Holder(t, address, Constants.MCP_TOOLS_TYPE)); + } + } + private void addToFail(final Holder t) { Holder oldObj = concurrentHashMap.get(t.getKey()); if (Objects.nonNull(oldObj)) { @@ -186,6 +223,9 @@ public void accept(final String key) { case Constants.API_DOC_TYPE: this.doPersistApiDoc((ApiDocRegisterDTO) holder.getObj()); break; + case Constants.MCP_TOOLS_TYPE: + this.doPersistMcpTools((McpToolsRegisterDTO) holder.getObj()); + break; default: break; } diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/HttpClientRegisterRepository.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/HttpClientRegisterRepository.java index e6898ed..0431573 100644 --- a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/HttpClientRegisterRepository.java +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/HttpClientRegisterRepository.java @@ -26,6 +26,7 @@ import org.apache.shenyu.client.core.constant.Constants; import org.apache.shenyu.client.core.dto.ApiDocRegisterDTO; import org.apache.shenyu.client.core.dto.DiscoveryConfigRegisterDTO; +import org.apache.shenyu.client.core.dto.McpToolsRegisterDTO; import org.apache.shenyu.client.core.dto.MetaDataRegisterDTO; import org.apache.shenyu.client.core.dto.URIRegisterDTO; import org.apache.shenyu.client.core.enums.EventType; @@ -152,6 +153,11 @@ public void doPersistInterface(final MetaDataRegisterDTO metadata) { doRegister(metadata, Constants.META_PATH, Constants.META_TYPE); } + @Override + protected void doPersistMcpTools(final McpToolsRegisterDTO registerDTO) { + doRegister(registerDTO, Constants.MCP_TOOLS_PATH, Constants.MCP_TOOLS_TYPE); + } + @Override public void closeRepository() { if (Objects.nonNull(uriRegisterDTO)) { diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/InstanceRegisterListener.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/InstanceRegisterListener.java new file mode 100644 index 0000000..4ddfdcb --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/InstanceRegisterListener.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.register; + +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.core.dto.DiscoveryUpstreamData; +import org.apache.shenyu.client.core.register.config.ShenyuDiscoveryConfig; +import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository; +import org.apache.shenyu.registry.api.config.RegisterConfig; +import org.apache.shenyu.registry.api.entity.InstanceEntity; +import org.apache.shenyu.spi.ExtensionLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.Ordered; + +import java.net.URI; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; + +/** + * Instance register listener. + */ +public class InstanceRegisterListener implements ApplicationListener, Ordered { + + private static final Logger LOGGER = LoggerFactory.getLogger(InstanceRegisterListener.class); + + private final DiscoveryUpstreamData currentInstanceUpstream; + + private final RegisterConfig discoveryConfig; + + private final String path; + + private ShenyuInstanceRegisterRepository discoveryService; + + public InstanceRegisterListener(final DiscoveryUpstreamData discoveryUpstream, final ShenyuDiscoveryConfig shenyuDiscoveryConfig) { + this.currentInstanceUpstream = discoveryUpstream; + this.currentInstanceUpstream.setProps("{\"warmupTime\":\"10\"}"); + this.discoveryConfig = new RegisterConfig(); + this.discoveryConfig.setServerLists(shenyuDiscoveryConfig.getServerList()); + this.discoveryConfig.setRegisterType(shenyuDiscoveryConfig.getType()); + this.discoveryConfig.setProps(Optional.ofNullable(shenyuDiscoveryConfig.getProps()).orElse(new Properties())); + this.path = shenyuDiscoveryConfig.getRegisterPath(); + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + LOGGER.info("unregister upstream server by jvm runtime hook"); + if (Objects.nonNull(discoveryService)) { + discoveryService.close(); + } + } + })); + } + + @Override + public void onApplicationEvent(final ContextRefreshedEvent event) { + try { + if (StringUtils.isBlank(discoveryConfig.getRegisterType()) || StringUtils.equalsIgnoreCase(discoveryConfig.getRegisterType(), "local")) { + return; + } + this.discoveryService = ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin(discoveryConfig.getRegisterType()); + discoveryConfig.getProps().put("watchPath", path); + discoveryService.init(discoveryConfig); + InstanceEntity instance = new InstanceEntity(); + instance.setStatus(currentInstanceUpstream.getStatus()); + instance.setWeight(currentInstanceUpstream.getWeight()); + URI uri = URI.create(currentInstanceUpstream.getProtocol() + currentInstanceUpstream.getUrl()); + instance.setPort(uri.getPort()); + instance.setHost(uri.getHost()); + instance.setAppName(discoveryConfig.getProps().getProperty("name")); + discoveryService.persistInstance(instance); + LOGGER.info("shenyu register into ShenyuDiscoveryService {} success", discoveryConfig.getRegisterType()); + } catch (Exception e) { + LOGGER.error("shenyu register into ShenyuDiscoveryService {} type find error", discoveryConfig.getRegisterType(), e); + throw new IllegalStateException(String.format("shenyu register into ShenyuDiscoveryService %s type find error", discoveryConfig.getRegisterType()), e); + } + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/ShenyuClientRegisterRepository.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/ShenyuClientRegisterRepository.java index 449a423..aed76e4 100644 --- a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/ShenyuClientRegisterRepository.java +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/ShenyuClientRegisterRepository.java @@ -19,6 +19,7 @@ import org.apache.shenyu.client.core.dto.ApiDocRegisterDTO; +import org.apache.shenyu.client.core.dto.McpToolsRegisterDTO; import org.apache.shenyu.client.core.dto.MetaDataRegisterDTO; import org.apache.shenyu.client.core.dto.URIRegisterDTO; import org.apache.shenyu.client.core.register.config.ShenyuRegisterCenterConfig; @@ -73,6 +74,14 @@ default void sendHeartbeat(URIRegisterDTO heartbeatDTO) { */ default void persistApiDoc(ApiDocRegisterDTO apiDocRegisterDTO) { } + + /** + * persistMcpTools. + * + * @param mcpToolsRegisterDTO mcpToolsRegisterDTO + */ + default void persistMcpTools(McpToolsRegisterDTO mcpToolsRegisterDTO) { + } /** * closeRepository. diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/type/DataType.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/type/DataType.java index f9eae28..3f64c61 100644 --- a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/type/DataType.java +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/type/DataType.java @@ -46,4 +46,9 @@ public enum DataType { * Discovery config type enum. */ DISCOVERY_CONFIG, + + /** + * Mcp tools type enum. + */ + MCP_TOOLS, } diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/RequestMethodUtils.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/RequestMethodUtils.java new file mode 100644 index 0000000..be4426d --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/RequestMethodUtils.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.utils; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Request method utils. + */ +public final class RequestMethodUtils { + + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private RequestMethodUtils() { + } + + /** + * Get supported method types from method. + * + * @param method method + * @return the list of method type + */ + public static List getRequestMethodTypes(final Method method) { + if (method.isAnnotationPresent(GetMapping.class)) { + return Collections.singletonList("GET"); + } + if (method.isAnnotationPresent(PostMapping.class)) { + return Collections.singletonList("POST"); + } + if (method.isAnnotationPresent(PutMapping.class)) { + return Collections.singletonList("PUT"); + } + if (method.isAnnotationPresent(DeleteMapping.class)) { + return Collections.singletonList("DELETE"); + } + if (method.isAnnotationPresent(PatchMapping.class)) { + return Collections.singletonList("PATCH"); + } + if (method.isAnnotationPresent(RequestMapping.class)) { + RequestMapping rm = method.getAnnotation(RequestMapping.class); + RequestMethod[] requestMethods = rm.method(); + if (requestMethods.length == 0) { + return Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"); + } + return Arrays.stream(requestMethods) + .map(Enum::name) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + /** + * Get the parameter position list. + * + * @param method method + * @return positionList + */ + public static List getParameterPositions(final Method method) { + List positions = new ArrayList<>(); + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + + for (Annotation[] annotations : parameterAnnotations) { + String position = "query"; + + for (Annotation annotation : annotations) { + if (annotation instanceof PathVariable) { + position = "path"; + break; + } else if (annotation instanceof RequestParam) { + position = "query"; + break; + } else if (annotation instanceof RequestBody) { + position = "body"; + break; + } else if (annotation instanceof RequestHeader) { + position = "header"; + break; + } else if (annotation instanceof CookieValue) { + position = "cookie"; + break; + } + } + + positions.add(position); + } + + return positions; + } + + /** + * Get the parameter names. + * + * @param method method + * @return the array of parameter names + */ + public static String[] getParameterNames(final Method method) { + Parameter[] parameters = method.getParameters(); + String[] namesFromDiscoverer = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + + String[] result = new String[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + String nameFromAnnotation = getNameFromSpringAnnotation(parameters[i].getAnnotations()); + + if (Objects.nonNull(nameFromAnnotation) && !nameFromAnnotation.isEmpty()) { + result[i] = nameFromAnnotation; + } else if (Objects.nonNull(namesFromDiscoverer) && namesFromDiscoverer.length > i) { + result[i] = namesFromDiscoverer[i]; + } else { + result[i] = "arg" + i; + } + } + return result; + } + + private static String getNameFromSpringAnnotation(final Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation instanceof RequestParam) { + RequestParam requestParam = (RequestParam) annotation; + if (!requestParam.name().isEmpty()) { + return requestParam.name(); + } + if (!requestParam.value().isEmpty()) { + return requestParam.value(); + } + } else if (annotation instanceof PathVariable) { + PathVariable pathVariable = (PathVariable) annotation; + if (!pathVariable.name().isEmpty()) { + return pathVariable.name(); + } + if (!pathVariable.value().isEmpty()) { + return pathVariable.value(); + } + } else if (annotation instanceof RequestHeader) { + RequestHeader requestHeader = (RequestHeader) annotation; + if (!requestHeader.name().isEmpty()) { + return requestHeader.name(); + } + if (!requestHeader.value().isEmpty()) { + return requestHeader.value(); + } + } else if (annotation instanceof CookieValue) { + CookieValue cookieValue = (CookieValue) annotation; + if (!cookieValue.name().isEmpty()) { + return cookieValue.name(); + } + if (!cookieValue.value().isEmpty()) { + return cookieValue.value(); + } + } + } + return null; + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/SystemInfoUtils.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/SystemInfoUtils.java new file mode 100644 index 0000000..60ebfea --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/SystemInfoUtils.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.utils; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.util.HashMap; +import java.util.Map; + +/** + * Java 8 compatible system information helper for heartbeat payloads. + */ +public final class SystemInfoUtils { + + private SystemInfoUtils() { + } + + /** + * Gets system info. + * + * @return system info json + */ + public static String getSystemInfo() { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + Map hostInfo = new HashMap<>(4); + hostInfo.put("arch", osBean.getArch()); + hostInfo.put("operatingSystem", osBean.getName()); + hostInfo.put("availableProcessors", osBean.getAvailableProcessors()); + hostInfo.put("systemLoadAverage", osBean.getSystemLoadAverage()); + return GsonUtils.getInstance().toJson(hostInfo); + } +} diff --git a/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/VersionUtils.java b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/VersionUtils.java new file mode 100644 index 0000000..09a9c36 --- /dev/null +++ b/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/VersionUtils.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.core.utils; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URL; +import java.security.CodeSource; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Version utils. + */ +public final class VersionUtils { + + private static final Logger LOG = LoggerFactory.getLogger(VersionUtils.class); + + private static final String VERSION = getVersion(VersionUtils.class, "1.0.0"); + + private static final String JAR = ".jar"; + + private VersionUtils() { + } + + /** + * Gets version. + * + * @return the version + */ + public static String getVersion() { + return VERSION; + } + + /** + * Gets version. + * + * @param cls the class + * @param defaultVersion the default version + * @return the version + */ + public static String getVersion(final Class cls, final String defaultVersion) { + String version = cls.getPackage().getImplementationVersion(); + if (StringUtils.isBlank(version)) { + version = cls.getPackage().getSpecificationVersion(); + } + if (StringUtils.isNoneBlank(version)) { + return version; + } + CodeSource codeSource = cls.getProtectionDomain().getCodeSource(); + if (Objects.isNull(codeSource)) { + LOG.info("No codeSource for class {} when getVersion, use default version {}", cls.getName(), defaultVersion); + return defaultVersion; + } + String file = codeSource.getLocation().getFile(); + if (Objects.nonNull(file) && file.endsWith(JAR)) { + file = file.substring(0, file.length() - 4); + int index = file.lastIndexOf('/'); + if (index >= 0) { + file = file.substring(index + 1); + } + index = file.indexOf("-"); + if (index >= 0) { + file = file.substring(index + 1); + } + while (StringUtils.isNoneBlank(file) && !Character.isDigit(file.charAt(0))) { + index = file.indexOf("-"); + if (index < 0) { + break; + } + file = file.substring(index + 1); + } + version = file; + } + return StringUtils.isBlank(version) ? defaultVersion : version; + } + + /** + * Check duplicate class resources. + * + * @param cls class + */ + public static void checkDuplicate(final Class cls) { + try { + String path = cls.getName().replace('.', '/') + ".class"; + Set files = readResources(path, cls); + if (files.size() > 1) { + String error = "Duplicate class " + path + " in " + files.size() + " jar " + files; + LOG.error("checkDuplicate error,{}", error); + } + } catch (Throwable e) { + LOG.error("checkDuplicate error,msg :{}", e.getMessage(), e); + } + } + + private static Set readResources(final String path, final Class cls) throws IOException { + Enumeration urls = cls.getClassLoader().getResources(path); + Set files = new HashSet<>(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + if (Objects.nonNull(url) && StringUtils.isNotEmpty(url.getFile())) { + files.add(url.getFile()); + } + } + return files; + } +} diff --git a/shenyu-client-mcp/pom.xml b/shenyu-client-mcp/pom.xml new file mode 100644 index 0000000..a0bad07 --- /dev/null +++ b/shenyu-client-mcp/pom.xml @@ -0,0 +1,41 @@ + + + + + + org.apache.shenyu + shenyu-client-java + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-client-mcp + pom + + + shenyu-client-mcp-common + shenyu-client-mcp-register + + + + + org.apache.shenyu + shenyu-client-core + ${project.version} + + + diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/pom.xml b/shenyu-client-mcp/shenyu-client-mcp-common/pom.xml new file mode 100644 index 0000000..ef3443a --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/pom.xml @@ -0,0 +1,56 @@ + + + + + + org.apache.shenyu + shenyu-client-mcp + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-client-mcp-common + + + + + io.swagger.core.v3 + swagger-annotations + ${swagger-core.version} + + + + io.swagger.core.v3 + swagger-models + ${swagger-core.version} + + + + org.springframework + spring-context + provided + + + + org.springframework + spring-web + provided + + + + + diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpHeader.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpHeader.java new file mode 100644 index 0000000..80e0a41 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpHeader.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.annotation; + +/** + * the headersMap. + */ +public @interface ShenyuMcpHeader { + + /** + * the key. + * + * @return key + */ + String key() default ""; + + /** + * the value. + * + * @return value + */ + String value() default ""; +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpRequestConfig.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpRequestConfig.java new file mode 100644 index 0000000..9a93a88 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpRequestConfig.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.annotation; + +/** + * the shenyuMcpRequestConfig. + */ +public @interface ShenyuMcpRequestConfig { + + /** + * headers. + * + * @return the headers + */ + ShenyuMcpHeader[] headers() default {}; + + /** + * bodyJson. + * @return the bodyJson + */ + String bodyToJson() default "false"; + +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpTool.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpTool.java new file mode 100644 index 0000000..e43c8be --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpTool.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.annotation; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The interface shenyu client. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface ShenyuMcpTool { + + + /** + * the openApi definition. + * + * @return definition + */ + OpenAPIDefinition definition() default @OpenAPIDefinition; + + /** + * the openApi operation. + * + * @return operation + */ + Operation operation() default @Operation; + + /** + * request config. + * + * @return the request config + */ + ShenyuMcpRequestConfig requestConfig() default @ShenyuMcpRequestConfig; + + /** + * Rule name string. + * + * @return the string + */ + String toolName() default ""; + + /** + * Desc string. + * + * @return String string + */ + String desc() default ""; + + /** + * Enabled boolean. + * + * @return the boolean + */ + boolean enabled() default true; + +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpToolParam.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpToolParam.java new file mode 100644 index 0000000..43ec715 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/annotation/ShenyuMcpToolParam.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.annotation; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ShenyuMcpToolParam. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ShenyuMcpToolParam { + + /** + * the parameter. + * + * @return parameter. + */ + Parameter parameter() default @Parameter; + +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/OpenApiConstants.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/OpenApiConstants.java new file mode 100644 index 0000000..2b8f7f0 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/OpenApiConstants.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.constants; + +/** + * openApi constants. + */ +public class OpenApiConstants { + + /** + * the version of openApi key. + */ + public static final String OPEN_API_VERSION_KEY = "openapi"; + + /** + * the info of openApi key. + */ + public static final String OPEN_API_INFO_KEY = "info"; + + /** + * the title of info key. + */ + public static final String OPEN_API_INFO_TITLE_KEY = "title"; + + /** + * the description of info key. + */ + public static final String OPEN_API_INFO_DESCRIPTION_KEY = "description"; + + /** + * the version of info key. + */ + public static final String OPEN_API_INFO_VERSION_KEY = "version"; + + /** + * the server of openApi key. + */ + public static final String OPEN_API_SERVER_KEY = "server"; + + /** + * the url of openApi server. + */ + public static final String OPEN_API_SERVER_URL_KEY = "url"; + + /** + * the path of openApi key. + */ + public static final String OPEN_API_PATH_KEY = "paths"; + + /** + * the summary of method key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_SUMMARY_KEY = "summary"; + + /** + * the description of method key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_DESCRIPTION_KEY = "description"; + + /** + * the operationId of method key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_OPERATION_ID_KEY = "operationId"; + + /** + * the tag of method key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_TAG_KEY = "tag"; + + /** + * the parameters of method key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_KEY = "parameters"; + + /** + * the name of parameter key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_NAME_KEY = "name"; + + /** + * the location of parameter key. + */ + public static final String OPEN_API_OPERATION_PATH_METHOD_PARAMETERS_IN_KEY = "in"; + + /** + * the description of parameter key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_DESCRIPTION_KEY = "description"; + + /** + * the required of parameter key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_REQUIRED_KEY = "required"; + + /** + * the schema of parameter key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_KEY = "schema"; + + /** + * the type of schema key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_TYPE_KEY = "type"; + + /** + * the defaultValue of schema key. + */ + public static final String OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_DEFAULT_VALUE_KEY = "defaultValue"; +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/RequestTemplateConstants.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/RequestTemplateConstants.java new file mode 100644 index 0000000..86542f9 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/RequestTemplateConstants.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.constants; + +/** + * requestTemplate constants. + */ +public class RequestTemplateConstants { + + /** + * the url key. + */ + public static final String URL_KEY = "url"; + + /** + * the method key. + */ + public static final String METHOD_KEY = "method"; + + /** + * the bodyJson key. + */ + public static final String BODY_JSON_KEY = "argsToJsonBody"; + + /** + * the headers key. + */ + public static final String HEADERS_KEY = "headers"; + + /** + * the argsPosition key. + */ + public static final String ARGS_POSITION_KEY = "argsPosition"; + + /** + * the requestTemplate key. + */ + public static final String REQUEST_TEMPLATE_KEY = "requestTemplate"; + +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/ShenyuToolConfigConstants.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/ShenyuToolConfigConstants.java new file mode 100644 index 0000000..6c20b52 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/constants/ShenyuToolConfigConstants.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.constants; + +/** + * shenyu tool config constants. + */ +public class ShenyuToolConfigConstants { + + /** + * the name key. + */ + public static final String NAME_KEY = "name"; + + /** + * the parameters key. + */ + public static final String PARAMETERS_KEY = "parameters"; + + /** + * the requestConfig key. + */ + public static final String REQUEST_CONFIG_KEY = "requestConfig"; + + /** + * the description key. + */ + public static final String DESCRIPTION_KEY = "description"; + + + +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpRequestConfig.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpRequestConfig.java new file mode 100644 index 0000000..63094ab --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpRequestConfig.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.dto; + +import java.util.HashMap; +import java.util.Map; + +/** + * the shenyu mcp request config object. + */ +public class ShenyuMcpRequestConfig { + + private Map headers = new HashMap<>(); + + private String bodyToJson; + + public ShenyuMcpRequestConfig() { + } + + /** + * get headers. + * + * @return headers + */ + public Map getHeaders() { + return headers; + } + + /** + * set headers. + * + * @param headers headers + */ + public void setHeaders(final Map headers) { + this.headers = headers; + } + + /** + * get bodyToJson. + * + * @return bodyToJson + */ + public String getBodyToJson() { + return bodyToJson; + } + + /** + * set bodyToJson. + * + * @param bodyToJson bodyToJson + */ + public void setBodyToJson(final String bodyToJson) { + this.bodyToJson = bodyToJson; + } +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpTool.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpTool.java new file mode 100644 index 0000000..80ef8dd --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/dto/ShenyuMcpTool.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.dto; + +import io.swagger.v3.oas.models.Operation; +import org.apache.shenyu.client.mcp.utils.OpenApiConvertorUtil; + +/** + * the shenyu mcp tool object. + */ +public class ShenyuMcpTool { + + private io.swagger.v3.oas.models.Operation operation; + + private ShenyuMcpRequestConfig requestConfig; + + private String toolName; + + private Boolean enable = true; + + private String method; + + public ShenyuMcpTool() { + } + + public ShenyuMcpTool(final org.apache.shenyu.client.mcp.common.annotation.ShenyuMcpTool shenyuMcpTool) { + this.operation = OpenApiConvertorUtil.convertOperation(shenyuMcpTool.operation()); + this.requestConfig = OpenApiConvertorUtil.convertRequestConfig(shenyuMcpTool.requestConfig()); + this.toolName = shenyuMcpTool.toolName(); + this.enable = shenyuMcpTool.enabled(); + } + + /** + * get operation. + * + * @return operation; + */ + public Operation getOperation() { + return operation; + } + + /** + * set operation. + * + * @param operation operation + */ + public void setOperation(final Operation operation) { + this.operation = operation; + } + + /** + * get requestConfig. + * + * @return requestConfig; + */ + public ShenyuMcpRequestConfig getRequestConfig() { + return requestConfig; + } + + /** + * set requestConfig. + * + * @param requestConfig requestConfig + */ + public void setRequestConfig(final ShenyuMcpRequestConfig requestConfig) { + this.requestConfig = requestConfig; + } + + /** + * get toolName. + * + * @return toolName; + */ + public String getToolName() { + return toolName; + } + + /** + * set toolName. + * + * @param toolName toolName + */ + public void setToolName(final String toolName) { + this.toolName = toolName; + } + + /** + * get enable. + * + * @return enable; + */ + public Boolean getEnable() { + return enable; + } + + /** + * set enable. + * + * @param enable enable + */ + public void setEnable(final Boolean enable) { + this.enable = enable; + } + + + /** + * get method. + * + * @return method; + */ + public String getMethod() { + return method; + } + + /** + * set method. + * + * @param method method + */ + public void setMethod(final String method) { + this.method = method; + } +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/eunm/McpParameterType.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/eunm/McpParameterType.java new file mode 100644 index 0000000..caaadc0 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/common/eunm/McpParameterType.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.common.eunm; + +import java.lang.reflect.Parameter; +import java.util.Objects; + +public enum McpParameterType { + STRING("string"), + INTEGER("integer"), + LONG("long"), + DOUBLE("double"), + FLOAT("float"), + BOOLEAN("boolean"), + OBJECT("object"), + ARRAY("array"); + + private final String typeName; + + McpParameterType(final String typeName) { + this.typeName = typeName; + } + + public String getTypeName() { + return typeName; + } + + /** + * get enum by java class. + * + * @param parameter parameter + * @return mcp parameter type + */ + public static McpParameterType fromParameter(final Parameter parameter) { + if (Objects.isNull(parameter)) { + return null; + } + Class clazz = parameter.getType(); + + if (clazz == String.class) { + return STRING; + } else if (clazz == Integer.class || clazz == int.class) { + return INTEGER; + } else if (clazz == Long.class || clazz == long.class) { + return LONG; + } else if (clazz == Double.class || clazz == double.class) { + return DOUBLE; + } else if (clazz == Float.class || clazz == float.class) { + return FLOAT; + } else if (clazz == Boolean.class || clazz == boolean.class) { + return BOOLEAN; + } else if (clazz.isArray() || java.util.Collection.class.isAssignableFrom(clazz)) { + return ARRAY; + } else if (clazz == Object.class) { + return OBJECT; + } else { + return OBJECT; + } + } +} \ No newline at end of file diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpOpenApiGenerator.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpOpenApiGenerator.java new file mode 100644 index 0000000..1446fa5 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpOpenApiGenerator.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.generator; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.parameters.Parameter; +import org.apache.shenyu.client.mcp.common.annotation.ShenyuMcpTool; +import org.apache.shenyu.client.mcp.common.constants.OpenApiConstants; + +import java.util.List; +import java.util.Objects; + +/** + * the openApi generator. + */ +public class McpOpenApiGenerator { + public static JsonObject generateOpenApiJson(final ShenyuMcpTool classMcpClient, + final org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool shenyuMcpTool, + final String url) { + JsonObject root = new JsonObject(); + root.addProperty(OpenApiConstants.OPEN_API_VERSION_KEY, "3.0.0"); + + // Info + JsonObject info = new JsonObject(); + OpenAPIDefinition definition = classMcpClient.definition(); + info.addProperty(OpenApiConstants.OPEN_API_INFO_TITLE_KEY, definition.info().title()); + info.addProperty(OpenApiConstants.OPEN_API_INFO_DESCRIPTION_KEY, definition.info().description()); + info.addProperty(OpenApiConstants.OPEN_API_INFO_VERSION_KEY, ""); + root.add(OpenApiConstants.OPEN_API_INFO_KEY, info); + + // Servers + JsonObject server = new JsonObject(); + root.add(OpenApiConstants.OPEN_API_SERVER_KEY, server); + server.addProperty(OpenApiConstants.OPEN_API_SERVER_URL_KEY, definition.servers()[0].url()); + + // Paths + JsonObject paths = new JsonObject(); + root.add(OpenApiConstants.OPEN_API_PATH_KEY, paths); + + String pathKey = url; + JsonObject pathMap = new JsonObject(); + paths.add(pathKey, pathMap); + + String methodType = shenyuMcpTool.getMethod(); + JsonObject methodMap = new JsonObject(); + pathMap.add(methodType, methodMap); + + methodMap.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_SUMMARY_KEY, ""); + methodMap.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_DESCRIPTION_KEY, ""); + methodMap.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_OPERATION_ID_KEY, ""); + methodMap.add(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_TAG_KEY, new JsonArray()); + + JsonArray parameters = new JsonArray(); + methodMap.add(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_KEY, parameters); + + List parameterList = shenyuMcpTool.getOperation().getParameters(); + + if (!parameterList.isEmpty()) { + + for (Parameter parameter : parameterList) { + JsonObject parameterObj = new JsonObject(); + parameterObj.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_NAME_KEY, parameter.getName()); + parameterObj.addProperty(OpenApiConstants.OPEN_API_OPERATION_PATH_METHOD_PARAMETERS_IN_KEY, parameter.getIn()); + parameterObj.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_DESCRIPTION_KEY, parameter.getDescription()); + parameterObj.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_REQUIRED_KEY, parameter.getRequired()); + + if (Objects.nonNull(parameter.getSchema())) { + JsonObject schemaMap = new JsonObject(); + schemaMap.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_TYPE_KEY, parameter.getSchema().getType()); + parameterObj.add(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_KEY, schemaMap); + } + parameters.add(parameterObj); + } + } + + return root; + } +} \ No newline at end of file diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java new file mode 100644 index 0000000..feea24f --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.generator; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.shenyu.client.mcp.common.constants.OpenApiConstants; +import org.apache.shenyu.client.mcp.common.constants.RequestTemplateConstants; +import org.apache.shenyu.client.mcp.common.dto.ShenyuMcpRequestConfig; + +import java.util.Objects; + + +/** + * the mcpRequestConfig generator. + */ +public class McpRequestConfigGenerator { + + public static JsonObject generateRequestConfig(final JsonObject openApiJson, final ShenyuMcpRequestConfig shenyuMcpRequestConfig) { + + // requestConfig + JsonObject root = new JsonObject(); + + // requestTemplate + JsonObject requestTemplate = new JsonObject(); + root.add(RequestTemplateConstants.REQUEST_TEMPLATE_KEY, requestTemplate); + + // url + JsonObject paths = openApiJson.get(OpenApiConstants.OPEN_API_PATH_KEY).getAsJsonObject(); + String path = null; + for (String methodKey : paths.keySet()) { + path = methodKey; + break; + } + requestTemplate.addProperty(RequestTemplateConstants.URL_KEY, path); + + // method + JsonObject method = paths.get(path).getAsJsonObject(); + String methodType = null; + for (String methodKey : method.keySet()) { + methodType = methodKey; + break; + } + requestTemplate.addProperty(RequestTemplateConstants.METHOD_KEY, methodType); + + // argsPosition + JsonObject argsPosition = new JsonObject(); + JsonObject methodTypeJson = method.getAsJsonObject(methodType); + JsonArray parameters = methodTypeJson.getAsJsonArray(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_KEY); + if (Objects.nonNull(parameters)) { + for (JsonElement parameter : parameters) { + JsonObject paramObj = parameter.getAsJsonObject(); + + if (paramObj.has(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_NAME_KEY) + && paramObj.has(OpenApiConstants.OPEN_API_OPERATION_PATH_METHOD_PARAMETERS_IN_KEY)) { + + String name = paramObj.get(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_NAME_KEY).getAsString(); + String inValue = paramObj.get(OpenApiConstants.OPEN_API_OPERATION_PATH_METHOD_PARAMETERS_IN_KEY).getAsString(); + + argsPosition.addProperty(name, inValue); + } + } + } + // Keep root-level argsPosition as canonical format used by gateway parser. + root.add(RequestTemplateConstants.ARGS_POSITION_KEY, argsPosition.deepCopy()); + // Keep requestTemplate-level argsPosition for backward compatibility. + requestTemplate.add(RequestTemplateConstants.ARGS_POSITION_KEY, argsPosition); + + // argsToJsonBody + requestTemplate.addProperty(RequestTemplateConstants.BODY_JSON_KEY, shenyuMcpRequestConfig.getBodyToJson()); + + // header + JsonArray headers = new JsonArray(); + shenyuMcpRequestConfig.getHeaders().forEach((key, value) -> { + JsonObject headerJson = new JsonObject(); + headerJson.addProperty("key", key); + headerJson.addProperty("value", value); + headers.add(headerJson); + }); + requestTemplate.add(RequestTemplateConstants.HEADERS_KEY, headers); + + return root; + } +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpToolsRegisterDTOGenerator.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpToolsRegisterDTOGenerator.java new file mode 100644 index 0000000..3638029 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpToolsRegisterDTOGenerator.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.generator; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.shenyu.client.mcp.common.constants.OpenApiConstants; +import org.apache.shenyu.client.mcp.common.constants.ShenyuToolConfigConstants; +import org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool; +import org.apache.shenyu.client.core.dto.McpToolsRegisterDTO; + +import java.util.Objects; + +/** + * the mcpToolsRegisterDTO generator. + */ +public class McpToolsRegisterDTOGenerator { + + public static McpToolsRegisterDTO generateRegisterDTO(final ShenyuMcpTool shenyuMcpTool, final JsonObject openApiJsonObject, + final String url, final String namespaceId) { + JsonObject root = new JsonObject(); + + JsonObject paths = openApiJsonObject.getAsJsonObject(OpenApiConstants.OPEN_API_PATH_KEY); + JsonObject path = paths.getAsJsonObject(url); + JsonObject method = path.getAsJsonObject(shenyuMcpTool.getMethod()); + JsonArray parameters = method.getAsJsonArray(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_KEY); + + JsonObject requestConfig = McpRequestConfigGenerator.generateRequestConfig(openApiJsonObject, shenyuMcpTool.getRequestConfig()); + root.addProperty(ShenyuToolConfigConstants.REQUEST_CONFIG_KEY, requestConfig.toString()); + + root.addProperty(ShenyuToolConfigConstants.NAME_KEY, shenyuMcpTool.getToolName()); + root.add(ShenyuToolConfigConstants.PARAMETERS_KEY, parameterFormatting(parameters)); + + root.addProperty(ShenyuToolConfigConstants.DESCRIPTION_KEY, shenyuMcpTool.getOperation().getDescription()); + + McpToolsRegisterDTO mcpToolsRegisterDTO = new McpToolsRegisterDTO(); + mcpToolsRegisterDTO.setNamespaceId(namespaceId); + mcpToolsRegisterDTO.setMcpConfig(root.toString()); + return mcpToolsRegisterDTO; + } + + public static JsonArray parameterFormatting(final JsonArray parameters) { + + parameters.forEach(parameter -> { + JsonObject paramObj = parameter.getAsJsonObject(); + + JsonElement schema = paramObj.remove(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_KEY); + if (Objects.nonNull(schema) && schema.isJsonObject()) { + JsonObject schemaObj = schema.getAsJsonObject(); + JsonElement typeElement = schemaObj.get(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_TYPE_KEY); + + if (Objects.nonNull(typeElement) && typeElement.isJsonPrimitive()) { + String type = typeElement.getAsString(); + paramObj.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_SCHEMA_TYPE_KEY, type); + } + } + if (!paramObj.has(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_DESCRIPTION_KEY)) { + paramObj.addProperty(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_DESCRIPTION_KEY, + paramObj.get(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_NAME_KEY).getAsString()); + } + paramObj.remove(OpenApiConstants.OPEN_API_OPERATION_PATH_METHOD_PARAMETERS_IN_KEY); + }); + return parameters; + + } +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/utils/OpenApiConvertorUtil.java b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/utils/OpenApiConvertorUtil.java new file mode 100644 index 0000000..fcbb5f1 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/utils/OpenApiConvertorUtil.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp.utils; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.parameters.Parameter; +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.mcp.common.annotation.ShenyuMcpHeader; +import org.apache.shenyu.client.mcp.common.annotation.ShenyuMcpRequestConfig; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * OpenApiConvertorUtil. + */ +public class OpenApiConvertorUtil { + + public static io.swagger.v3.oas.models.parameters.Parameter convertParameter(final io.swagger.v3.oas.annotations.Parameter annotation) { + + io.swagger.v3.oas.models.parameters.Parameter parameter = new io.swagger.v3.oas.models.parameters.Parameter(); + + if (!Objects.nonNull(annotation)) { + return parameter; + } + + parameter.setName(annotation.name()); + parameter.setIn(annotation.in().toString().toLowerCase()); + parameter.setDescription(annotation.description()); + parameter.setRequired(annotation.required()); + + io.swagger.v3.oas.annotations.media.Schema schemaAnn = annotation.schema(); + if (!schemaAnn.implementation().equals(Void.class) + || StringUtils.isNoneBlank(schemaAnn.type()) + || StringUtils.isNoneBlank(schemaAnn.format())) { + io.swagger.v3.oas.models.media.Schema schema = new io.swagger.v3.oas.models.media.Schema<>(); + if (StringUtils.isNoneBlank(schemaAnn.type())) { + schema.setType(schemaAnn.type()); + } + parameter.setSchema(schema); + } + return parameter; + } + + public static Operation convertOperation(final io.swagger.v3.oas.annotations.Operation operationAnnotation) { + Operation operation = new Operation(); + + if (Objects.isNull(operationAnnotation)) { + return operation; + } + + operation.setSummary(operationAnnotation.summary()); + operation.setDescription(operationAnnotation.description()); + operation.setDeprecated(operationAnnotation.deprecated()); + operation.setOperationId(operationAnnotation.operationId()); + + // Tags + if (Objects.nonNull(operationAnnotation.tags()) && operationAnnotation.tags().length > 0) { + operation.setTags(Arrays.asList(operationAnnotation.tags())); + } + + // Parameters + if (Objects.nonNull(operationAnnotation.parameters()) && operationAnnotation.parameters().length > 0) { + List parameters = Arrays.stream(operationAnnotation.parameters()) + .map(OpenApiConvertorUtil::convertParameter) + .collect(Collectors.toList()); + operation.setParameters(parameters); + } + + return operation; + } + + public static org.apache.shenyu.client.mcp.common.dto.ShenyuMcpRequestConfig convertRequestConfig(final ShenyuMcpRequestConfig requestConfigAnnotation) { + org.apache.shenyu.client.mcp.common.dto.ShenyuMcpRequestConfig requestConfigObject = new org.apache.shenyu.client.mcp.common.dto.ShenyuMcpRequestConfig(); + ShenyuMcpHeader[] headersAnnotation = requestConfigAnnotation.headers(); + Map headersObject = requestConfigObject.getHeaders(); + for (ShenyuMcpHeader shenyuMcpHeader : headersAnnotation) { + headersObject.put(shenyuMcpHeader.key(), shenyuMcpHeader.value()); + } + requestConfigObject.setBodyToJson(requestConfigAnnotation.bodyToJson()); + return requestConfigObject; + } +} diff --git a/shenyu-client-mcp/shenyu-client-mcp-register/pom.xml b/shenyu-client-mcp/shenyu-client-mcp-register/pom.xml new file mode 100644 index 0000000..9516894 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-register/pom.xml @@ -0,0 +1,56 @@ + + + + + + org.apache.shenyu + shenyu-client-mcp + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-client-mcp-register + + + + + org.apache.shenyu + shenyu-client-mcp-common + ${project.version} + + + + org.springframework + spring-aop + provided + + + + org.springframework + spring-context + provided + + + + org.springframework + spring-web + provided + + + + + diff --git a/shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java b/shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java new file mode 100644 index 0000000..8ec24c8 --- /dev/null +++ b/shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.client.mcp; + +import com.google.gson.JsonObject; +import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.core.client.AbstractContextRefreshedEventListener; +import org.apache.shenyu.client.core.utils.RequestMethodUtils; +import org.apache.shenyu.client.mcp.common.annotation.ShenyuMcpTool; +import org.apache.shenyu.client.mcp.common.annotation.ShenyuMcpToolParam; +import org.apache.shenyu.client.mcp.common.eunm.McpParameterType; +import org.apache.shenyu.client.mcp.generator.McpOpenApiGenerator; +import org.apache.shenyu.client.mcp.generator.McpToolsRegisterDTOGenerator; +import org.apache.shenyu.client.mcp.utils.OpenApiConvertorUtil; +import org.apache.shenyu.client.core.dto.McpToolsRegisterDTO; +import org.apache.shenyu.client.core.dto.MetaDataRegisterDTO; +import org.apache.shenyu.client.core.dto.URIRegisterDTO; +import org.apache.shenyu.client.core.enums.ApiHttpMethodEnum; +import org.apache.shenyu.client.core.enums.RpcTypeEnum; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.javatuples.Sextet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Mcp service event Listener. + */ +public class McpServiceEventListener extends AbstractContextRefreshedEventListener { + + private static final Logger log = LoggerFactory.getLogger(McpServiceEventListener.class); + + private final Environment env; + + /** + * Instantiates a new context refreshed event listener. + * + * @param clientConfig the shenyu client config + * @param shenyuClientRegisterRepository the shenyuClientRegisterRepository + * @param env the spring environment + */ + public McpServiceEventListener(final ShenyuClientConfig clientConfig, + final ShenyuClientRegisterRepository shenyuClientRegisterRepository, + final Environment env) { + super(clientConfig, shenyuClientRegisterRepository); + this.env = env; + } + + @Override + protected Sextet buildApiDocSextet(final Method method, final Annotation annotation, final Map beans) { + return null; + } + + @Override + protected Map getBeans(final ApplicationContext context) { + Map controllerBeans = context.getBeansWithAnnotation(Controller.class); + return controllerBeans.entrySet().stream() + .filter(entry -> { + Object bean = entry.getValue(); + + Class targetClass = AopUtils.getTargetClass(bean); + + return targetClass.isAnnotationPresent(ShenyuMcpTool.class); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + protected URIRegisterDTO buildURIRegisterDTO(final ApplicationContext context, final Map beans, final String namespaceId) { + return null; + } + + @Override + protected String getClientName() { + return RpcTypeEnum.MCP.getName(); + } + + @Override + protected void handleClass(final Class clazz, final Object bean, final ShenyuMcpTool beanShenyuClient, final String superPath) { + } + + @Override + protected void handleMethod(final Object bean, final Class clazz, final ShenyuMcpTool classMcpClient, final Method method, final String superPath) { + ShenyuMcpTool methodMcpClient; + if (method.isAnnotationPresent(ShenyuMcpTool.class)) { + methodMcpClient = method.getAnnotation(ShenyuMcpTool.class); + } else { + return; + } + + Operation operation = OpenApiConvertorUtil.convertOperation(methodMcpClient.operation()); + + List parameters; + String methodType = methodMcpClient.operation().method(); + + if (ArrayUtils.isNotEmpty(methodMcpClient.operation().parameters())) { + parameters = Arrays.stream(methodMcpClient.operation().parameters()) + .map(OpenApiConvertorUtil::convertParameter) + .collect(Collectors.toCollection(ArrayList::new)); + } else { + parameters = new ArrayList<>(); + } + + // inject default ToolDescription without shenyuMcpTool operation description configuration + String toolDescription = injectToolDescription(operation.getDescription(), method); + + // inject default MethodType without ShenyuMcpTool operation method configuration + methodType = injectMethodType(methodType, method); + + // inject default Parameters without ShenyuMcpTool operation parameters configuration + injectParameter(parameters, method); + + operation.setDescription(toolDescription); + operation.setParameters(parameters); + + org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool shenyuMcpToolMethod = new org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool(methodMcpClient); + + // inject default ToolDescription without shenyuMcpTool operation description configuration + String toolName = injectToolName(methodMcpClient.toolName(), method); + + shenyuMcpToolMethod.setOperation(operation); + shenyuMcpToolMethod.setMethod(methodType); + shenyuMcpToolMethod.setToolName(toolName); + + List namespaceIds = this.getNamespace(); + List mergeUrls = findMergeUrl(clazz, method); + mergeUrls.forEach(url -> { + namespaceIds.forEach(namespaceId -> getPublisher().publishEvent( + buildMcpToolsRegisterDTO(bean, clazz, classMcpClient, shenyuMcpToolMethod, + superPath, method, url, namespaceId))); + }); + } + + private String injectToolName(final String toolName, final Method method) { + if (StringUtils.isNoneBlank(toolName)) { + return toolName; + } + return method.getName(); + } + + private String injectToolDescription(final String description, final Method method) { + if (StringUtils.isNoneBlank(description)) { + return description; + } + return method.getName(); + } + + private String injectMethodType(final String methodType, final Method method) { + if (StringUtils.isNoneBlank(methodType)) { + return methodType; + } + List requestMethodTypes = RequestMethodUtils.getRequestMethodTypes(method); + if (requestMethodTypes.size() != 1) { + log.warn("Method [{}] in class [{}] has no restful mapping annotation; defaulting to GET", + method.getName(), method.getDeclaringClass().getName()); + return "GET"; + } else { + return requestMethodTypes.get(0); + } + } + + private void injectParameter(final List parametersList, final Method method) { + if (!parametersList.isEmpty()) { + return; + } + List parameterPositions = RequestMethodUtils.getParameterPositions(method); + String[] parameterNames = RequestMethodUtils.getParameterNames(method); + Parameter[] parameters = method.getParameters(); + for (int i = 0; i < parameters.length; i++) { + if (parameters[i].isAnnotationPresent(ShenyuMcpToolParam.class)) { + ShenyuMcpToolParam mcpToolParam = parameters[i].getAnnotation(ShenyuMcpToolParam.class); + if (Objects.nonNull(mcpToolParam.parameter())) { + io.swagger.v3.oas.models.parameters.Parameter parameterObject = OpenApiConvertorUtil.convertParameter(mcpToolParam.parameter()); + // inject in + if (StringUtils.isBlank(parameterObject.getIn())) { + parameterObject.setIn(parameterPositions.get(i)); + } + // inject required + if (Objects.isNull(parameterObject.getRequired())) { + parameterObject.setRequired(false); + } + // inject name + if (StringUtils.isBlank(parameterObject.getName())) { + parameterObject.setName(parameterNames[i]); + } + // inject description + if (StringUtils.isBlank(parameterObject.getDescription())) { + parameterObject.setDescription(parameterNames[i]); + } + // inject type + if (Objects.isNull(parameterObject.getSchema()) || StringUtils.isBlank(parameterObject.getSchema().getType())) { + Schema schema = new Schema<>(); + McpParameterType parameterType = McpParameterType.fromParameter(parameters[i]); + schema.setType(parameterType.getTypeName()); + + parameterObject.setSchema(schema); + } + parametersList.add(parameterObject); + } + + } + } + } + + private List findMergeUrl(final Class clazz, final Method method) { + + List classPaths = Collections.emptyList(); + RequestMapping classMapping = AnnotatedElementUtils.findMergedAnnotation(clazz, RequestMapping.class); + if (Objects.nonNull(classMapping)) { + String[] paths = classMapping.path(); + if (paths.length > 0) { + classPaths = Arrays.asList(paths); + } else { + String[] values = classMapping.value(); + if (values.length > 0) { + classPaths = Arrays.asList(values); + } + } + } + List methodPaths = Collections.emptyList(); + RequestMapping methodMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); + if (Objects.nonNull(methodMapping)) { + String[] paths = methodMapping.path(); + if (paths.length > 0) { + methodPaths = Arrays.asList(paths); + } else { + String[] values = methodMapping.value(); + if (values.length > 0) { + methodPaths = Arrays.asList(values); + } + } + } + if (classPaths.isEmpty()) { + classPaths = Collections.singletonList(""); + } + if (methodPaths.isEmpty()) { + methodPaths = Collections.singletonList(""); + } + + List combinedPaths = new ArrayList<>(); + final String servletPath = StringUtils.defaultString(this.env.getProperty("spring.mvc.servlet.path"), ""); + final String servletContextPath = StringUtils.defaultString(this.env.getProperty("server.servlet.context-path"), ""); + final String rootPath = concatPaths(servletContextPath, servletPath); + for (String cp : classPaths) { + for (String mp : methodPaths) { + String path = concatPaths(cp, mp); + String prefix = concatPaths(getContextPath(), rootPath); + String finalPath = concatPaths(prefix, path); + combinedPaths.add(finalPath); + } + } + return combinedPaths; + } + + private static String concatPaths(final String path1, final String path2) { + if (path1.endsWith("/") && path2.startsWith("/")) { + return path1 + path2.substring(1); + } else if (!path1.endsWith("/") && !path2.startsWith("/")) { + if (path1.isEmpty() || path2.isEmpty()) { + return path1 + path2; + } + return path1 + "/" + path2; + } else { + return path1 + path2; + } + } + + @Override + protected String buildApiSuperPath(final Class clazz, final ShenyuMcpTool beanShenyuClient) { + Server[] servers = beanShenyuClient.definition().servers(); + if (servers.length == 0) { + return ""; + } + if (servers.length != 1) { + log.warn("The shenyuMcp service supports only a single server entry. Please ensure that only one server is configured"); + } + String superUrl = servers[0].url(); + if (StringUtils.isNotEmpty(superUrl)) { + return superUrl; + } + return ""; + } + + @Override + protected Class getAnnotationType() { + return ShenyuMcpTool.class; + } + + @Override + protected String buildApiPath(final Method method, final String superPath, final ShenyuMcpTool methodShenyuClient) { + return null; + } + + @Override + protected MetaDataRegisterDTO buildMetaDataDTO(final Object bean, final ShenyuMcpTool shenyuClient, final String path, + final Class clazz, final Method method, final String namespaceId) { + ShenyuMcpTool methodClient = AnnotatedElementUtils.findMergedAnnotation(method, ShenyuMcpTool.class); + String desc = shenyuClient.desc(); + String configRuleName = null; + if (Objects.nonNull(methodClient)) { + configRuleName = injectToolName(methodClient.toolName(), method); + } + String ruleName = ("".equals(configRuleName)) ? path : configRuleName; + String methodName = method.getName(); + Class[] parameterTypesClazz = method.getParameterTypes(); + String parameterTypes = Arrays.stream(parameterTypesClazz).map(Class::getName) + .collect(Collectors.joining(",")); + String serviceName = shenyuClient.definition().info().title(); + return MetaDataRegisterDTO.builder() + .appName(this.getAppName()) + .serviceName(serviceName) + .methodName(methodName) + .contextPath(this.getContextPath()) + .path(path) + .port(Integer.parseInt(super.getPort())) + .host(super.getHost()) + .ruleName(ruleName) + .pathDesc(desc) + .parameterTypes(parameterTypes) + .rpcType(RpcTypeEnum.MCP.getName()) + .namespaceId(namespaceId) + .build(); + } + + private McpToolsRegisterDTO buildMcpToolsRegisterDTO(final Object bean, final Class clazz, + final ShenyuMcpTool classShenyuClient, + final org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool shenyuMcpTool, + final String superPath, final Method method, + final String url, final String namespaceId) { + validateClientConfig(shenyuMcpTool, url); + JsonObject openApiJson = McpOpenApiGenerator.generateOpenApiJson(classShenyuClient, shenyuMcpTool, url); + McpToolsRegisterDTO mcpToolsRegisterDTO = McpToolsRegisterDTOGenerator.generateRegisterDTO(shenyuMcpTool, openApiJson, url, namespaceId); + MetaDataRegisterDTO metaDataRegisterDTO = buildMetaDataDTO(bean, classShenyuClient, url, clazz, method, namespaceId); + metaDataRegisterDTO.setEnabled(shenyuMcpTool.getEnable()); + mcpToolsRegisterDTO.setMetaDataRegisterDTO(metaDataRegisterDTO); + return mcpToolsRegisterDTO; + } + + private void validateClientConfig(final org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool methodShenyuClient, final String url) { + if (StringUtils.isBlank(url)) { + log.error("OpenAPI pathKey is null or empty, please check OpenApiConfig"); + throw new IllegalArgumentException("OpenAPI pathKey cannot be null or empty"); + } + + if (StringUtils.isBlank(methodShenyuClient.getMethod())) { + log.error("OpenAPI methodType is null or empty, please check OpenApiConfig"); + throw new IllegalArgumentException("OpenAPI methodType cannot be null or empty"); + } + } + +} diff --git a/shenyu-register-client-beat/pom.xml b/shenyu-register-client-beat/pom.xml new file mode 100644 index 0000000..fab2213 --- /dev/null +++ b/shenyu-register-client-beat/pom.xml @@ -0,0 +1,57 @@ + + + + + org.apache.shenyu + shenyu-client-java + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + + shenyu-register-client-beat + + + + org.apache.shenyu + shenyu-client-core + ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + + + org.springframework.boot + spring-boot-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + diff --git a/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java b/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java new file mode 100644 index 0000000..19cb81d --- /dev/null +++ b/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.register.client.beat; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.core.utils.ShenyuThreadFactory; +import org.apache.shenyu.client.core.config.ShenyuConfig; +import org.apache.shenyu.client.core.constant.Constants; +import org.apache.shenyu.client.core.constant.InstanceTypeConstants; +import org.apache.shenyu.client.core.utils.AesUtils; +import org.apache.shenyu.client.core.utils.GsonUtils; +import org.apache.shenyu.client.core.utils.IpUtils; +import org.apache.shenyu.client.core.utils.SystemInfoUtils; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class HeartbeatListener { + + private static final Logger LOG = LoggerFactory.getLogger(HeartbeatListener.class); + + private static final int INITIAL_DELAY = 0; + + private static final int CHECK_PERIOD = 5; + + private ScheduledThreadPoolExecutor executor; + + private final ShenyuConfig shenyuConfig; + + private String username; + + private String password; + + private List serverList; + + /** + * server -> accessToken. + */ + private LoadingCache accessToken; + + private ShenyuBootstrapHeartBeatConfig config; + + public HeartbeatListener(final ShenyuBootstrapHeartBeatConfig config, final ShenyuConfig shenyuConfig, final ServerProperties serverProperties) { + executor = new ScheduledThreadPoolExecutor(1, ShenyuThreadFactory.create("scheduled-instance-task", false)); + this.shenyuConfig = shenyuConfig; + LOG.info("Web server initialized on port {}, starting heartbeat reporter", serverProperties.getPort()); + this.username = config.getProps().getProperty(Constants.USER_NAME); + this.password = config.getProps().getProperty(Constants.PASS_WORD); + this.config = config; + String secretKey = config.getProps().getProperty(Constants.AES_SECRET_KEY); + String secretIv = config.getProps().getProperty(Constants.AES_SECRET_IV); + if (StringUtils.isNotBlank(secretKey) && StringUtils.isNotBlank(secretIv)) { + this.password = AesUtils.cbcEncrypt(secretKey, secretIv, password); + } + this.serverList = Lists.newArrayList(Splitter.on(",").split(config.getServerLists())); + this.accessToken = Caffeine.newBuilder() + //see org.apache.shenyu.admin.config.properties.JwtProperties#expiredSeconds + .expireAfterWrite(24L, TimeUnit.HOURS) + .build(new CacheLoader() { + @Override + public @Nullable String load(@NonNull final String server) { + try { + Optional login = RegisterUtils.doLogin(username, password, server.concat(Constants.LOGIN_PATH)); + return login.map(String::valueOf).orElse(null); + } catch (Exception e) { + LOG.error("Login admin url :{} is fail, will retry. cause: {} ", server, e.getMessage()); + return null; + } + } + }); + executor.scheduleAtFixedRate(() -> { + InstanceBeatInfoDTO instanceBeatInfoDTO = new InstanceBeatInfoDTO(); + instanceBeatInfoDTO.setInstancePort(String.valueOf(serverProperties.getPort())); + instanceBeatInfoDTO.setInstanceIp(IpUtils.getHost()); + instanceBeatInfoDTO.setNamespaceId(shenyuConfig.getNamespace()); + instanceBeatInfoDTO.setInstanceInfo(SystemInfoUtils.getSystemInfo()); + instanceBeatInfoDTO.setInstanceType(InstanceTypeConstants.BOOTSTRAP_INSTANCE_INFO); + sendHeartbeat(instanceBeatInfoDTO); + }, INITIAL_DELAY, CHECK_PERIOD, TimeUnit.SECONDS + ); + } + + private void sendHeartbeat(final InstanceBeatInfoDTO instanceBeatInfoDTO) { + int i = 0; + for (String server : serverList) { + i++; + String concat = server.concat(Constants.BEAT_URI_PATH); + try { + String accessToken = this.accessToken.get(server); + if (StringUtils.isBlank(accessToken)) { + throw new NullPointerException("accessToken is null"); + } + RegisterUtils.doHeartBeat(GsonUtils.getInstance().toJson(instanceBeatInfoDTO), concat, Constants.HEARTBEAT, accessToken); + } catch (Exception e) { + LOG.error("HeartBeat admin url :{} is fail, will retry.", server, e); + if (i == serverList.size()) { + throw new RuntimeException(e); + } + } + } + } + + @EventListener(ContextClosedEvent.class) + public void onShutdown() { + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/ShenyuBootstrapHeartBeatConfig.java b/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/ShenyuBootstrapHeartBeatConfig.java new file mode 100644 index 0000000..5f9286f --- /dev/null +++ b/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/ShenyuBootstrapHeartBeatConfig.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.register.client.beat; + +import org.apache.shenyu.client.core.register.config.PropertiesConfig; + +import java.util.Properties; + +public class ShenyuBootstrapHeartBeatConfig extends PropertiesConfig { + + private String serverLists; + + public ShenyuBootstrapHeartBeatConfig() { + + } + + public ShenyuBootstrapHeartBeatConfig( + final String serverLists, + final Properties props) { + this.serverLists = serverLists; + this.setProps(props); + } + + /** + * getServerLists. + * + * @return String + */ + public String getServerLists() { + return serverLists; + } + + /** + * setServerLists. + * + * @param serverLists serverLists + */ + public void setServerLists(final String serverLists) { + this.serverLists = serverLists; + } +} diff --git a/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java b/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java new file mode 100644 index 0000000..5cbf556 --- /dev/null +++ b/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.register.client.beat; + +import org.apache.shenyu.client.core.config.ShenyuConfig; +import org.apache.shenyu.client.core.constant.Constants; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.autoconfigure.web.ServerProperties; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; + +@ExtendWith(MockitoExtension.class) +class HeartbeatListenerTest { + + private HeartbeatListener heartbeatListener; + + private ShenyuBootstrapHeartBeatConfig config; + + private ShenyuConfig shenyuConfig; + + private ServerProperties serverProperties; + + @BeforeEach + void setUp() { + config = createMockConfig(); + shenyuConfig = createMockShenyuConfig(); + serverProperties = createMockServerProperties(); + } + + private ShenyuBootstrapHeartBeatConfig createMockConfig() { + + Properties props = new Properties(); + props.setProperty(Constants.USER_NAME, "admin"); + props.setProperty(Constants.PASS_WORD, "123456"); + props.setProperty(Constants.AES_SECRET_KEY, ""); + props.setProperty(Constants.AES_SECRET_IV, ""); + + ShenyuBootstrapHeartBeatConfig config = new ShenyuBootstrapHeartBeatConfig(); + config.setServerLists("http://localhost:9095,http://localhost:9096"); + config.setProps(props); + + return config; + } + + private ShenyuConfig createMockShenyuConfig() { + ShenyuConfig config = new ShenyuConfig(); + config.setNamespace("shenyu"); + return config; + } + + private ServerProperties createMockServerProperties() { + + ServerProperties properties = new ServerProperties(); + properties.setPort(8080); + + return properties; + } + + @Test + void testHeartbeatListenerCreation() { + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.of("mock-token")); + + assertDoesNotThrow(() -> { + heartbeatListener = new HeartbeatListener(config, shenyuConfig, serverProperties); + }); + + assertNotNull(heartbeatListener); + } + } + + @Test + void testHeartbeatListenerWithEncryption() { + + Properties props = new Properties(); + props.setProperty(Constants.USER_NAME, "admin"); + props.setProperty(Constants.PASS_WORD, "123456"); + props.setProperty(Constants.AES_SECRET_KEY, "2095132720951327"); + props.setProperty(Constants.AES_SECRET_IV, "6859932669599326"); + + ShenyuBootstrapHeartBeatConfig configWithEncryption = new ShenyuBootstrapHeartBeatConfig(); + configWithEncryption.setServerLists("http://localhost:9095"); + configWithEncryption.setProps(props); + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.of("mock-token")); + + assertDoesNotThrow(() -> { + heartbeatListener = new HeartbeatListener(configWithEncryption, shenyuConfig, serverProperties); + }); + + assertNotNull(heartbeatListener); + } + } + + @Test + void testSendHeartbeatSuccess() throws Exception { + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.of("mock-token")); + + registerUtilsMockedStatic.when(() -> RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString())) + .then(invocation -> null); + + heartbeatListener = new HeartbeatListener(config, shenyuConfig, serverProperties); + + // Use reflection to access private method sendHeartbeat + java.lang.reflect.Method sendHeartbeatMethod = HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO.class); + sendHeartbeatMethod.setAccessible(true); + + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO beatInfo = + new org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO(); + beatInfo.setInstancePort("8080"); + beatInfo.setInstanceIp("127.0.0.1"); + beatInfo.setNamespaceId("shenyu"); + + assertDoesNotThrow(() -> { + try { + sendHeartbeatMethod.invoke(heartbeatListener, beatInfo); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + // Wait a bit to allow the heartbeat to be processed + Thread.sleep(100); + + // Should be called for both servers in serverList + registerUtilsMockedStatic.verify(() -> RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString()), + Mockito.times(2)); + } + } + + @Test + void testSendHeartbeatWithLoginFailure() throws Exception { + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.empty()); + + heartbeatListener = new HeartbeatListener(config, shenyuConfig, serverProperties); + + // Use reflection to access private method sendHeartbeat + java.lang.reflect.Method sendHeartbeatMethod = HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO.class); + sendHeartbeatMethod.setAccessible(true); + + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO beatInfo = + new org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO(); + + // Should throw RuntimeException due to login failure + try { + sendHeartbeatMethod.invoke(heartbeatListener, beatInfo); + } catch (Exception e) { + assertTrue(e.getCause() instanceof RuntimeException); + } + } + } + + @Test + void testOnShutdown() throws Exception { + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.of("mock-token")); + + heartbeatListener = new HeartbeatListener(config, shenyuConfig, serverProperties); + + // Use reflection to access the executor + Field executorField = HeartbeatListener.class.getDeclaredField("executor"); + executorField.setAccessible(true); + ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) executorField.get(heartbeatListener); + + assertNotNull(executor); + assertTrue(!executor.isShutdown()); + + heartbeatListener.onShutdown(); + + // Wait a bit for shutdown to complete + Thread.sleep(100); + + assertTrue(executor.isShutdown()); + } + } + + @Test + void testConfigurationValues() throws Exception { + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.of("mock-token")); + + heartbeatListener = new HeartbeatListener(config, shenyuConfig, serverProperties); + + // Use reflection to check private fields + Field usernameField = HeartbeatListener.class.getDeclaredField("username"); + usernameField.setAccessible(true); + String username = (String) usernameField.get(heartbeatListener); + assertEquals("admin", username); + + Field passwordField = HeartbeatListener.class.getDeclaredField("password"); + passwordField.setAccessible(true); + String password = (String) passwordField.get(heartbeatListener); + assertEquals("123456", password); + + Field serverListField = HeartbeatListener.class.getDeclaredField("serverList"); + serverListField.setAccessible(true); + @SuppressWarnings("unchecked") + java.util.List serverList = (java.util.List) serverListField.get(heartbeatListener); + assertEquals(2, serverList.size()); + assertTrue(serverList.contains("http://localhost:9095")); + assertTrue(serverList.contains("http://localhost:9096")); + } + } + + @Test + void testHeartbeatWithMultipleServersOneFailure() throws Exception { + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.of("mock-token")); + + // First server call fails, second succeeds + registerUtilsMockedStatic.when(() -> RegisterUtils.doHeartBeat(anyString(), + Mockito.contains("localhost:9095"), anyString(), anyString())) + .thenThrow(new RuntimeException("Connection failed")); + + registerUtilsMockedStatic.when(() -> RegisterUtils.doHeartBeat(anyString(), + Mockito.contains("localhost:9096"), anyString(), anyString())) + .then(invocation -> null); + + heartbeatListener = new HeartbeatListener(config, shenyuConfig, serverProperties); + + // Use reflection to access private method sendHeartbeat + java.lang.reflect.Method sendHeartbeatMethod = HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO.class); + sendHeartbeatMethod.setAccessible(true); + + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO beatInfo = + new org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO(); + + assertDoesNotThrow(() -> { + try { + sendHeartbeatMethod.invoke(heartbeatListener, beatInfo); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + } + + @Test + void testHeartbeatWithAllServersFailure() throws Exception { + + try (MockedStatic registerUtilsMockedStatic = Mockito.mockStatic(RegisterUtils.class)) { + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(anyString(), anyString(), anyString())) + .thenReturn(Optional.of("mock-token")); + + // All server calls fail + registerUtilsMockedStatic.when(() -> RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new RuntimeException("Connection failed")); + + heartbeatListener = new HeartbeatListener(config, shenyuConfig, serverProperties); + + // Use reflection to access private method sendHeartbeat + java.lang.reflect.Method sendHeartbeatMethod = HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO.class); + sendHeartbeatMethod.setAccessible(true); + + org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO beatInfo = + new org.apache.shenyu.client.core.dto.InstanceBeatInfoDTO(); + + // Should throw RuntimeException when all servers fail + try { + sendHeartbeatMethod.invoke(heartbeatListener, beatInfo); + } catch (Exception e) { + assertTrue(e.getCause() instanceof RuntimeException); + } + } + } +} diff --git a/shenyu-registry-api/pom.xml b/shenyu-registry-api/pom.xml new file mode 100644 index 0000000..63df02e --- /dev/null +++ b/shenyu-registry-api/pom.xml @@ -0,0 +1,44 @@ + + + + + + org.apache.shenyu + shenyu-client-java + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-registry-api + + + + + org.apache.shenyu + shenyu-spi + 2.6.1 + + + + org.springframework.boot + spring-boot-test + test + + + + + diff --git a/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/ShenyuInstanceRegisterRepository.java b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/ShenyuInstanceRegisterRepository.java new file mode 100644 index 0000000..7d45a5e --- /dev/null +++ b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/ShenyuInstanceRegisterRepository.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.registry.api; + +import org.apache.shenyu.registry.api.config.RegisterConfig; +import org.apache.shenyu.registry.api.entity.InstanceEntity; +import org.apache.shenyu.registry.api.event.ChangedEventListener; +import org.apache.shenyu.spi.SPI; + +import java.util.Collections; +import java.util.List; + +/** + * Shenyu instance register repository. + */ +@SPI +public interface ShenyuInstanceRegisterRepository { + + /** + * Init. + * + * @param config the config + */ + default void init(RegisterConfig config) { + } + + /** + * Persist instance. + * + * @param instance instance + */ + void persistInstance(InstanceEntity instance); + + /** + * selectInstances. + * + * @param selectKey selectKey + * @return {@link List} + */ + default List selectInstances(final String selectKey) { + return Collections.emptyList(); + } + + /** + * serviceExists. + * + * @param key key + * @return {@link boolean} + */ + default boolean serviceExists(String key) { + return true; + } + + /** + * watchInstances. + * + * @param key key + * @param changedEventListener changedEventListener + */ + default void watchInstances(String key, ChangedEventListener changedEventListener) { + } + + /** + * unWatchInstances. + * + * @param key key + */ + default void unWatchInstances(String key) { + } + + /** + * Close. + */ + default void close() { + } +} diff --git a/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/config/RegisterConfig.java b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/config/RegisterConfig.java new file mode 100644 index 0000000..3b4c636 --- /dev/null +++ b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/config/RegisterConfig.java @@ -0,0 +1,262 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.registry.api.config; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; +import java.util.Objects; +import java.util.Properties; + +/** + * The type Register config. + */ +public class RegisterConfig { + + private boolean enabled; + + private String registerType; + + private String serverLists; + + private Properties props = new Properties(); + + /** + * RegisterConfig. + */ + public RegisterConfig() { + + } + + /** + * registerType. + * + * @param registerType the register type + * @param serverLists the server lists + * @param props the props + */ + public RegisterConfig(final String registerType, final String serverLists, final Properties props) { + this.registerType = registerType; + this.serverLists = serverLists; + this.props = props; + } + + /** + * getRegisterType. + * + * @return String register type + */ + public String getRegisterType() { + return registerType; + } + + /** + * setRegisterType. + * + * @param registerType registerType + */ + public void setRegisterType(final String registerType) { + this.registerType = registerType; + } + + /** + * getServerLists. + * + * @return String server lists + */ + public String getServerLists() { + return serverLists; + } + + /** + * setServerLists. + * + * @param serverLists serverLists + */ + public void setServerLists(final String serverLists) { + this.serverLists = serverLists; + } + + /** + * getProps. + * + * @return String props + */ + public Properties getProps() { + return props; + } + + /** + * setProps. + * + * @param props props + */ + public void setProps(final Properties props) { + this.props = props; + } + + /** + * Gets enabled. + * + * @return the enabled + */ + public boolean getEnabled() { + return enabled; + } + + /** + * Sets enabled. + * + * @param enabled the enabled + */ + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean equals(final Object obj) { + if (Objects.isNull(obj)) { + return false; + } + RegisterConfig registerConfig = (RegisterConfig) obj; + if (!this.getRegisterType().equals(registerConfig.getRegisterType())) { + return false; + } + if (!this.getServerLists().equals(registerConfig.getServerLists())) { + return false; + } + Properties properties = this.getProps(); + Properties registerConfigProps = registerConfig.getProps(); + if (Objects.isNull(properties) && Objects.isNull(registerConfigProps)) { + return true; + } + if (Objects.isNull(properties) || Objects.isNull(registerConfigProps)) { + return false; + } + if (properties.entrySet().size() != registerConfigProps.entrySet().size()) { + return false; + } + for (Map.Entry entry : properties.entrySet()) { + Object newValue = entry.getValue(); + Object oldValue = registerConfigProps.get(entry.getKey()); + if (!newValue.equals(oldValue)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + String registerTypeStr = getRegisterType(); + int result = StringUtils.isNotEmpty(registerTypeStr) ? registerTypeStr.hashCode() : 0; + String serverListsStr = getServerLists(); + result = 31 * result + (StringUtils.isNotEmpty(serverListsStr) ? serverListsStr.hashCode() : 0); + + Properties properties = getProps(); + if (Objects.nonNull(properties)) { + for (Map.Entry entry : properties.entrySet()) { + Object entryKey = entry.getKey(); + result = 31 * result + (Objects.nonNull(entryKey) ? entryKey.hashCode() : 0); + Object entryValue = entry.getValue(); + result = 31 * result + (Objects.nonNull(entryValue) ? entryValue.hashCode() : 0); + } + } + + return result; + } + + /** + * The type Builder. + */ + public static final class Builder { + + private boolean enabled; + + private String registerType; + + private String serverLists; + + private Properties props; + + private Builder() { + } + + public static Builder builder() { + return new Builder(); + } + + /** + * enabled. + * + * @param enabled enabled + * @return Builder builder + */ + public Builder enabled(final boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * registerType. + * + * @param registerType registerType + * @return Builder builder + */ + public Builder registerType(final String registerType) { + this.registerType = registerType; + return this; + } + + /** + * serverLists. + * + * @param serverLists serverLists + * @return Builder builder + */ + public Builder serverLists(final String serverLists) { + this.serverLists = serverLists; + return this; + } + + /** + * props. + * + * @param props props + * @return Builder builder + */ + public Builder props(final Properties props) { + this.props = props; + return this; + } + + /** + * build. + * + * @return Builder instance register dto + */ + public RegisterConfig build() { + RegisterConfig registerConfig = new RegisterConfig(); + registerConfig.setEnabled(enabled); + registerConfig.setRegisterType(registerType); + registerConfig.setServerLists(serverLists); + registerConfig.setProps(props); + return registerConfig; + } + } +} diff --git a/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/entity/InstanceEntity.java b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/entity/InstanceEntity.java new file mode 100644 index 0000000..a1d1451 --- /dev/null +++ b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/entity/InstanceEntity.java @@ -0,0 +1,286 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.registry.api.entity; + +import java.net.URI; +import java.util.Objects; + +/** + * The type Instance register dto. + */ +public class InstanceEntity { + + private String appName; + + private String host; + + private Integer port; + + private URI uri; + + private int status; + + private int weight; + + /** + * Instantiates a new Instance register dto. + * + * @param appName the app name + * @param host the host + * @param port the port + */ + public InstanceEntity(final String appName, final String host, final Integer port) { + this.appName = appName; + this.host = host; + this.port = port; + } + + /** + * Instantiates a new Instance register dto. + */ + public InstanceEntity() { + } + + private InstanceEntity(final Builder builder) { + appName = builder.appName; + host = builder.host; + port = builder.port; + uri = builder.uri; + } + + + /** + * return builder. + * + * @return Builder builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * getAppName. + * + * @return String app name + */ + public String getAppName() { + return appName; + } + + /** + * setAppName. + * + * @param appName appName + */ + public void setAppName(final String appName) { + this.appName = appName; + } + + /** + * getHost. + * + * @return String host + */ + public String getHost() { + return host; + } + + /** + * setHost. + * + * @param host host + */ + public void setHost(final String host) { + this.host = host; + } + + /** + * getPort. + * + * @return String port + */ + public Integer getPort() { + return port; + } + + /** + * setPort. + * + * @param port port + */ + public void setPort(final Integer port) { + this.port = port; + } + + /** + * getUri. + * + * @return URI uri + */ + public URI getUri() { + return uri; + } + + /** + * setUri. + * + * @param uri uri + */ + public void setUri(final URI uri) { + this.uri = uri; + } + + /** + * status. + * + * @return Status + */ + public int getStatus() { + return status; + } + + /** + * set status. + * + * @param status status + */ + public void setStatus(final int status) { + this.status = status; + } + + /** + * weight. + * + * @return Weight + */ + public int getWeight() { + return weight; + } + + /** + * set weight. + * + * @param weight weight + */ + public void setWeight(final int weight) { + this.weight = weight; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return Boolean.TRUE; + } + + if (Objects.isNull(o) || getClass() != o.getClass()) { + return Boolean.FALSE; + } + + InstanceEntity that = (InstanceEntity) o; + return Objects.equals(getAppName(), that.getAppName()) + && Objects.equals(getHost(), that.getHost()) + && Objects.equals(getPort(), that.getPort()); + } + + @Override + public int hashCode() { + return Objects.hash(getAppName(), getHost(), getPort()); + } + + @Override + public String toString() { + return "URIRegisterDTO{" + + "appName='" + + appName + + ", host='" + + host + + ", port=" + + port + + '}'; + } + + /** + * The type Builder. + */ + public static final class Builder { + + private String appName; + + private String host; + + private Integer port; + + private URI uri; + + private Builder() { + } + + /** + * appName. + * + * @param appName appName + * @return Builder builder + */ + public Builder appName(final String appName) { + this.appName = appName; + return this; + } + + /** + * host. + * + * @param host host + * @return Builder builder + */ + public Builder host(final String host) { + this.host = host; + return this; + } + + /** + * port. + * + * @param port port + * @return Builder builder + */ + public Builder port(final Integer port) { + this.port = port; + return this; + } + + /** + * uri. + * + * @param uri uri + * @return Builder builder + */ + public Builder uri(final URI uri) { + this.uri = uri; + return this; + } + + /** + * build. + * + * @return Builder instance register dto + */ + public InstanceEntity build() { + return new InstanceEntity(this); + } + } +} diff --git a/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/event/ChangedEventListener.java b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/event/ChangedEventListener.java new file mode 100644 index 0000000..9fb1106 --- /dev/null +++ b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/event/ChangedEventListener.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.registry.api.event; + +@FunctionalInterface +public interface ChangedEventListener { + + /** + * Data changed event. + */ + enum Event { + + /** + * Added event. + */ + ADDED, + /** + * Updated event. + */ + UPDATED, + /** + * Deleted event. + */ + DELETED, + /** + * Ignored event. + */ + IGNORED + } + + /** + * onEvent. + * + * @param key key + * @param value value + * @param event event + */ + void onEvent(String key, String value, Event event); +} diff --git a/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/path/InstancePathConstants.java b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/path/InstancePathConstants.java new file mode 100644 index 0000000..b17721f --- /dev/null +++ b/shenyu-registry-api/src/main/java/org/apache/shenyu/registry/api/path/InstancePathConstants.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.registry.api.path; + +/** + * zookeeper register center. + */ +public class InstancePathConstants { + + /** + * root path of zookeeper register center. + */ + public static final String ROOT_PATH = "/shenyu/register"; + + /** + * constants of separator. + */ + private static final String SEPARATOR = "/"; + + /** + * Dot separator. + */ + private static final String DOT_SEPARATOR = "."; + + /** + * Build instance parent path string. + * build child path of "/shenyu/register/instance/ + * + * @return the string + */ + public static String buildInstanceParentPath() { + return String.join(SEPARATOR, ROOT_PATH, "instance"); + } + + /** + * Build instance parent path string. + * build child path of "/shenyu/register/instance/serviceName + * + * @param serviceName serviceName + * @return the string + */ + public static String buildInstanceParentPath(final String serviceName) { + return String.join(SEPARATOR, ROOT_PATH, "instance", serviceName); + } + + /** + * Build real node string. + * + * @param nodePath the node path + * @param nodeName the node name + * @return the string + */ + public static String buildRealNode(final String nodePath, final String nodeName) { + return String.join(SEPARATOR, nodePath, nodeName); + } +} diff --git a/shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/config/RegisterConfigTest.java b/shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/config/RegisterConfigTest.java new file mode 100644 index 0000000..877d310 --- /dev/null +++ b/shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/config/RegisterConfigTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.registry.api.config; + +import org.junit.jupiter.api.Test; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RegisterConfigTest { + + @Test + void testDefaultConstructor() { + RegisterConfig config = new RegisterConfig(); + assertFalse(config.getEnabled()); + assertNull(config.getRegisterType()); + assertNull(config.getServerLists()); + assertNotNull(config.getProps()); + } + + @Test + void testParameterizedConstructor() { + Properties props = new Properties(); + props.setProperty("key", "value"); + + RegisterConfig config = new RegisterConfig("type", "localhost:8080", props); + assertTrue(config.getProps().containsKey("key")); + assertEquals("value", config.getProps().getProperty("key")); + assertEquals("type", config.getRegisterType()); + assertEquals("localhost:8080", config.getServerLists()); + } + + @Test + void testSettersAndGetters() { + RegisterConfig config = new RegisterConfig(); + config.setEnabled(true); + config.setRegisterType("type"); + config.setServerLists("localhost:8080"); + + Properties props = new Properties(); + props.setProperty("key", "value"); + config.setProps(props); + + assertTrue(config.getEnabled()); + assertEquals("type", config.getRegisterType()); + assertEquals("localhost:8080", config.getServerLists()); + assertEquals("value", config.getProps().getProperty("key")); + } + + @Test + void testEquals() { + Properties props1 = new Properties(); + props1.setProperty("key", "value"); + + Properties props2 = new Properties(); + props2.setProperty("key", "value"); + + RegisterConfig config1 = new RegisterConfig("type", "localhost:8080", props1); + RegisterConfig config2 = new RegisterConfig("type", "localhost:8080", props2); + + assertEquals(config1, config2); + assertEquals(config1.hashCode(), config2.hashCode()); + + config2.setRegisterType("differentType"); + assertNotEquals(config1, config2); + } + + @Test + void testBuilder() { + Properties props = new Properties(); + props.setProperty("key", "value"); + + RegisterConfig config = RegisterConfig.Builder.builder() + .enabled(true) + .registerType("type") + .serverLists("localhost:8080") + .props(props) + .build(); + + assertTrue(config.getEnabled()); + assertEquals("type", config.getRegisterType()); + assertEquals("localhost:8080", config.getServerLists()); + assertEquals("value", config.getProps().getProperty("key")); + } +} diff --git a/shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/path/InstancePathConstantsTest.java b/shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/path/InstancePathConstantsTest.java new file mode 100644 index 0000000..f5d695b --- /dev/null +++ b/shenyu-registry-api/src/test/java/org/apache/shenyu/registry/api/path/InstancePathConstantsTest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.registry.api.path; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class InstancePathConstantsTest { + + @Test + void testBuildInstanceParentPathWithoutServiceName() { + String expectedPath = "/shenyu/register/instance"; + String actualPath = InstancePathConstants.buildInstanceParentPath(); + assertEquals(expectedPath, actualPath); + } + + @Test + void testBuildInstanceParentPathWithServiceName() { + String serviceName = "myService"; + String expectedPath = "/shenyu/register/instance/myService"; + String actualPath = InstancePathConstants.buildInstanceParentPath(serviceName); + assertEquals(expectedPath, actualPath); + } + + @Test + void testBuildRealNode() { + String nodePath = "/shenyu/register/instance/myService"; + String nodeName = "node1"; + String expectedPath = "/shenyu/register/instance/myService/node1"; + String actualPath = InstancePathConstants.buildRealNode(nodePath, nodeName); + assertEquals(expectedPath, actualPath); + } +} diff --git a/shenyu-spring-boot-starter-client/pom.xml b/shenyu-spring-boot-starter-client/pom.xml new file mode 100644 index 0000000..61c188a --- /dev/null +++ b/shenyu-spring-boot-starter-client/pom.xml @@ -0,0 +1,53 @@ + + + + + + org.apache.shenyu + shenyu-client-java + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-client + pom + + + shenyu-spring-boot-starter-client-springmvc + shenyu-spring-boot-starter-client-apache-dubbo + shenyu-spring-boot-starter-client-sofa + shenyu-spring-boot-starter-client-tars + shenyu-spring-boot-starter-client-grpc + shenyu-spring-boot-starter-client-mcp + shenyu-spring-boot-starter-client-common + shenyu-spring-boot-starter-client-spring-websocket + shenyu-spring-boot-starter-client-beat + + + + + org.springframework.boot + spring-boot-test + test + + + org.assertj + assertj-core + test + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/pom.xml new file mode 100644 index 0000000..411bb45 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/pom.xml @@ -0,0 +1,46 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-client-apache-dubbo + + + + org.apache.shenyu + shenyu-client-apache-dubbo + ${project.version} + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + org.apache.dubbo + dubbo + ${apache.dubbo.version} + provided + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfiguration.java new file mode 100644 index 0000000..d6c795f --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfiguration.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.apache.dubbo; + +import org.apache.shenyu.client.apache.dubbo.ApacheDubboServiceBeanListener; +import org.apache.shenyu.client.core.utils.VersionUtils; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The type shenyu apache dubbo client configuration. + */ +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuApacheDubboClientConfiguration { + + static { + VersionUtils.checkDuplicate(ShenyuApacheDubboClientConfiguration.class); + } + + /** + * Apache dubbo service bean listener. + * + * @param clientConfig the client config + * @param shenyuClientRegisterRepository the shenyu client register repository + * @return the apache dubbo service bean listener + */ + @Bean + public ApacheDubboServiceBeanListener apacheDubboServiceBeanListener(final ShenyuClientConfig clientConfig, + final ShenyuClientRegisterRepository shenyuClientRegisterRepository) { + return new ApacheDubboServiceBeanListener(clientConfig, shenyuClientRegisterRepository); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..c5c54e9 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.springboot.starter.client.apache.dubbo.ShenyuApacheDubboClientConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.provides b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.provides new file mode 100644 index 0000000..8d4469a --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: shenyu-spring-boot-starter-client-apache-dubbo diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..4611af1 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.springboot.starter.client.apache.dubbo.ShenyuApacheDubboClientConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfigurationTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfigurationTest.java new file mode 100644 index 0000000..ead5154 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/java/org/apache/shenyu/springboot/starter/client/apache/dubbo/ShenyuApacheDubboClientConfigurationTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.apache.dubbo; + +import org.apache.shenyu.client.apache.dubbo.ApacheDubboServiceBeanListener; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +/** + * Test case for {@link ShenyuApacheDubboClientConfiguration}. + */ +@Configuration +@EnableConfigurationProperties +@PropertySource(value = "classpath:application.properties") +public class ShenyuApacheDubboClientConfigurationTest { + + @Test + public void testShenyuApacheDubboClientConfiguration() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuApacheDubboClientConfiguration.class)) + .withBean(ShenyuApacheDubboClientConfigurationTest.class) + .withPropertyValues("debug=true") + .run( + context -> { + ApacheDubboServiceBeanListener listener = context.getBean("apacheDubboServiceBeanListener", ApacheDubboServiceBeanListener.class); + assertNotNull(listener); + } + ); + registerUtilsMockedStatic.close(); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/resources/application.properties b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/resources/application.properties new file mode 100644 index 0000000..fbd7297 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-apache-dubbo/src/test/resources/application.properties @@ -0,0 +1,33 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dubbo.registry.address=zookeeper://localhost:2181 + +# more see shenyu-register-center module +shenyu.register.register-type=http +shenyu.register.serverLists=http://localhost:9095 +shenyu.register.props.username=admin +shenyu.register.props.password=123456 + +# more see org.apache.shenyu.client.core.shutdown.ShenyuClientShutdownHook.TakeoverOtherHooksThread.class +shenyu.register.props[shutdownWaitTime]=3000 +shenyu.register.props[delayOtherHooksExecTime]=2000 +shenyu.register.props[applicationShutdownHooksClassName]=java.lang.ApplicationShutdownHooks +shenyu.register.props[applicationShutdownHooksFieldName]=hooks + +shenyu.client.dubbo.props[contextPath]=/dubbo +shenyu.client.dubbo.props[appName]=apache-dubbo +shenyu.client.dubbo.props[host]=localhost +shenyu.client.dubbo.props[port]=20888 diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/pom.xml new file mode 100644 index 0000000..7b5e963 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/pom.xml @@ -0,0 +1,40 @@ + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + + shenyu-spring-boot-starter-client-beat + + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + org.apache.shenyu + shenyu-register-client-beat + ${project.version} + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java new file mode 100644 index 0000000..aa45aab --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.register.client.beat; + +import org.apache.shenyu.client.core.config.ShenyuConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnExpression( + "${shenyu.heartbeat.enabled:true} and " + + "'${shenyu.sync.websocket.urls:}'.isEmpty() and " + + "'${shenyu.sync.http.url:}'.isEmpty()" +) +public class HeartbeatListenerConfiguration { + + /** + * Heartbeat bean listener. + * + * @param shenyuBootstrapHeartBeatConfig the shenyuBootstrapHeartBeatConfig + * @param shenyuConfig the shenyu config + * @param serverProperties the server properties + * @return the heartbeat bean listener. + */ + @Bean + public HeartbeatListener heartbeatListener(final ShenyuBootstrapHeartBeatConfig shenyuBootstrapHeartBeatConfig, + final ShenyuConfig shenyuConfig, + final ServerProperties serverProperties) { + return new HeartbeatListener(shenyuBootstrapHeartBeatConfig, shenyuConfig, serverProperties); + } + + /** + * ShenyuBootstrapHeartBeatConfig. + * + * @return the shenyuBootstrapHeartBeatConfig. + */ + @Bean + @ConfigurationProperties(prefix = "shenyu.heartbeat") + public ShenyuBootstrapHeartBeatConfig shenyuBootstrapHeartBeatConfig() { + return new ShenyuBootstrapHeartBeatConfig(); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..23326a8 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.register.client.beat.HeartbeatListenerConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f79b9ba --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.register.client.beat.HeartbeatListenerConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/pom.xml new file mode 100644 index 0000000..601a25b --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/pom.xml @@ -0,0 +1,43 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-client-common + + + + org.apache.shenyu + shenyu-client-core + ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework + spring-context + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfiguration.java new file mode 100644 index 0000000..779fbcf --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfiguration.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.common.config; + +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepositoryFactory; +import org.apache.shenyu.client.core.config.ShenyuConfig; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.client.core.register.config.ShenyuDiscoveryConfig; +import org.apache.shenyu.client.core.register.config.ShenyuRegisterCenterConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The type shenyu client common bean configuration. + */ +@Configuration +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuClientCommonBeanConfiguration { + + /** + * Register the register repository for http client bean post processor. + * + * @param config the config + * @return the client register repository + */ + @Bean + public ShenyuClientRegisterRepository shenyuClientRegisterRepository(final ShenyuRegisterCenterConfig config) { + return ShenyuClientRegisterRepositoryFactory.newInstance(config); + } + + /** + * Shenyu Register Center Config. + * + * @return the Register Center Config + */ + @Bean + @ConfigurationProperties(prefix = "shenyu.register") + public ShenyuRegisterCenterConfig shenyuRegisterCenterConfig() { + return new ShenyuRegisterCenterConfig(); + } + + /** + * Shenyu client config. + * + * @return the shenyu client config + */ + @Bean + @ConfigurationProperties(prefix = "shenyu") + public ShenyuClientConfig shenyuClientConfig() { + return new ShenyuClientConfig(); + } + + /** + * Shenyu config. + * + * @return the shenyu config + */ + @Bean + @ConfigurationProperties(prefix = "shenyu") + public ShenyuConfig shenyuConfig() { + return new ShenyuConfig(); + } + + /** + * Shenyu discovery config. + * + * @return the shenyu discovery config + */ + @Bean + @ConditionalOnProperty(prefix = "shenyu.discovery", name = "enable", havingValue = "true") + @ConfigurationProperties(prefix = "shenyu.discovery") + public ShenyuDiscoveryConfig shenyuDiscoveryConfig() { + return new ShenyuDiscoveryConfig(); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..f76ca4f --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..77a7621 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/test/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfigurationTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/test/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfigurationTest.java new file mode 100644 index 0000000..7911a72 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-common/src/test/java/org/apache/shenyu/springboot/starter/client/common/config/ShenyuClientCommonBeanConfigurationTest.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.common.config; + +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.client.core.register.config.ShenyuDiscoveryConfig; +import org.apache.shenyu.client.core.register.config.ShenyuRegisterCenterConfig; +import org.apache.shenyu.client.core.enums.RegisterTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +/** + * Test case for {@link ShenyuClientCommonBeanConfiguration}. + */ +@Configuration +@EnableConfigurationProperties +public class ShenyuClientCommonBeanConfigurationTest { + + private ApplicationContextRunner applicationContextRunner; + + @BeforeEach + public void before() { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuClientCommonBeanConfiguration.class)) + .withBean(ShenyuClientCommonBeanConfigurationTest.class) + .withPropertyValues( + "debug=true", + "shenyu.register.registerType=http", + "shenyu.register.serverLists=http://localhost:9095", + "shenyu.register.props.username=admin", + "shenyu.register.props.password=123456", + "shenyu.client.dubbo.props[contextPath]=/common", + "shenyu.client.dubbo.props[appName]=common", + "shenyu.client.dubbo.props[host]=localhost", + "shenyu.discovery.name=local", + "shenyu.discovery.enable=true", + "shenyu.discovery.type=local", + "shenyu.discovery.serverList=20888", + "shenyu.discovery.props[sleep]=1000", + "shenyu.discovery.props[maxRetry]=3" + + ); + } + + @Test + public void testShenyuClientRegisterRepository() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + ShenyuClientRegisterRepository repository = context.getBean("shenyuClientRegisterRepository", ShenyuClientRegisterRepository.class); + assertNotNull(repository); + }); + registerUtilsMockedStatic.close(); + } + + @Test + public void testShenyuRegisterCenterConfig() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + ShenyuRegisterCenterConfig config = context.getBean("shenyuRegisterCenterConfig", ShenyuRegisterCenterConfig.class); + assertNotNull(config); + assertThat(config.getRegisterType()).isEqualTo(RegisterTypeEnum.HTTP.getName()); + }); + registerUtilsMockedStatic.close(); + } + + @Test + public void testShenyuClientConfig() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + ShenyuClientConfig config = context.getBean("shenyuClientConfig", ShenyuClientConfig.class); + assertNotNull(config); + assertThat(config.getClient()).containsKey("dubbo"); + }); + registerUtilsMockedStatic.close(); + } + + @Test + public void testShenyuDiscoveryConfig() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + ShenyuDiscoveryConfig config = context.getBean("shenyuDiscoveryConfig", ShenyuDiscoveryConfig.class); + assertNotNull(config); + assertThat(config.getType()).isEqualTo("local"); + }); + registerUtilsMockedStatic.close(); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/pom.xml new file mode 100644 index 0000000..a6f3e45 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/pom.xml @@ -0,0 +1,45 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-client-grpc + + + + org.apache.shenyu + shenyu-client-grpc + ${project.version} + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + org.apache.shenyu + shenyu-client-autoconfig + ${project.version} + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfiguration.java new file mode 100644 index 0000000..6f8e2dd --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfiguration.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.springboot.starter.client.grpc; + +import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.client.core.register.ClientRegisterConfigImpl; +import org.apache.shenyu.client.grpc.GrpcClientEventListener; +import org.apache.shenyu.client.grpc.server.GrpcServerBuilder; +import org.apache.shenyu.client.grpc.server.GrpcServerRunner; +import org.apache.shenyu.client.core.enums.RpcTypeEnum; +import org.apache.shenyu.client.core.utils.VersionUtils; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import java.util.Objects; +import java.util.Properties; + +/** + * Grpc type client bean postprocessor. + */ +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuGrpcClientConfiguration { + + static { + VersionUtils.checkDuplicate(ShenyuGrpcClientConfiguration.class); + } + + /** + * Grpc client event listener. + * + * @param clientConfig the client config + * @param env env + * @param shenyuClientRegisterRepository the shenyu client register repository + * @return the grpc client bean post processor + */ + @Bean + public GrpcClientEventListener grpcClientEventListener(final ShenyuClientConfig clientConfig, + final Environment env, + final ShenyuClientRegisterRepository shenyuClientRegisterRepository) { + ShenyuClientConfig.ClientPropertiesConfig clientPropertiesConfig = clientConfig.getClient().get(RpcTypeEnum.GRPC.getName()); + Properties props = Objects.isNull(clientPropertiesConfig) ? null : clientPropertiesConfig.getProps(); + String discoveryMode = env.getProperty("shenyu.discovery.type", ShenyuClientConstants.DISCOVERY_LOCAL_MODE); + if (Objects.nonNull(props)) { + props.setProperty(ShenyuClientConstants.DISCOVERY_LOCAL_MODE_KEY, Boolean.valueOf(ShenyuClientConstants.DISCOVERY_LOCAL_MODE.equals(discoveryMode)).toString()); + } + return new GrpcClientEventListener(clientConfig, shenyuClientRegisterRepository); + } + + /** + * Grpc Server. + * + * @param grpcServerBuilder grpcServerBuilder + * @param grpcClientEventListener grpcClientEventListener + * @return the grpc server + */ + @Bean + public GrpcServerRunner grpcServer(final GrpcServerBuilder grpcServerBuilder, + final GrpcClientEventListener grpcClientEventListener) { + return new GrpcServerRunner(grpcServerBuilder, grpcClientEventListener); + } + + /** + * ClientRegisterConfig Bean. + * + * @param shenyuClientConfig shenyuClientConfig + * @param applicationContext applicationContext + * @param env env + * @return clientRegisterConfig + */ + @Bean("grpcClientRegisterConfig") + public ClientRegisterConfig clientRegisterConfig(final ShenyuClientConfig shenyuClientConfig, + final ApplicationContext applicationContext, + final Environment env) { + return new ClientRegisterConfigImpl(shenyuClientConfig, RpcTypeEnum.GRPC, applicationContext, env); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcDiscoveryConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcDiscoveryConfiguration.java new file mode 100644 index 0000000..28f6cd3 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcDiscoveryConfiguration.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.springboot.starter.client.grpc; + +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.client.core.register.ClientDiscoveryConfigRefreshedEventListener; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.client.core.register.InstanceRegisterListener; +import org.apache.shenyu.client.core.dto.DiscoveryUpstreamData; +import org.apache.shenyu.client.core.enums.PluginEnum; +import org.apache.shenyu.client.core.register.HttpClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.client.core.register.config.ShenyuDiscoveryConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import java.util.Objects; +import java.util.Optional; + +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +public class ShenyuGrpcDiscoveryConfiguration { + + /** + * InstanceRegisterListener. + * + * @param clientRegisterConfig clientRegisterConfig + * @param shenyuDiscoveryConfig shenyuDiscoveryConfig + * @param shenyuClientConfig shenyuClientConfig + * @param environment environment + * @return InstanceRegisterListener + */ + @Bean("grpcInstanceRegisterListener") + @ConditionalOnProperty(prefix = "shenyu.discovery", name = "register", matchIfMissing = false) + @ConditionalOnBean(ShenyuDiscoveryConfig.class) + public InstanceRegisterListener instanceRegisterListener(final ClientRegisterConfig clientRegisterConfig, + final ShenyuDiscoveryConfig shenyuDiscoveryConfig, + final ShenyuClientConfig shenyuClientConfig, + final Environment environment) { + DiscoveryUpstreamData discoveryUpstreamData = new DiscoveryUpstreamData(); + discoveryUpstreamData.setUrl(clientRegisterConfig.getHost() + ":" + clientRegisterConfig.getPort()); + discoveryUpstreamData.setStatus(0); + discoveryUpstreamData.setWeight(50); + discoveryUpstreamData.setProtocol(Optional.ofNullable(shenyuDiscoveryConfig.getProtocol()).orElse(ShenyuClientConstants.HTTP)); + discoveryUpstreamData.setNamespaceId(shenyuClientConfig.getNamespace()); + final String appName = environment.getProperty("spring.application.name"); + if (StringUtils.isEmpty(shenyuDiscoveryConfig.getProps().getProperty("name")) && Objects.nonNull(appName)) { + shenyuDiscoveryConfig.getProps().put("name", appName); + } + return new InstanceRegisterListener(discoveryUpstreamData, shenyuDiscoveryConfig); + } + + /** + * clientDiscoveryConfigRefreshedEventListener. + * + * @param shenyuDiscoveryConfig shenyuDiscoveryConfig + * @param httpClientRegisterRepository httpClientRegisterRepository + * @param clientRegisterConfig clientRegisterConfig + * @param shenyuClientConfig shenyuClientConfig + * @return ClientDiscoveryConfigRefreshedEventListener + */ + @Bean("GrpcClientDiscoveryConfigRefreshedEventListener") + @ConditionalOnProperty(prefix = "shenyu.discovery", name = "serverList", matchIfMissing = false) + @ConditionalOnBean(ShenyuDiscoveryConfig.class) + public ClientDiscoveryConfigRefreshedEventListener clientDiscoveryConfigRefreshedEventListener(final ShenyuDiscoveryConfig shenyuDiscoveryConfig, + final HttpClientRegisterRepository httpClientRegisterRepository, + final ClientRegisterConfig clientRegisterConfig, + final ShenyuClientConfig shenyuClientConfig) { + return new ClientDiscoveryConfigRefreshedEventListener(shenyuDiscoveryConfig, httpClientRegisterRepository, clientRegisterConfig, PluginEnum.GRPC, shenyuClientConfig); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..df60c49 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.springboot.starter.client.grpc.ShenyuGrpcClientConfiguration,\ +org.apache.springboot.starter.client.grpc.ShenyuGrpcDiscoveryConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides new file mode 100644 index 0000000..6ac092b --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: shenyu-spring-boot-starter-client-grpc diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3ae0050 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.springboot.starter.client.grpc.ShenyuGrpcClientConfiguration +org.apache.springboot.starter.client.grpc.ShenyuGrpcDiscoveryConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfigurationTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfigurationTest.java new file mode 100644 index 0000000..6b065dd --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/ShenyuGrpcClientConfigurationTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.springboot.starter.client.grpc; + +import org.apache.shenyu.client.grpc.GrpcClientEventListener; +import org.apache.shenyu.client.grpc.server.GrpcServerRunner; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +/** + * Test case for {@link ShenyuGrpcClientConfiguration}. + */ +@Configuration +@EnableConfigurationProperties +@ComponentScan(value = "org.apache.springboot.starter.client.grpc.server") +public class ShenyuGrpcClientConfigurationTest { + + private ApplicationContextRunner applicationContextRunner; + + @BeforeEach + public void before() { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuGrpcClientConfiguration.class)) + .withBean(ShenyuGrpcClientConfigurationTest.class) + .withPropertyValues( + "debug=true", + "shenyu.register.registerType=http", + "shenyu.register.serverLists=http://localhost:9095", + "shenyu.register.props.username=admin", + "shenyu.register.props.password=123456", + "shenyu.client.grpc.props[contextPath]=/grpc", + "shenyu.client.grpc.props[appName]=grpc", + "shenyu.client.grpc.props[ipAndPort]=127.0.0.1:8080", + "shenyu.client.grpc.props[port]=8080" + ); + } + + @Test + public void testGrpcClientBeanPostProcessor() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.of("token")); + applicationContextRunner.run(context -> { + GrpcClientEventListener listener = context.getBean("grpcClientEventListener", GrpcClientEventListener.class); + assertNotNull(listener); + }); + registerUtilsMockedStatic.close(); + } + + @Test + public void testGrpcContextRefreshedEventListener() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.of("token")); + applicationContextRunner.run(context -> { + GrpcClientEventListener listener = context.getBean("grpcClientEventListener", GrpcClientEventListener.class); + assertNotNull(listener); + }); + registerUtilsMockedStatic.close(); + } + + @Test + public void testGrpcServerRunner() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.of("token")); + applicationContextRunner.run(context -> { + GrpcServerRunner runner = context.getBean("grpcServer", GrpcServerRunner.class); + assertNotNull(runner); + }); + registerUtilsMockedStatic.close(); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/server/ShenyuGrpcServerBuilderTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/server/ShenyuGrpcServerBuilderTest.java new file mode 100644 index 0000000..762711d --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-grpc/src/test/java/org/apache/springboot/starter/client/grpc/server/ShenyuGrpcServerBuilderTest.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.springboot.starter.client.grpc.server; + +import io.grpc.ServerBuilder; +import org.apache.shenyu.client.grpc.server.GrpcServerBuilder; +import org.springframework.stereotype.Component; + +/** + * Grpc ServerBuilder Test. + */ +@Component +public class ShenyuGrpcServerBuilderTest implements GrpcServerBuilder { + + @Override + public ServerBuilder buildServerBuilder() { + return ServerBuilder.forPort(8080); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/pom.xml new file mode 100644 index 0000000..c50f000 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/pom.xml @@ -0,0 +1,41 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + + 4.0.0 + shenyu-spring-boot-starter-client-mcp + + + + org.apache.shenyu + shenyu-client-mcp-register + ${project.version} + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/src/main/java/org/apache/shenyu/springboot/starter/client/mcp/ShenyuMcpClientConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/src/main/java/org/apache/shenyu/springboot/starter/client/mcp/ShenyuMcpClientConfiguration.java new file mode 100644 index 0000000..4d0d731 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-mcp/src/main/java/org/apache/shenyu/springboot/starter/client/mcp/ShenyuMcpClientConfiguration.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.mcp; + +import org.apache.shenyu.client.mcp.McpServiceEventListener; +import org.apache.shenyu.client.core.utils.VersionUtils; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +/** + * The type shenyu apache mcp client configuration. + */ + +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuMcpClientConfiguration { + + static { + VersionUtils.checkDuplicate(ShenyuMcpClientConfiguration.class); + } + + /** + * Apache mcp service bean listener. + * + * @param clientConfig the client config + * @param shenyuClientRegisterRepository the shenyu client register repository + * @param env the spring environment + * @return the apache mcp service bean listener + */ + @Bean + public McpServiceEventListener mcpServiceEventListener(final ShenyuClientConfig clientConfig, + final ShenyuClientRegisterRepository shenyuClientRegisterRepository, + final Environment env) { + return new McpServiceEventListener(clientConfig, shenyuClientRegisterRepository, env); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/pom.xml new file mode 100644 index 0000000..da7863c --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/pom.xml @@ -0,0 +1,50 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-client-sofa + + + 3.1.4 + + + + + org.apache.shenyu + shenyu-client-sofa + ${project.version} + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + com.alipay.sofa + runtime-sofa-boot-starter + ${runtime-sofa-boot-starter.version} + test + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfiguration.java new file mode 100644 index 0000000..b2a862b --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfiguration.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.sofa; + +import org.apache.shenyu.client.sofa.SofaServiceEventListener; +import org.apache.shenyu.client.core.utils.VersionUtils; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Sofa type client event listener. + */ +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuSofaClientConfiguration { + + static { + VersionUtils.checkDuplicate(ShenyuSofaClientConfiguration.class); + } + + /** + * Sofa service event listener. + * + * @param clientConfig the client config + * @param shenyuClientRegisterRepository the shenyuClientRegisterRepository + * @return the sofa service event listener + */ + @Bean + public SofaServiceEventListener sofaServiceEventListener(final ShenyuClientConfig clientConfig, final ShenyuClientRegisterRepository shenyuClientRegisterRepository) { + return new SofaServiceEventListener(clientConfig, shenyuClientRegisterRepository); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..4f9afdc --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.springboot.starter.client.sofa.ShenyuSofaClientConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.provides b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.provides new file mode 100644 index 0000000..181e634 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: shenyu-spring-boot-starter-client-sofa diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..78d2ff8 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.springboot.starter.client.sofa.ShenyuSofaClientConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/test/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfigurationTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/test/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfigurationTest.java new file mode 100644 index 0000000..5feb930 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-sofa/src/test/java/org/apache/shenyu/springboot/starter/client/sofa/ShenyuSofaClientConfigurationTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.sofa; + +import org.apache.shenyu.client.sofa.SofaServiceEventListener; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +/** + * Test case for {@link ShenyuSofaClientConfiguration}. + */ +@Configuration +@EnableConfigurationProperties +public class ShenyuSofaClientConfigurationTest { + + private ApplicationContextRunner applicationContextRunner; + + @BeforeEach + public void before() { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuSofaClientConfiguration.class)) + .withBean(ShenyuSofaClientConfigurationTest.class) + .withPropertyValues( + "debug=true", + "shenyu.register.registerType=http", + "shenyu.register.serverLists=http://localhost:9095", + "shenyu.register.props.username=admin", + "shenyu.register.props.password=123456", + "shenyu.client.sofa.props[contextPath]=/sofa", + "shenyu.client.sofa.props[appName]=sofa", + "shenyu.client.sofa.props[host]=127.0.0.1", + "shenyu.client.sofa.props[port]=8888" + ); + } + + @Test + public void testSofaServiceEventListener() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + SofaServiceEventListener eventListener = context.getBean("sofaServiceEventListener", SofaServiceEventListener.class); + assertNotNull(eventListener); + assertEquals(eventListener.getAppName(), "sofa"); + assertEquals(eventListener.getHost(), "127.0.0.1"); + assertEquals(eventListener.getPort(), "8888"); + assertEquals(eventListener.getContextPath(), "/sofa"); + }); + registerUtilsMockedStatic.close(); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/pom.xml new file mode 100644 index 0000000..f406ea0 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/pom.xml @@ -0,0 +1,46 @@ + + + + + shenyu-spring-boot-starter-client + org.apache.shenyu + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + + shenyu-spring-boot-starter-client-spring-websocket + + + + org.apache.shenyu + shenyu-client-spring-websocket + ${project.version} + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + org.apache.shenyu + shenyu-client-autoconfig + ${project.version} + + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfiguration.java new file mode 100644 index 0000000..01c076e --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfiguration.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.spring.websocket; + +import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.client.core.register.ClientRegisterConfigImpl; +import org.apache.shenyu.client.spring.websocket.init.SpringWebSocketClientEventListener; +import org.apache.shenyu.client.core.enums.RpcTypeEnum; +import org.apache.shenyu.client.core.utils.VersionUtils; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import java.util.Objects; +import java.util.Properties; + +/** + * The type shenyu websocket client http configuration. + */ +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuSpringWebSocketClientConfiguration { + + static { + VersionUtils.checkDuplicate(ShenyuSpringWebSocketClientConfiguration.class); + } + + /** + * Spring web socket client event listener. + * + * @param clientConfig the client config + * @param shenyuClientRegisterRepository the shenyu client register repository + * @param env env + * @return the spring web socket client event listener + */ + @Bean + public SpringWebSocketClientEventListener springWebSocketClientEventListener( + final ShenyuClientConfig clientConfig, + final Environment env, + final ShenyuClientRegisterRepository shenyuClientRegisterRepository) { + ShenyuClientConfig.ClientPropertiesConfig clientPropertiesConfig = clientConfig.getClient().get(RpcTypeEnum.WEB_SOCKET.getName()); + Properties props = Objects.isNull(clientPropertiesConfig) ? null : clientPropertiesConfig.getProps(); + String discoveryMode = env.getProperty("shenyu.discovery.type", ShenyuClientConstants.DISCOVERY_LOCAL_MODE); + if (Objects.nonNull(props)) { + props.setProperty(ShenyuClientConstants.DISCOVERY_LOCAL_MODE_KEY, Boolean.valueOf(ShenyuClientConstants.DISCOVERY_LOCAL_MODE.equals(discoveryMode)).toString()); + } + return new SpringWebSocketClientEventListener(clientConfig, shenyuClientRegisterRepository); + } + + /** + * ClientRegisterConfig Bean. + * + * @param shenyuClientConfig shenyuClientConfig + * @param applicationContext applicationContext + * @param env env + * @return clientRegisterConfig + */ + @Bean("webSocketClientRegisterConfig") + public ClientRegisterConfig clientRegisterConfig(final ShenyuClientConfig shenyuClientConfig, + final ApplicationContext applicationContext, + final Environment env) { + return new ClientRegisterConfigImpl(shenyuClientConfig, RpcTypeEnum.WEB_SOCKET, applicationContext, env); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketDiscoveryConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketDiscoveryConfiguration.java new file mode 100644 index 0000000..9928c24 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketDiscoveryConfiguration.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.spring.websocket; + +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.client.core.register.ClientDiscoveryConfigRefreshedEventListener; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.client.core.register.InstanceRegisterListener; +import org.apache.shenyu.client.spring.websocket.init.SpringWebSocketClientEventListener; +import org.apache.shenyu.client.core.dto.DiscoveryUpstreamData; +import org.apache.shenyu.client.core.enums.PluginEnum; +import org.apache.shenyu.client.core.register.HttpClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.client.core.register.config.ShenyuDiscoveryConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import java.util.Objects; + +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +public class ShenyuSpringWebSocketDiscoveryConfiguration { + + /** + * clientDiscoveryConfigRefreshedEventListener. + * + * @param shenyuDiscoveryConfig shenyuDiscoveryConfig + * @param httpClientRegisterRepository httpClientRegisterRepository + * @param clientRegisterConfig clientRegisterConfig + * @param shenyuClientConfig shenyuClientConfig + * @return ClientDiscoveryConfigRefreshedEventListener + */ + @Bean("WebSocketClientDiscoveryConfigRefreshedEventListener") + @ConditionalOnProperty(prefix = "shenyu.discovery", name = "serverList", matchIfMissing = false) + @ConditionalOnBean(ShenyuDiscoveryConfig.class) + public ClientDiscoveryConfigRefreshedEventListener clientDiscoveryConfigRefreshedEventListener(final ShenyuDiscoveryConfig shenyuDiscoveryConfig, + final HttpClientRegisterRepository httpClientRegisterRepository, + final ClientRegisterConfig clientRegisterConfig, + final ShenyuClientConfig shenyuClientConfig) { + return new ClientDiscoveryConfigRefreshedEventListener(shenyuDiscoveryConfig, httpClientRegisterRepository, clientRegisterConfig, PluginEnum.WEB_SOCKET, shenyuClientConfig); + } + + /** + * InstanceRegisterListener. + * + * @param eventListener eventListener + * @param shenyuDiscoveryConfig discoveryConfig + * @param shenyuClientConfig shenyuClientConfig + * @param environment environment + * @return InstanceRegisterListener + */ + @Bean("websocketInstanceRegisterListener") + @ConditionalOnBean(ShenyuDiscoveryConfig.class) + @ConditionalOnProperty(prefix = "shenyu.discovery", name = "register", matchIfMissing = false) + public InstanceRegisterListener instanceRegisterListener(final SpringWebSocketClientEventListener eventListener, + final ShenyuDiscoveryConfig shenyuDiscoveryConfig, + final ShenyuClientConfig shenyuClientConfig, + final Environment environment) { + DiscoveryUpstreamData discoveryUpstreamData = new DiscoveryUpstreamData(); + discoveryUpstreamData.setProtocol(ShenyuClientConstants.WS); + discoveryUpstreamData.setStatus(0); + discoveryUpstreamData.setWeight(50); + discoveryUpstreamData.setUrl(eventListener.getHost() + ":" + eventListener.getPort()); + discoveryUpstreamData.setNamespaceId(shenyuClientConfig.getNamespace()); + final String appName = environment.getProperty("spring.application.name"); + if (StringUtils.isEmpty(shenyuDiscoveryConfig.getProps().getProperty("name")) && Objects.nonNull(appName)) { + shenyuDiscoveryConfig.getProps().put("name", appName); + } + return new InstanceRegisterListener(discoveryUpstreamData, shenyuDiscoveryConfig); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..2482fc8 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.factories @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.springboot.starter.client.spring.websocket.ShenyuSpringWebSocketClientConfiguration,\ +org.apache.shenyu.springboot.starter.client.spring.websocket.ShenyuSpringWebSocketDiscoveryConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.provides b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.provides new file mode 100644 index 0000000..20844bf --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: shenyu-spring-boot-starter-client-spring-websocket diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3dc5898 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.springboot.starter.client.spring.websocket.ShenyuSpringWebSocketClientConfiguration +org.apache.shenyu.springboot.starter.client.spring.websocket.ShenyuSpringWebSocketDiscoveryConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/test/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfigurationTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/test/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfigurationTest.java new file mode 100644 index 0000000..8f558c0 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-spring-websocket/src/test/java/org/apache/shenyu/springboot/starter/client/spring/websocket/ShenyuSpringWebSocketClientConfigurationTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.spring.websocket; + +import org.apache.shenyu.client.spring.websocket.init.SpringWebSocketClientEventListener; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +/** + * Test case for {@link ShenyuSpringWebSocketClientConfiguration}. + */ +@Configuration +@EnableConfigurationProperties +public class ShenyuSpringWebSocketClientConfigurationTest { + + private ApplicationContextRunner applicationContextRunner; + + @BeforeEach + public void before() { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuSpringWebSocketClientConfiguration.class)) + .withBean(ShenyuSpringWebSocketClientConfigurationTest.class) + .withPropertyValues( + "debug=true", + "shenyu.register.registerType=http", + "shenyu.register.serverLists=http://localhost:9095", + "shenyu.register.props.username=admin", + "shenyu.register.props.password=123456", + "shenyu.client.websocket.props[contextPath]=/websocket", + "shenyu.client.websocket.props[appName]=websocket", + "shenyu.client.websocket.props[port]=8001" + ); + } + + @Test + public void testSpringWebSocketClientEventListener() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + SpringWebSocketClientEventListener eventListener = context.getBean("springWebSocketClientEventListener", SpringWebSocketClientEventListener.class); + assertNotNull(eventListener); + }); + registerUtilsMockedStatic.close(); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/pom.xml new file mode 100644 index 0000000..7f4691c --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/pom.xml @@ -0,0 +1,50 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-client-springmvc + + + + org.apache.shenyu + shenyu-client-springmvc + ${project.version} + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + org.apache.shenyu + shenyu-client-autoconfig + ${project.version} + + + org.springframework + spring-web + provided + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfiguration.java new file mode 100644 index 0000000..187eab5 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfiguration.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.springmvc; + +import java.util.Objects; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.auto.config.ClientRegisterConfiguration; +import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.client.core.register.ClientRegisterConfigImpl; +import org.apache.shenyu.client.springmvc.init.SpringMvcClientEventListener; +import org.apache.shenyu.client.core.enums.RpcTypeEnum; +import org.apache.shenyu.client.core.utils.VersionUtils; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig.ClientPropertiesConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; + +import java.util.Properties; + +/** + * The type shenyu spring mvc client configuration. + */ +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuSpringMvcClientConfiguration { + + static { + VersionUtils.checkDuplicate(ShenyuSpringMvcClientConfiguration.class); + } + + /** + * Spring mvc client event listener. + * + * @param clientConfig the client config + * @param shenyuClientRegisterRepository the shenyu client register repository + * @param env the env + * @return the spring mvc client event listener + */ + @Bean + @ConditionalOnMissingBean(ClientRegisterConfiguration.class) + public SpringMvcClientEventListener springHttpClientEventListener(final ShenyuClientConfig clientConfig, + final ShenyuClientRegisterRepository shenyuClientRegisterRepository, + final Environment env) { + ClientPropertiesConfig clientPropertiesConfig = clientConfig.getClient().get(RpcTypeEnum.HTTP.getName()); + Properties props = Optional.ofNullable(clientPropertiesConfig).map(ClientPropertiesConfig::getProps).orElse(null); + String applicationName = env.getProperty("spring.application.name"); + String discoveryMode = env.getProperty("shenyu.discovery.type", ShenyuClientConstants.DISCOVERY_LOCAL_MODE); + if (Objects.nonNull(props)) { + String appName = props.getProperty(ShenyuClientConstants.APP_NAME); + if (StringUtils.isBlank(appName) && StringUtils.isBlank(applicationName)) { + throw new IllegalArgumentException("spring.application.name or shenyu.client.http.props.appName must not be empty"); + } + if (StringUtils.isBlank(appName)) { + props.setProperty(ShenyuClientConstants.APP_NAME, applicationName); + } + String contextPath = props.getProperty(ShenyuClientConstants.CONTEXT_PATH); + if (StringUtils.isBlank(contextPath)) { + props.setProperty(ShenyuClientConstants.CONTEXT_PATH, String.format("/%s", applicationName)); + } + props.setProperty(ShenyuClientConstants.DISCOVERY_LOCAL_MODE_KEY, Boolean.valueOf(ShenyuClientConstants.DISCOVERY_LOCAL_MODE.equals(discoveryMode)).toString()); + } + return new SpringMvcClientEventListener(clientConfig, shenyuClientRegisterRepository, env); + } + + /** + * ClientRegisterConfig Bean. + * + * @param shenyuClientConfig shenyuClientConfig + * @param applicationContext applicationContext + * @param env env + * @return clientRegisterConfig + */ + @Bean("springMvcClientRegisterConfig") + @Primary + public ClientRegisterConfig clientRegisterConfig(final ShenyuClientConfig shenyuClientConfig, + final ApplicationContext applicationContext, + final Environment env) { + return new ClientRegisterConfigImpl(shenyuClientConfig, RpcTypeEnum.HTTP, applicationContext, env); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientInfoRegisterConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientInfoRegisterConfiguration.java new file mode 100644 index 0000000..a3e3894 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientInfoRegisterConfiguration.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.springmvc; + +import org.apache.shenyu.client.auto.config.ClientRegisterConfiguration; +import org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.client.core.register.matcher.ExtractorProcessor; +import org.apache.shenyu.client.core.register.registrar.AbstractApiDocRegistrar; +import org.apache.shenyu.client.core.register.registrar.AbstractApiMetaRegistrar; +import org.apache.shenyu.client.core.register.registrar.HttpApiDocRegistrar; +import org.apache.shenyu.client.springmvc.proceeor.register.ShenyuSpringMvcClientProcessorImpl; +import org.apache.shenyu.client.springmvc.register.SpringMvcApiBeansExtractor; +import org.apache.shenyu.client.springmvc.register.SpringMvcApiMetaRegister; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(ClientRegisterConfiguration.class) +public class ShenyuSpringMvcClientInfoRegisterConfiguration { + + public ShenyuSpringMvcClientInfoRegisterConfiguration() { + } + + /** + * ApiBeansExtractor Bean. + * + * @param extractorProcessorList extractorProcessorList + * @return apiBeansExtractor + */ + @Bean + @ConditionalOnMissingBean + public SpringMvcApiBeansExtractor springMvcApiBeansExtractor(final List extractorProcessorList) { + final SpringMvcApiBeansExtractor extractor = SpringMvcApiBeansExtractor.buildDefaultSpringMvcApiBeansExtractor(); + for (ExtractorProcessor processor : extractorProcessorList) { + extractor.addExtractorProcessor(processor); + } + return extractor; + } + + /** + * shenyuSpringMvcClientProcessor. + * + * @return shenyuSpringMvcClientProcessor + */ + @Bean + public ShenyuSpringMvcClientProcessorImpl shenyuSpringMvcClientProcessor() { + return new ShenyuSpringMvcClientProcessorImpl(); + } + + /** + * Builds ApiMetaRegistrar Bean. + * + * @param publisher publisher + * @param clientRegisterConfig clientRegisterConfig + * @return ApiMetaRegistrar + */ + public AbstractApiMetaRegistrar buildApiMetaRegistrar(final ShenyuClientRegisterEventPublisher publisher, + final ClientRegisterConfig clientRegisterConfig) { + + return new SpringMvcApiMetaRegister(publisher, clientRegisterConfig); + } + + /** + * Builds ApiDocRegistrar Bean. + * + * @param publisher publisher + * @param clientRegisterConfig clientRegisterConfig + * @return ApiDocRegistrar + */ + public AbstractApiDocRegistrar buildApiDocRegistrar(final ShenyuClientRegisterEventPublisher publisher, + final ClientRegisterConfig clientRegisterConfig) { + return new HttpApiDocRegistrar(publisher, clientRegisterConfig); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcDiscoveryConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcDiscoveryConfiguration.java new file mode 100644 index 0000000..152aa4b --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcDiscoveryConfiguration.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.springmvc; + +import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.client.core.constant.ShenyuClientConstants; +import org.apache.shenyu.client.core.register.ClientDiscoveryConfigRefreshedEventListener; +import org.apache.shenyu.client.core.register.ClientRegisterConfig; +import org.apache.shenyu.client.core.register.InstanceRegisterListener; +import org.apache.shenyu.client.core.dto.DiscoveryUpstreamData; +import org.apache.shenyu.client.core.enums.PluginEnum; +import org.apache.shenyu.client.core.register.HttpClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.client.core.register.config.ShenyuDiscoveryConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; + +import java.util.Objects; +import java.util.Optional; + +@Configuration +@ConditionalOnBean(ClientRegisterConfig.class) +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +public class ShenyuSpringMvcDiscoveryConfiguration { + + /** + * clientDiscoveryConfigRefreshedEventListener Bean. + * + * @param shenyuDiscoveryConfig shenyuDiscoveryConfig + * @param httpClientRegisterRepository httpClientRegisterRepository + * @param clientRegisterConfig clientRegisterConfig + * @param shenyuClientConfig shenyuClientConfig + * @return ClientDiscoveryConfigRefreshedEventListener + */ + @Bean("SpringMvcClientDiscoveryConfigRefreshedEventListener") + @ConditionalOnProperty(prefix = "shenyu.discovery", name = "serverList", matchIfMissing = false) + @ConditionalOnBean(ShenyuDiscoveryConfig.class) + public ClientDiscoveryConfigRefreshedEventListener clientDiscoveryConfigRefreshedEventListener(final ShenyuDiscoveryConfig shenyuDiscoveryConfig, + final HttpClientRegisterRepository httpClientRegisterRepository, + final ClientRegisterConfig clientRegisterConfig, + final ShenyuClientConfig shenyuClientConfig) { + return new ClientDiscoveryConfigRefreshedEventListener(shenyuDiscoveryConfig, httpClientRegisterRepository, clientRegisterConfig, PluginEnum.DIVIDE, shenyuClientConfig); + } + + /** + * InstanceRegisterListener. + * + * @param clientRegisterConfig clientRegisterConfig + * @param shenyuDiscoveryConfig shenyuDiscoveryConfig + * @param shenyuClientConfig shenyuClientConfig + * @param environment environment + * @return InstanceRegisterListener + */ + @Bean("springmvcInstanceRegisterListener") + @ConditionalOnProperty(prefix = "shenyu.discovery", name = "register", matchIfMissing = false) + @ConditionalOnBean(ShenyuDiscoveryConfig.class) + @Primary + public InstanceRegisterListener instanceRegisterListener(final ClientRegisterConfig clientRegisterConfig, + final ShenyuDiscoveryConfig shenyuDiscoveryConfig, + final ShenyuClientConfig shenyuClientConfig, + final Environment environment) { + DiscoveryUpstreamData discoveryUpstreamData = new DiscoveryUpstreamData(); + discoveryUpstreamData.setUrl(clientRegisterConfig.getHost() + ":" + clientRegisterConfig.getPort()); + discoveryUpstreamData.setStatus(0); + discoveryUpstreamData.setWeight(50); + discoveryUpstreamData.setProtocol(Optional.ofNullable(shenyuDiscoveryConfig.getProtocol()).orElse(ShenyuClientConstants.HTTP)); + discoveryUpstreamData.setNamespaceId(shenyuClientConfig.getNamespace()); + final String appName = environment.getProperty("spring.application.name"); + if (StringUtils.isEmpty(shenyuDiscoveryConfig.getProps().getProperty("name")) && Objects.nonNull(appName)) { + shenyuDiscoveryConfig.getProps().put("name", appName); + } + return new InstanceRegisterListener(discoveryUpstreamData, shenyuDiscoveryConfig); + } + +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..31e130c --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.springboot.starter.client.springmvc.ShenyuSpringMvcClientConfiguration,\ +org.apache.shenyu.springboot.starter.client.springmvc.ShenyuSpringMvcClientInfoRegisterConfiguration,\ +org.apache.shenyu.springboot.starter.client.springmvc.ShenyuSpringMvcDiscoveryConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.provides b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.provides new file mode 100644 index 0000000..c68b353 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: shenyu-spring-boot-starter-client-springmvc diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..33454d6 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.springboot.starter.client.springmvc.ShenyuSpringMvcClientConfiguration +org.apache.shenyu.springboot.starter.client.springmvc.ShenyuSpringMvcClientInfoRegisterConfiguration +org.apache.shenyu.springboot.starter.client.springmvc.ShenyuSpringMvcDiscoveryConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/test/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfigurationTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/test/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfigurationTest.java new file mode 100644 index 0000000..8a21a50 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-springmvc/src/test/java/org/apache/shenyu/springboot/starter/client/springmvc/ShenyuSpringMvcClientConfigurationTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.springmvc; + +import org.apache.shenyu.client.springmvc.init.SpringMvcClientEventListener; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.test.util.AssertionErrors.assertEquals; + +/** + * Test case for {@link ShenyuSpringMvcClientConfiguration}. + */ +@Configuration +@EnableConfigurationProperties +public class ShenyuSpringMvcClientConfigurationTest { + + private ApplicationContextRunner applicationContextRunner; + + @BeforeEach + public void before() { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuSpringMvcClientConfiguration.class)) + .withBean(ShenyuSpringMvcClientConfigurationTest.class) + .withPropertyValues( + "debug=true", + "shenyu.register.registerType=http", + "shenyu.register.serverLists=http://localhost:9095", + "shenyu.register.props.username=admin", + "shenyu.register.props.password=123456", + "shenyu.client.http.props[contextPath]=/http", + "shenyu.client.http.props[appName]=http", + "shenyu.client.http.props[port]=8189" + ); + } + + @BeforeEach + public void beforeWithDefault() { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuSpringMvcClientConfiguration.class)) + .withBean(ShenyuSpringMvcClientConfigurationTest.class) + .withPropertyValues( + "debug=true", + "shenyu.register.registerType=http", + "shenyu.register.serverLists=http://localhost:9095", + "shenyu.register.props.username=admin", + "shenyu.register.props.password=123456", + "spring.application.name=test-for-http", + "shenyu.client.http.props[port]=8189" + ); + } + + @Test + public void testSpringMvcClientEventListener() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + SpringMvcClientEventListener processor = context.getBean("springHttpClientEventListener", SpringMvcClientEventListener.class); + assertNotNull(processor); + }); + registerUtilsMockedStatic.close(); + } + + @Test + public void testSpringMvcClientEventListenerWithDefault() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + SpringMvcClientEventListener processor = context.getBean("springHttpClientEventListener", SpringMvcClientEventListener.class); + assertEquals("default-appName", "test-for-http", processor.getAppName()); + assertEquals("default-contextPath", "/test-for-http", processor.getContextPath()); + }); + registerUtilsMockedStatic.close(); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/pom.xml b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/pom.xml new file mode 100644 index 0000000..2f69a36 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/pom.xml @@ -0,0 +1,41 @@ + + + + + + org.apache.shenyu + shenyu-spring-boot-starter-client + 2.7.0.1-jdk8-SNAPSHOT + + 4.0.0 + shenyu-spring-boot-starter-client-tars + + + + org.apache.shenyu + shenyu-client-tars + ${project.version} + + + + org.apache.shenyu + shenyu-spring-boot-starter-client-common + ${project.version} + + + diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfiguration.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfiguration.java new file mode 100644 index 0000000..c9d0365 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfiguration.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.tars; + +import org.apache.shenyu.client.tars.TarsServiceBeanEventListener; +import org.apache.shenyu.client.core.utils.VersionUtils; +import org.apache.shenyu.client.core.register.ShenyuClientRegisterRepository; +import org.apache.shenyu.client.core.register.config.ShenyuClientConfig; +import org.apache.shenyu.springboot.starter.client.common.config.ShenyuClientCommonBeanConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Tars type client bean postprocessor. + */ +@Configuration +@ImportAutoConfiguration(ShenyuClientCommonBeanConfiguration.class) +@ConditionalOnProperty(value = "shenyu.register.enabled", matchIfMissing = true, havingValue = "true") +public class ShenyuTarsClientConfiguration { + + static { + VersionUtils.checkDuplicate(ShenyuTarsClientConfiguration.class); + } + + /** + * Tars service bean post processor. + * + * @param clientConfig the client config + * @param shenyuClientRegisterRepository the shenyuClientRegisterRepository + * @return the tars service bean post processor + */ + @Bean + public TarsServiceBeanEventListener tarsServiceBeanEventListener(final ShenyuClientConfig clientConfig, final ShenyuClientRegisterRepository shenyuClientRegisterRepository) { + return new TarsServiceBeanEventListener(clientConfig, shenyuClientRegisterRepository); + } +} diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..60a0ce1 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.factories @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.shenyu.springboot.starter.client.tars.ShenyuTarsClientConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.provides b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.provides new file mode 100644 index 0000000..3a6464f --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring.provides @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +provides: shenyu-spring-boot-starter-client-tars diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..e145b17 --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.apache.shenyu.springboot.starter.client.tars.ShenyuTarsClientConfiguration diff --git a/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/test/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfigurationTest.java b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/test/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfigurationTest.java new file mode 100644 index 0000000..cd1ea5b --- /dev/null +++ b/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-tars/src/test/java/org/apache/shenyu/springboot/starter/client/tars/ShenyuTarsClientConfigurationTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.client.tars; + +import org.apache.shenyu.client.tars.TarsServiceBeanEventListener; +import org.apache.shenyu.client.core.utils.RegisterUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +/** + * Test case for {@link ShenyuTarsClientConfiguration}. + */ +@Configuration +@EnableConfigurationProperties +public class ShenyuTarsClientConfigurationTest { + + private ApplicationContextRunner applicationContextRunner; + + @BeforeEach + public void before() { + applicationContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShenyuTarsClientConfiguration.class)) + .withBean(ShenyuTarsClientConfigurationTest.class) + .withPropertyValues( + "debug=true", + "shenyu.register.registerType=http", + "shenyu.register.serverLists=http://localhost:9095", + "shenyu.register.props.username=admin", + "shenyu.register.props.password=123456", + "shenyu.client.tars.props[contextPath]=/tars", + "shenyu.client.tars.props[appName]=tars", + "shenyu.client.tars.props[host]=127.0.0.1", + "shenyu.client.tars.props[port]=21715" + ); + } + + @Test + public void testTarsServiceBeanPostProcessor() { + MockedStatic registerUtilsMockedStatic = mockStatic(RegisterUtils.class); + registerUtilsMockedStatic.when(() -> RegisterUtils.doLogin(any(), any(), any())).thenReturn(Optional.ofNullable("token")); + applicationContextRunner.run(context -> { + TarsServiceBeanEventListener listener = context.getBean("tarsServiceBeanEventListener", TarsServiceBeanEventListener.class); + assertNotNull(listener); + }); + registerUtilsMockedStatic.close(); + } +}