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(); + } +}