diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..29f81d812f3e768fa89638d1f72920dbfd1413a8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/pom.xml b/pom.xml
index 9c62529e88194b2f24be2b366878550ca77d93eb..0960df5f0580761fb82e063944e7a29c0fadc2d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,8 @@
2.6.0
5.8.29
+
+ 4.10.0
@@ -75,6 +77,11 @@
fastjson
1.2.83
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
+
org.openjdk.jmh
jmh-core
@@ -152,6 +159,7 @@
+ -parameters
--module-path
${project.build.directory}
diff --git a/quick-landing/dynamic-rest-caller/module-info.md b/quick-landing/dynamic-rest-caller/module-info.md
new file mode 100644
index 0000000000000000000000000000000000000000..b2657924eb2cff57ef74f331a0d029954d4fb69f
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/module-info.md
@@ -0,0 +1,60 @@
+
+> 问题地址:[mercyblitz-训练营-问答互动](https://www.yuque.com/mercyblitz/tech-weekly/lmdkv8x1xm4z6n7k)
+
+### 模块背景
+#### 问题:小马哥,openfeign 如何动态化,就是A服务要调用其他服务,但是一开始我是不知道要调用谁的,所以一开始我没法写好feign接口给A服务用。 怎么做到运行时,调用任意服务的任意接口呢?有点像泛化调用。
+#### 答:
+- 解决方案一:的确是泛化调用,这个需要设计一个特殊的接口,可以参考Dubbo 泛化接口设计,也可以把这个功能放到网关上
+- 解决方案二:搞个 API 网关,OpenFeign 接口去调用网关
+前提:具备一个服务提供接口目录,提供具体 Web Service URI,请求参数(HTTP Request -> 具体方法参数),通过契约/约定的方式让方法参数转换成 HTTP Request
+- 建议:RestTemplate、WebClient
+
+- - -
+
+> 这里使用了 RestTemplate 作为示例,以下是对本模块的具体描述
+
+### 动态REST调用器
+
+这个项目展示了一个使用 Spring Boot 和 DDD 原则实现的健壮、高性能和可扩展的动态服务调用系统。
+
+### 功能特性
+
+- 动态服务发现和调用
+- 可配置的 HTTP 客户端,支持 HttpClient 5 和 OkHttp
+- 可扩展的服务发现机制
+- 使用 Java 17 新特性
+
+### 快速开始
+
+1. 克隆仓库
+2. 构建项目:`mvn clean install`
+3. 运行应用:`java -jar target/dynamic-rest-caller-0.0.1-SNAPSHOT.jar`
+
+### 使用方法
+
+向 `/call` 发送 POST 请求,使用以下 JSON 结构:
+```java
+{
+ "serviceName":"userService",
+ "endpointName":"getUser",
+ "payload":{},
+ "uriVariables":{
+ "id":"123"
+ }
+}
+```
+### 配置
+
+调整 `application.yml` 文件以自定义 HttpClient 和 OkHttp 设置。
+
+### 扩展服务发现
+
+要添加新的服务发现机制:
+
+1. 实现 `ServiceDiscovery` 接口
+2. 在 `ServiceDiscoveryAutoConfiguration` 中添加新的 bean 创建方法
+3. 在 `application.yml` 中更新 `service-discovery.type` 属性
+
+### 贡献
+欢迎提交 Pull Requests 来改进这个项目。对于重大更改,请先开 issue 讨论您想要改变的内容。
+
diff --git a/quick-landing/dynamic-rest-caller/pom.xml b/quick-landing/dynamic-rest-caller/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bb3bd17b2a9c16eb113be57b5ac1e3fa9514114a
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/pom.xml
@@ -0,0 +1,69 @@
+
+ 4.0.0
+
+ cc.magicjson
+ quick-landing
+ ${revision}
+ ../pom.xml
+
+
+ dynamic-rest-caller
+ jar
+
+ dynamic-rest-selector
+ https://gitee.com/MagicJson/learning-training.git
+ 基于 DDD 的动态服务调用器
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+
+ com.squareup.okhttp3
+ okhttp
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/DynamicServiceApplication.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/DynamicServiceApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..4dbf28999d1ee0343a8991b0fa381c40efe317ae
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/DynamicServiceApplication.java
@@ -0,0 +1,16 @@
+package cc.magicjson.caller;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@SpringBootApplication
+public class DynamicServiceApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(DynamicServiceApplication.class, args);
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/application/service/DynamicServiceCaller.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/application/service/DynamicServiceCaller.java
new file mode 100644
index 0000000000000000000000000000000000000000..49d1ed26bfba76fc4298d0fdb5166a786e064495
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/application/service/DynamicServiceCaller.java
@@ -0,0 +1,89 @@
+package cc.magicjson.caller.application.service;
+
+
+import cc.magicjson.caller.domain.discovery.ServiceDiscovery;
+import cc.magicjson.caller.domain.model.Endpoint;
+import cc.magicjson.caller.domain.model.Service;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+import org.springframework.web.util.UriUtils;
+
+import java.util.Map;
+
+/**
+ * 动态调用其他服务的服务类。
+ * 这个类使用 ServiceDiscovery 来定位服务及其端点,
+ * 然后使用 RestTemplate 来进行实际的 HTTP 调用。
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DynamicServiceCaller {
+ private final ServiceDiscovery serviceDiscovery;
+ private final RestTemplate restTemplate;
+
+ /**
+ * 动态调用服务端点。
+ *
+ * @param serviceName 要调用的服务名称
+ * @param endpointName 要调用的端点名称
+ * @param request 请求体
+ * @param responseType 预期的响应类型
+ * @param uriVariables 要替换在 URL 路径中的变量
+ * @param 响应的类型
+ * @return 来自服务调用的响应
+ * @throws IllegalArgumentException 如果找不到服务或端点
+ */
+ public T callService(String serviceName, String endpointName, Object request, Class responseType, Map uriVariables) {
+ Service service = serviceDiscovery.getService(serviceName)
+ .orElseThrow(() -> new IllegalArgumentException("Service not found: " + serviceName));
+
+ Endpoint endpoint = serviceDiscovery.getEndpoint(serviceName, endpointName)
+ .orElseThrow(() -> new IllegalArgumentException("Endpoint not found: " + endpointName));
+
+ String baseUrl = service.url();
+ String path = endpoint.path();
+
+ // 构建 URI
+ UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl).path(path);
+
+ // 添加查询参数(如果有的话)
+ // 处理 URI 变量和查询参数
+ if (uriVariables != null) {
+ for (Map.Entry entry : uriVariables.entrySet()) {
+ String key = entry.getKey();
+ String value = (String) entry.getValue();
+ if (path.contains("{" + key + "}")) {
+ // 这是一个路径变量
+ path = path.replace("{" + key + "}", UriUtils.encodePathSegment(value, "UTF-8"));
+ } else {
+ // 这是一个查询参数
+ builder.queryParam(key, value);
+ }
+ }
+ }
+
+ // 构建最终的 URI
+ String uri = builder.build(false).toUriString();
+
+
+ // 打印拼接后的 URI
+ log.info("Calling service with URI: {}", uri);
+
+ return restTemplate.exchange(
+ uri,
+ HttpMethod.valueOf(endpoint.method()),
+ new HttpEntity<>(request),
+ responseType,
+ uriVariables
+ ).getBody();
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/discovery/ServiceDiscovery.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/discovery/ServiceDiscovery.java
new file mode 100644
index 0000000000000000000000000000000000000000..ade22a896af595b12280e9431e2c2f1b586b6b6a
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/discovery/ServiceDiscovery.java
@@ -0,0 +1,45 @@
+package cc.magicjson.caller.domain.discovery;
+
+
+import cc.magicjson.caller.domain.model.Endpoint;
+import cc.magicjson.caller.domain.model.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 服务发现操作的接口。
+ * 这个接口定义了发现服务及其端点的契约。
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ *
+ */
+public interface ServiceDiscovery {
+ /**
+ * 通过名称检索服务。
+ *
+ * @param serviceName 要检索的服务名称
+ * @return 包含 Service 的 Optional,如果找到则有值,否则为空
+ */
+ Optional getService(String serviceName);
+
+ /**
+ * 检索给定服务的所有端点。
+ *
+ * @param serviceName 要检索端点的服务名称
+ * @return 给定服务的 Endpoint 列表
+ */
+ List getEndpoints(String serviceName);
+
+
+
+ /**
+ * 获取指定服务的指定端点
+ *
+ * @param serviceName 服务名称
+ * @param endpointName 端点名称
+ * @return 包含 Endpoint 的 Optional,如果找到则有值,否则为空
+ */
+ Optional getEndpoint(String serviceName, String endpointName);
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Endpoint.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Endpoint.java
new file mode 100644
index 0000000000000000000000000000000000000000..da2e8b01830d8d752d2f55623b8b2bebbcb254a1
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Endpoint.java
@@ -0,0 +1,14 @@
+package cc.magicjson.caller.domain.model;
+
+/**
+ * 表示服务的一个端点。
+ * 这个记录包含了关于端点的基本信息
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ *
+ * @param name 端点的名称
+ * @param path 端点的路径
+ * @param method 端点的 HTTP 方法
+ */
+public record Endpoint(String name, String path, String method) {}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Service.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Service.java
new file mode 100644
index 0000000000000000000000000000000000000000..5180f94a778b71064026f874b7c4201cc4d9aeba
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Service.java
@@ -0,0 +1,13 @@
+package cc.magicjson.caller.domain.model;
+
+/**
+ * 表示系统中的一个服务。
+ * 这个记录包含了关于服务的基本信息
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ *
+ * @param name 服务的名称
+ * @param url 服务的基础 URL
+ */
+public record Service(String name, String url) {}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/adapter/external/mock/MockServicesController.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/adapter/external/mock/MockServicesController.java
new file mode 100644
index 0000000000000000000000000000000000000000..6368b96d4801857483e16e23842c1207022179ca
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/adapter/external/mock/MockServicesController.java
@@ -0,0 +1,205 @@
+package cc.magicjson.caller.infrastructure.adapter.external.mock;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ *
+ * 模拟服务控制器 其中注释示例为动态Rest服务调用案例
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@RestController
+public class MockServicesController {
+
+ /**
+ * 获取用户信息
+ * 示例请求体:
+ * {
+ * "serviceName": "userService",
+ * "endpointName": "getUserInfo",
+ * "payload": {
+ * "userId": "12345"
+ * },
+ * "uriVariables": {
+ * "id": "12345"
+ * }
+ * }
+ * @param id 用户ID
+ * @return 包含用户信息的ResponseEntity
+ */
+ @GetMapping("/users/{id}")
+ public ResponseEntity getUserInfo(@PathVariable String id) {
+ User user = new User(id, "John Doe", "john.doe@example.com");
+ return ResponseEntity.ok(user);
+ }
+
+ /**
+ * 创建新用户
+ * 示例请求体:
+ * {
+ * "serviceName": "userService",
+ * "endpointName": "createUser",
+ * "payload": {
+ * "name": "John Doe",
+ * "email": "EMAIL"
+ * },
+ * "uriVariables": {
+ * "id": "12345"
+ * }
+ * }
+ * @param user 要创建的用户信息
+ * @return 包含创建的用户信息的ResponseEntity
+ */
+ @PostMapping("/users")
+ public ResponseEntity createUser(@RequestBody User user) {
+ return ResponseEntity.ok(new User(user.name, user.email));
+ }
+
+ /**
+ * 创建新订单
+ * 示例请求体:
+ * {
+ * "serviceName": "orderService",
+ * "endpointName": "createOrder",
+ * "payload": {
+ * "customerId": "67890",
+ * "items": [
+ * {
+ * "productId": "P001",
+ * "quantity": 2
+ * },
+ * {
+ * "productId": "P002",
+ * "quantity": 1
+ * }
+ * ],
+ * "shippingAddress": {
+ * "street": "123 Main St",
+ * "city": "Anytown",
+ * "country": "USA",
+ * "zipCode": "12345"
+ * }
+ * },
+ * "uriVariables": {}
+ * }
+ * @param order 要创建的订单信息
+ * @return 包含创建的订单信息的ResponseEntity
+ */
+ @PostMapping("/orders")
+ public ResponseEntity createOrder(@RequestBody Order order) {
+ return ResponseEntity.ok(
+ new Order(order.customerId, order.items, order.shippingAddress));
+ }
+
+ /**
+ * 获取订单详情
+ * 示例请求体:
+ * {
+ * "serviceName": "orderService",
+ * "endpointName": "getOrderDetails",
+ * "payload": {},
+ * "uriVariables": {
+ * "id": "12345"
+ * }
+ * }
+ * @param id 订单ID
+ * @return 包含订单详情的ResponseEntity
+ */
+ @GetMapping("/orders/{id}")
+ public ResponseEntity getOrderDetails(@PathVariable String id) {
+ Order order = new Order(id, "67890", List.of(
+ new OrderItem("P001", 2),
+ new OrderItem("P002", 1)
+ ), new Address("123 Main St", "Anytown", "USA", "12345"));
+ return ResponseEntity.ok(order);
+ }
+
+ /**
+ * 搜索产品
+ * 示例请求体:
+ * {
+ * "serviceName": "productService",
+ * "endpointName": "searchProducts",
+ * "payload": {},
+ * "uriVariables": {
+ * "category": "electronics",
+ * "minPrice": "100",
+ * "maxPrice": "1000"
+ * }
+ * }
+ * @param category 产品类别
+ * @param minPrice 最低价格
+ * @param maxPrice 最高价格
+ * @return 包含符合条件的产品列表的ResponseEntity
+ */
+ @GetMapping("/products/search")
+ public ResponseEntity> searchProducts(
+ @RequestParam String category,
+ @RequestParam double minPrice,
+ @RequestParam double maxPrice) {
+ List products = List.of(
+ new Product("P001", "Smartphone", "electronics", 599.99),
+ new Product("P002", "Laptop", "electronics", 999.99),
+ new Product("P003", "Headphones", "electronics", 199.99)
+ );
+ List filteredProducts = products.stream()
+ .filter(p -> p.category.equals(category))
+ .filter(p -> p.price >= minPrice && p.price <= maxPrice)
+ .toList();
+ return ResponseEntity.ok(filteredProducts);
+ }
+
+ /**
+ * 获取产品详情
+ * 示例请求体:
+ * {
+ * "serviceName": "productService",
+ * "endpointName": "getProductDetails",
+ * "payload": {},
+ * "uriVariables": {
+ * "id": "P001"
+ * }
+ * }
+ * @param id 产品ID
+ * @return 包含产品详情的ResponseEntity
+ */
+ @GetMapping("/products/{id}")
+ public ResponseEntity getProductDetails(@PathVariable String id) {
+ Product product = new Product(id, "Sample Product", "electronics", 299.99);
+ return ResponseEntity.ok(product);
+ }
+
+
+ record User(String id, String name, String email){
+ User(String name, String email) {
+ this(UUID.randomUUID().toString(), name, email);
+ }
+ }
+
+ record Order(String id, String customerId, List items
+ , Address shippingAddress){
+ Order(String customerId, List items, Address shippingAddress){
+ this(UUID.randomUUID().toString(), customerId, items, shippingAddress);
+ }
+ }
+
+ record OrderItem(String productId, double quantity) {
+ OrderItem(double quantity) {
+ this(UUID.randomUUID().toString(), quantity);
+ }
+ }
+
+ record Address(String street, String city, String country, String zipCode) {
+ }
+
+ record Product(String id, String name, String category, double price) {
+ Product( String name, String category, double price) {
+ this(UUID.randomUUID().toString(), name, category, price);
+ }
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/autoconfigure/ServiceDiscoveryAutoConfiguration.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/autoconfigure/ServiceDiscoveryAutoConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..77048fbdd510fff7a131a90c0a8984fa4f884c82
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/autoconfigure/ServiceDiscoveryAutoConfiguration.java
@@ -0,0 +1,50 @@
+package cc.magicjson.caller.infrastructure.autoconfigure;
+
+
+import cc.magicjson.caller.domain.discovery.ServiceDiscovery;
+
+import cc.magicjson.caller.infrastructure.discovery.SimpleServiceDiscovery;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * 服务发现的自动配置类。
+ * 这个类根据配置设置适当的 ServiceDiscovery bean
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@AutoConfiguration
+public class ServiceDiscoveryAutoConfiguration {
+
+ /**
+ * 如果没有其他 ServiceDiscovery bean 存在,
+ * 且 service-discovery.type 属性设置为 'simple' 或未设置,
+ * 则创建一个 SimpleServiceDiscovery bean。
+ *
+ * @return SimpleServiceDiscovery 实例
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(name = "service-discovery.type", havingValue = "simple", matchIfMissing = true)
+ public ServiceDiscovery simpleServiceDiscovery() {
+ return new SimpleServiceDiscovery();
+ }
+
+ // TODO 预埋-在这里添加其他服务发现实现
+
+// @Bean
+// @ConditionalOnProperty(name = "service-discovery.type", havingValue = "nacos")
+// public ServiceDiscovery nacosServiceDiscovery() {
+// return new NacosServiceDiscovery();
+// }
+//
+// @Bean
+// @ConditionalOnProperty(name = "service-discovery.type", havingValue = "zookeeper")
+// public ServiceDiscovery zookeeperServiceDiscovery() {
+// return new ZookeeperServiceDiscovery();
+// }
+
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/discovery/SimpleServiceDiscovery.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/discovery/SimpleServiceDiscovery.java
new file mode 100644
index 0000000000000000000000000000000000000000..4372eef525d241e4a4ec1eeeb16267aaeb0a22fe
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/discovery/SimpleServiceDiscovery.java
@@ -0,0 +1,102 @@
+package cc.magicjson.caller.infrastructure.discovery;
+
+import cc.magicjson.caller.domain.discovery.ServiceDiscovery;
+import cc.magicjson.caller.domain.model.Endpoint;
+import cc.magicjson.caller.domain.model.Service;
+
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * SimpleDiscovery 类实现了 ServiceDiscovery 接口,提供了一个简单的服务发现机制。
+ * 这个类模拟了一个服务注册表,存储了预定义的服务和端点信息
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Component
+public class SimpleServiceDiscovery implements ServiceDiscovery {
+
+ // 存储服务信息的 Map,键为服务名,值为 Service 对象
+ private final Map services;
+ // 存储端点信息的 Map,键为服务名,值为该服务的端点列表
+ private final Map> endpoints;
+
+ /**
+ * 构造函数,初始化服务和端点信息
+ */
+ public SimpleServiceDiscovery() {
+ services = new HashMap<>();
+ endpoints = new HashMap<>();
+
+ // 初始化预定义的服务和端点
+ initializeServices();
+ }
+
+ /**
+ * 初始化预定义的服务和端点信息
+ * 这个方法模拟了服务注册的过程
+ */
+ private void initializeServices() {
+ // 初始化 userService
+ Service userService = new Service("userService", "http://localhost:8080");
+ services.put("userService", userService);
+ endpoints.put("userService", List.of(
+ new Endpoint("getUserInfo", "/users/{id}", "GET"),
+ new Endpoint("createUser", "/users", "POST")
+ ));
+
+ // 初始化 orderService
+ Service orderService = new Service("orderService", "http://localhost:8080");
+ services.put("orderService", orderService);
+ endpoints.put("orderService", List.of(
+ new Endpoint("createOrder", "/orders", "POST"),
+ new Endpoint("getOrderDetails", "/orders/{id}", "GET")
+ ));
+
+ // 初始化 productService
+ Service productService = new Service("productService", "http://localhost:8080");
+ services.put("productService", productService);
+ endpoints.put("productService", List.of(
+ new Endpoint("searchProducts", "/products/search", "GET"),
+ new Endpoint("getProductDetails", "/products/{id}", "GET")
+ ));
+ }
+
+ /**
+ * 根据服务名获取服务信息
+ * @param serviceName 服务名
+ * @return 包含 Service 对象的 Optional,如果服务不存在则返回空 Optional
+ */
+ @Override
+ public Optional getService(String serviceName) {
+ return Optional.ofNullable(services.get(serviceName));
+ }
+
+ /**
+ * 获取指定服务的所有端点
+ * @param serviceName 服务名
+ * @return 端点列表,如果服务不存在则返回空列表
+ */
+ @Override
+ public List getEndpoints(String serviceName) {
+ return endpoints.getOrDefault(serviceName, List.of());
+ }
+
+ /**
+ * 获取指定服务的特定端点
+ * @param serviceName 服务名
+ * @param endpointName 端点名
+ * @return 包含 Endpoint 对象的 Optional,如果端点不存在则返回空 Optional
+ */
+ @Override
+ public Optional getEndpoint(String serviceName, String endpointName) {
+ return getEndpoints(serviceName).stream()
+ .filter(endpoint -> endpoint.name().equals(endpointName))
+ .findFirst();
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProvider.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..18dc5a048068a411c8965e17c00218625e18fb6b
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProvider.java
@@ -0,0 +1,31 @@
+package cc.magicjson.caller.infrastructure.http;
+
+import cc.magicjson.caller.infrastructure.http.config.HttpClientConfig;
+import cc.magicjson.caller.infrastructure.http.factory.ApacheHttpClientFactory;
+import cc.magicjson.caller.infrastructure.http.factory.HttpClientFactory;
+import cc.magicjson.caller.infrastructure.http.factory.OkHttpClientFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * HTTP 客户端工厂提供器
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Component
+public class HttpClientFactoryProvider {
+
+ private final HttpClientConfig config;
+
+ public HttpClientFactoryProvider(HttpClientConfig config) {
+ this.config = config;
+ }
+
+ public HttpClientFactory getHttpClientFactory() {
+ return switch (config.getType()) {
+ case APACHE -> new ApacheHttpClientFactory(config);
+ case OKHTTP -> new OkHttpClientFactory(config);
+ // 在这里添加其他 HTTP 客户端类型的 case
+ };
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/HttpClientConfig.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/HttpClientConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e6fd45cdfdd802e56f6c7fdbe411e7404c87915
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/HttpClientConfig.java
@@ -0,0 +1,26 @@
+package cc.magicjson.caller.infrastructure.http.config;
+
+import cc.magicjson.caller.infrastructure.http.type.HttpClientType;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+
+/**
+ * http客户端配置类
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Getter
+@Setter
+@Configuration
+@ConfigurationProperties(prefix = "http.client")
+public class HttpClientConfig {
+ private HttpClientType type = HttpClientType.APACHE;
+ private int maxTotal = 100;
+ private int defaultMaxPerRoute = 20;
+ private int connectTimeout = 5000;
+ private int socketTimeout = 65000;
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfig.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..75034738504e567207096d81bbeb06ff037b1b63
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfig.java
@@ -0,0 +1,26 @@
+package cc.magicjson.caller.infrastructure.http.config;
+
+
+import cc.magicjson.caller.infrastructure.http.HttpClientFactoryProvider;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * RestTemplate 配置类
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Configuration
+@RequiredArgsConstructor
+public class RestTemplateConfig {
+
+ private final HttpClientFactoryProvider httpClientFactoryProvider;
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return httpClientFactoryProvider.getHttpClientFactory().createRestTemplate();
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/ApacheHttpClientFactory.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/ApacheHttpClientFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..716a1f601377068543305edb7c740f3f4e83f1fe
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/ApacheHttpClientFactory.java
@@ -0,0 +1,43 @@
+package cc.magicjson.caller.infrastructure.http.factory;
+
+import cc.magicjson.caller.infrastructure.http.config.HttpClientConfig;
+import lombok.RequiredArgsConstructor;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.core5.util.Timeout;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Apache HttpClient 5 的工厂实现
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Component
+@ConditionalOnProperty(name = "http.client.type", havingValue = "apache", matchIfMissing = true)
+@RequiredArgsConstructor
+public class ApacheHttpClientFactory implements HttpClientFactory {
+
+ private final HttpClientConfig config;
+
+ @Override
+ public RestTemplate createRestTemplate() {
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+ connectionManager.setMaxTotal(config.getMaxTotal());
+ connectionManager.setDefaultMaxPerRoute(config.getDefaultMaxPerRoute());
+
+ CloseableHttpClient httpClient = HttpClients.custom()
+ .setConnectionManager(connectionManager)
+ .build();
+
+ HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
+ requestFactory.setConnectTimeout(Timeout.ofMilliseconds(config.getConnectTimeout()).toDuration());
+ requestFactory.setConnectionRequestTimeout(Timeout.ofMilliseconds(config.getSocketTimeout()).toDuration());
+
+ return new RestTemplate(requestFactory);
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/HttpClientFactory.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/HttpClientFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..41d1acf701e394d5711996cb93bd2c36b821fe86
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/HttpClientFactory.java
@@ -0,0 +1,19 @@
+package cc.magicjson.caller.infrastructure.http.factory;
+
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * HTTP 客户端工厂接口
+ * 用于创建 RestTemplate 实例
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+public interface HttpClientFactory {
+ /**
+ * 创建 RestTemplate 实例
+ *
+ * @return 配置好的 RestTemplate
+ */
+ RestTemplate createRestTemplate();
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/OkHttpClientFactory.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/OkHttpClientFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e6d08bad95a64ac11306ff8c0358965be30fd58
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/OkHttpClientFactory.java
@@ -0,0 +1,55 @@
+package cc.magicjson.caller.infrastructure.http.factory;
+
+import cc.magicjson.caller.infrastructure.http.config.HttpClientConfig;
+import cc.magicjson.caller.infrastructure.http.request.OkHttpClientHttpRequest;
+import lombok.RequiredArgsConstructor;
+import okhttp3.OkHttpClient;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * OkHttp 的工厂实现
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Component
+@ConditionalOnProperty(name = "http.client.type", havingValue = "okhttp")
+@RequiredArgsConstructor
+public class OkHttpClientFactory implements HttpClientFactory {
+
+ private final HttpClientConfig config;
+
+ @Override
+ public RestTemplate createRestTemplate() {
+ OkHttpClient okHttpClient = new OkHttpClient.Builder()
+ .connectTimeout(config.getConnectTimeout(), TimeUnit.MILLISECONDS)
+ .readTimeout(config.getSocketTimeout(), TimeUnit.MILLISECONDS)
+ .writeTimeout(config.getSocketTimeout(), TimeUnit.MILLISECONDS)
+ .build();
+
+ ClientHttpRequestFactory requestFactory = new OkHttpClientHttpRequestFactory(okHttpClient);
+ return new RestTemplate(requestFactory);
+ }
+
+ /**
+ * 自定义的 ClientHttpRequestFactory,使用 OkHttpClient
+ */
+ private record OkHttpClientHttpRequestFactory(OkHttpClient okHttpClient) implements ClientHttpRequestFactory {
+
+ @NotNull
+ @Override
+ public ClientHttpRequest createRequest(@NotNull URI uri, @NotNull HttpMethod httpMethod){
+ return new OkHttpClientHttpRequest(okHttpClient, uri, httpMethod);
+ }
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/request/OkHttpClientHttpRequest.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/request/OkHttpClientHttpRequest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a3ebad0c928cbc41277ad1c434a814da9a7c24f1
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/request/OkHttpClientHttpRequest.java
@@ -0,0 +1,85 @@
+package cc.magicjson.caller.infrastructure.http.request;
+
+import cc.magicjson.caller.infrastructure.http.response.OkHttpClientHttpResponse;
+import okhttp3.*;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpResponse;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * OkHttp 的 ClientHttpRequest 实现
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+public class OkHttpClientHttpRequest implements ClientHttpRequest {
+
+ private final OkHttpClient client;
+ private final URI uri;
+ private final HttpMethod method;
+ private final HttpHeaders headers;
+ private final ByteArrayOutputStream bodyStream;
+
+ public OkHttpClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) {
+ this.client = client;
+ this.uri = uri;
+ this.method = method;
+ this.headers = new HttpHeaders();
+ this.bodyStream = new ByteArrayOutputStream(1024);
+ }
+
+ @NotNull
+ @Override
+ public HttpMethod getMethod() {
+ return method;
+ }
+
+ @NotNull
+ @Override
+ public URI getURI() {
+ return uri;
+ }
+
+ @NotNull
+ @Override
+ public HttpHeaders getHeaders() {
+ return headers;
+ }
+
+ @NotNull
+ @Override
+ public OutputStream getBody() throws IOException {
+ return bodyStream;
+ }
+
+ @NotNull
+ @Override
+ public ClientHttpResponse execute() throws IOException {
+ byte[] bytes = bodyStream.toByteArray();
+ RequestBody requestBody = RequestBody.create(bytes, null);
+
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(uri.toURL())
+ .method(method.name(), method == HttpMethod.GET || method == HttpMethod.HEAD ? null : requestBody);
+
+ for (Map.Entry> entry : headers.entrySet()) {
+ for (String value : entry.getValue()) {
+ requestBuilder.addHeader(entry.getKey(), value);
+ }
+ }
+
+ Request request = requestBuilder.build();
+ Response response = client.newCall(request).execute();
+
+ return new OkHttpClientHttpResponse(response);
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/response/OkHttpClientHttpResponse.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/response/OkHttpClientHttpResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..2cfe064190ae335b85e5a6b52d04b55c10e82dd0
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/response/OkHttpClientHttpResponse.java
@@ -0,0 +1,53 @@
+package cc.magicjson.caller.infrastructure.http.response;
+
+import okhttp3.Response;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.client.ClientHttpResponse;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * OkHttp 的 ClientHttpResponse 实现
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+public class OkHttpClientHttpResponse implements ClientHttpResponse {
+
+ private final Response response;
+
+ public OkHttpClientHttpResponse(Response response) {
+ this.response = response;
+ }
+
+ @Override
+ public HttpStatus getStatusCode() throws IOException {
+ return HttpStatus.valueOf(response.code());
+ }
+
+ @Override
+ public String getStatusText() throws IOException {
+ return response.message();
+ }
+
+ @Override
+ public void close() {
+ response.close();
+ }
+
+ @Override
+ public InputStream getBody() throws IOException {
+ return response.body().byteStream();
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ HttpHeaders headers = new HttpHeaders();
+ for (String name : response.headers().names()) {
+ headers.addAll(name, response.headers(name));
+ }
+ return headers;
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/type/HttpClientType.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/type/HttpClientType.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0f386d5a2e61b6c2a2d00dd540a634bcbe003da
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/type/HttpClientType.java
@@ -0,0 +1,13 @@
+package cc.magicjson.caller.infrastructure.http.type;
+
+/**
+ * HTTP 客户端类型枚举类
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+public enum HttpClientType {
+ APACHE,
+ OKHTTP
+ // 可以在此添加其他 HTTP 客户端类型
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/openapi/OpenApiConfig.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/openapi/OpenApiConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..5305d8e5f885cf89826108dca5658b687dab0d9c
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/openapi/OpenApiConfig.java
@@ -0,0 +1,39 @@
+package cc.magicjson.caller.infrastructure.openapi;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import org.springdoc.core.models.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * OpenAPI 配置类
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Configuration
+public class OpenApiConfig {
+
+ @Bean
+ public OpenAPI customOpenAPI() {
+ return new OpenAPI()
+ .info(new Info()
+ .title("Spring Boot 3 API 示例")
+ .version("1.0")
+ .description("这是一个使用 Spring Boot 3 和 SpringDoc 的 API 文档示例")
+ .termsOfService("http://swagger.io/terms/")
+ .contact(new Contact().name("API 支持团队").email("support@example.com"))
+ .license(new License().name("Apache 2.0").url("http://springdoc.org")));
+ }
+
+ @Bean
+ public GroupedOpenApi publicApi() {
+ return GroupedOpenApi.builder()
+ .group("springshop-public")
+ .pathsToMatch("/**")
+ .build();
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/DynamicServiceController.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/DynamicServiceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..878cf167811466b57bc9a4008157da5b55e327bf
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/DynamicServiceController.java
@@ -0,0 +1,50 @@
+package cc.magicjson.caller.interfaces.rest;
+
+import cc.magicjson.caller.application.service.DynamicServiceCaller;
+
+import cc.magicjson.caller.domain.discovery.ServiceDiscovery;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 服务调用控制器
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@RequiredArgsConstructor
+@RestController
+public class DynamicServiceController {
+
+ private final DynamicServiceCaller serviceCaller;
+ private final ServiceDiscovery serviceDiscovery;
+
+ /**
+ * 服务调用
+ *
+ * @param request 服务调用请求 包含服务名称、端点名称、请求参数、URI变量
+ * @return 对应服务调用的响应
+ */
+ @PostMapping("/call")
+ public ResponseEntity> callService(@RequestBody ServiceCallRequest request) {
+ // 检查服务是否存在
+ if (serviceDiscovery.getService(request.getServiceName()).isEmpty()) {
+ return ResponseEntity.badRequest().body("Service not found: " + request.getServiceName());
+ }
+ if (serviceDiscovery.getEndpoint(request.getServiceName(), request.getEndpointName()).isEmpty()) {
+ return ResponseEntity.badRequest().body("Endpoint not found: " + request.getServiceName());
+ }
+
+ Object response = serviceCaller.callService(
+ request.getServiceName(),
+ request.getEndpointName(),
+ request.getPayload(),
+ Object.class,
+ request.getUriVariables()
+ );
+ return ResponseEntity.ok(response);
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/ServiceCallRequest.java b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/ServiceCallRequest.java
new file mode 100644
index 0000000000000000000000000000000000000000..896125e50b2e7a28e501cabd5c39a2780209aa43
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/ServiceCallRequest.java
@@ -0,0 +1,22 @@
+package cc.magicjson.caller.interfaces.rest;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Map;
+
+/**
+ * 服务调用请求对象
+ * 这个类定义了动态服务调用所需的参数
+ *
+ * @author MagicJson
+ * @since 1.0.0
+ */
+@Getter
+@Setter
+public class ServiceCallRequest {
+ private String serviceName; // 要调用的服务名称
+ private String endpointName; // 要调用的端点名称
+ private Object payload; // 请求负载
+ private Map uriVariables; // URL 路径变量
+}
diff --git a/quick-landing/dynamic-rest-caller/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/quick-landing/dynamic-rest-caller/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000000000000000000000000000000000..3c5c0bb1f55a598cc6d5b14acfc62cd2f87064d1
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+cc.magicjson.caller.infrastructure.autoconfigure.ServiceDiscoveryAutoConfiguration
+
diff --git a/quick-landing/dynamic-rest-caller/src/main/resources/application.yml b/quick-landing/dynamic-rest-caller/src/main/resources/application.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5e493b575c8b07847e76fcb19dfc31594ede80f9
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/main/resources/application.yml
@@ -0,0 +1,25 @@
+spring:
+ application:
+ name: dynamic-rest-caller
+springdoc:
+ api-docs:
+ path: /v3/api-docs
+ enabled: true
+ swagger-ui:
+ path: /swagger-ui.html
+ use-management-port: false
+ use-javadoc: true
+
+
+
+http:
+ client:
+ type: APACHE # 或者 OKHTTP
+ max-total: 100 # 连接池最大连接数
+ default-max-per-route: 20 # 每个路由的最大连接数
+ connect-timeout: 5000 # 连接超时时间(毫秒)
+ socket-timeout: 65000 # Socket 读取超时时间(毫秒)
+
+# 服务发现配置
+service-discovery:
+ type: simple # 服务发现类型,可以是 'simple'、'nacos'、'zookeeper' 等
diff --git a/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/application/service/DynamicServiceCallerTest.java b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/application/service/DynamicServiceCallerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5837ea0a52f782e1c191c59368ca7fc1183e11c3
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/application/service/DynamicServiceCallerTest.java
@@ -0,0 +1,113 @@
+package cc.magicjson.caller.application.service;
+
+
+import cc.magicjson.caller.domain.model.Endpoint;
+import cc.magicjson.caller.domain.model.Service;
+import cc.magicjson.caller.domain.discovery.ServiceDiscovery;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class DynamicServiceCallerTest {
+
+ @Mock
+ private ServiceDiscovery serviceDiscovery;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private DynamicServiceCaller serviceCaller;
+
+ /**
+ * 测试成功调用服务的场景
+ */
+ @Test
+ public void callServiceShouldSucceed() {
+ // 准备测试数据
+ String serviceName = "testService";
+ String endpointName = "testEndpoint";
+ String request = "testRequest";
+ Map uriVars = Map.of("id", "123");
+
+ Service service = new Service(serviceName, "http://test.com");
+ Endpoint endpoint = new Endpoint(endpointName, "/api/test/{id}", "GET");
+
+ // 配置模拟行为
+ when(serviceDiscovery.getService(serviceName)).thenReturn(Optional.of(service));
+ when(serviceDiscovery.getEndpoints(serviceName)).thenReturn(List.of(endpoint));
+
+ ResponseEntity expectedResponse = ResponseEntity.ok("testResponse");
+ when(restTemplate.exchange(
+ eq("http://test.com/api/test/{id}"),
+ eq(HttpMethod.GET),
+ any(HttpEntity.class),
+ eq(String.class),
+ eq(uriVars)
+ )).thenReturn(expectedResponse);
+
+ // 执行测试
+ String response = serviceCaller.callService(serviceName, endpointName, request, String.class, uriVars);
+
+ // 验证结果
+ assertEquals("testResponse", response, "响应应匹配预期值");
+ verify(serviceDiscovery).getService(serviceName);
+ verify(serviceDiscovery).getEndpoints(serviceName);
+ verify(restTemplate).exchange(
+ eq("http://test.com/api/test/{id}"),
+ eq(HttpMethod.GET),
+ any(HttpEntity.class),
+ eq(String.class),
+ eq(uriVars)
+ );
+ }
+
+ /**
+ * 参数化测试:验证异常情况
+ */
+ @ParameterizedTest
+ @MethodSource("exceptionTestCases")
+ public void callServiceShouldThrowException(String serviceName, String endpointName,
+ Optional serviceOpt,
+ List endpoints,
+ Class extends Exception> expectedException) {
+ // 配置模拟行为
+ when(serviceDiscovery.getService(serviceName)).thenReturn(serviceOpt);
+ when(serviceDiscovery.getEndpoints(serviceName)).thenReturn(endpoints);
+
+ // 执行测试并验证结果
+ assertThrows(expectedException, () ->
+ serviceCaller.callService(serviceName, endpointName, "request", String.class, Map.of()));
+ }
+
+ /**
+ * 提供异常测试用例
+ */
+ private static Stream exceptionTestCases() {
+ return Stream.of(
+ Arguments.of("nonExistentService", "testEndpoint", Optional.empty(), List.of(), IllegalArgumentException.class),
+ Arguments.of("testService", "nonExistentEndpoint", Optional.of(new Service("testService", "http://test.com")), List.of(), IllegalArgumentException.class)
+ );
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProviderTest.java b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProviderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..32a31f698755a5c499d0fb3ce771cfe347fc1056
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProviderTest.java
@@ -0,0 +1,49 @@
+package cc.magicjson.caller.infrastructure.http;
+
+import cc.magicjson.caller.infrastructure.http.config.HttpClientConfig;
+import cc.magicjson.caller.infrastructure.http.factory.ApacheHttpClientFactory;
+import cc.magicjson.caller.infrastructure.http.factory.HttpClientFactory;
+import cc.magicjson.caller.infrastructure.http.factory.OkHttpClientFactory;
+import cc.magicjson.caller.infrastructure.http.type.HttpClientType;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class HttpClientFactoryProviderTest {
+
+ @Mock
+ private HttpClientConfig config;
+
+ @InjectMocks
+ private HttpClientFactoryProvider provider;
+
+ /**
+ * 测试获取工厂方法
+ * 验证不同HTTP客户端类型是否返回正确的工厂实例
+ * @param type HTTP客户端类型
+ */
+ @ParameterizedTest
+ @EnumSource(HttpClientType.class)
+ public void getFactoryShouldMatchType(HttpClientType type) {
+ // 设置模拟配置返回指定类型
+ when(config.getType()).thenReturn(type);
+
+ // 获取工厂实例
+ HttpClientFactory factory = provider.getHttpClientFactory();
+
+ // 根据类型确定期望的工厂类
+ Class> expectedClass = type == HttpClientType.APACHE ?
+ ApacheHttpClientFactory.class : OkHttpClientFactory.class;
+
+ // 验证返回的工厂实例类型是否符合预期
+ assertTrue(expectedClass.isInstance(factory),
+ String.format("工厂实例应为 %s 类型", expectedClass.getSimpleName()));
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfigTest.java b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfigTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0a54ebc6b46898aa60814e44254723675dbef7b
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfigTest.java
@@ -0,0 +1,49 @@
+package cc.magicjson.caller.infrastructure.http.config;
+
+
+import cc.magicjson.caller.infrastructure.http.HttpClientFactoryProvider;
+import cc.magicjson.caller.infrastructure.http.factory.HttpClientFactory;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.web.client.RestTemplate;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class RestTemplateConfigTest {
+
+ @Mock
+ private HttpClientFactoryProvider factoryProvider;
+
+ @Mock
+ private HttpClientFactory clientFactory;
+
+ @InjectMocks
+ private RestTemplateConfig restTemplateConfig;
+
+ /**
+ * 测试RestTemplate的创建过程
+ * 验证是否正确使用HttpClientFactory创建RestTemplate
+ */
+ @Test
+ public void createRestTemplateShouldUseFactory() {
+ // 准备测试数据
+ RestTemplate expectedTemplate = new RestTemplate();
+
+ // 设置模拟行为
+ when(factoryProvider.getHttpClientFactory()).thenReturn(clientFactory);
+ when(clientFactory.createRestTemplate()).thenReturn(expectedTemplate);
+
+ // 执行测试
+ RestTemplate actualTemplate = restTemplateConfig.restTemplate();
+
+ // 验证结果
+ assertSame(expectedTemplate, actualTemplate, "应返回由HttpClientFactory创建的RestTemplate");
+ verify(factoryProvider).getHttpClientFactory();
+ verify(clientFactory).createRestTemplate();
+ }
+}
diff --git a/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/interfaces/rest/DynamicServiceControllerIntegrationTest.java b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/interfaces/rest/DynamicServiceControllerIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..05296383b0a73d6059c28b050fc54bc642697b9f
--- /dev/null
+++ b/quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/interfaces/rest/DynamicServiceControllerIntegrationTest.java
@@ -0,0 +1,92 @@
+package cc.magicjson.caller.interfaces.rest;
+
+import cc.magicjson.caller.application.service.DynamicServiceCaller;
+
+import cc.magicjson.caller.domain.model.Endpoint;
+import cc.magicjson.caller.domain.model.Service;
+import cc.magicjson.caller.domain.discovery.ServiceDiscovery;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(DynamicServiceController.class)
+public class DynamicServiceControllerIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockBean
+ private DynamicServiceCaller serviceCaller;
+
+ @MockBean
+ private ServiceDiscovery serviceDiscovery;
+
+ /**
+ * 测试有效请求的服务调用
+ */
+ @Test
+ public void callServiceShouldSucceed() throws Exception {
+ // 准备测试数据
+ String serviceName = "testService";
+ String endpointName = "testEndpoint";
+ Map uriVars = Map.of("id", "123");
+
+ ServiceCallRequest request = new ServiceCallRequest();
+ request.setServiceName(serviceName);
+ request.setEndpointName(endpointName);
+ request.setPayload("testPayload");
+ request.setUriVariables(uriVars);
+
+ String expectedResponse = "Test response";
+
+ // 配置模拟行为
+ when(serviceDiscovery.getService(serviceName)).thenReturn(Optional.of(new Service(serviceName, "http://test.com")));
+ when(serviceDiscovery.getEndpoints(serviceName)).thenReturn(List.of(new Endpoint(endpointName, "/api/test", "GET")));
+ when(serviceCaller.callService(eq(serviceName), eq(endpointName), any(), eq(Object.class), eq(uriVars)))
+ .thenReturn(expectedResponse);
+
+ // 执行测试并验证结果
+ mockMvc.perform(post("/call")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isOk())
+ .andExpect(content().string(expectedResponse));
+ }
+
+ /**
+ * 测试服务不存在时的错误处理
+ */
+ @Test
+ public void callServiceShouldFailWhenServiceNotFound() throws Exception {
+ // 准备测试数据
+ ServiceCallRequest request = new ServiceCallRequest();
+ request.setServiceName("nonExistentService");
+ request.setEndpointName("testEndpoint");
+
+ // 配置模拟行为
+ when(serviceDiscovery.getService("nonExistentService")).thenReturn(Optional.empty());
+
+ // 执行测试并验证结果
+ mockMvc.perform(post("/call")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isBadRequest())
+ .andExpect(content().string("Service not found: nonExistentService"));
+ }
+}
diff --git a/quick-landing/pom.xml b/quick-landing/pom.xml
index 46f86b9c93b49b358094060ae74040287cc3404b..7dfb4caf567730eef5152f37aa67153ca011b91c 100644
--- a/quick-landing/pom.xml
+++ b/quick-landing/pom.xml
@@ -13,6 +13,19 @@
quick-landing
https://gitee.com/MagicJson/learning-training.git
-
+
+ dynamic-rest-caller
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
+
+ com.github.therapi
+ therapi-runtime-javadoc
+
+
+