From a06427055cbec75ef9888fbd1ba694a38dfa2c47 Mon Sep 17 00:00:00 2001 From: MagicJson Date: Sat, 7 Sep 2024 19:04:44 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=AF=B9=E8=AF=A5=E9=97=AE=E7=AD=94?= =?UTF-8?q?=E4=BA=92=E5=8A=A8=E9=97=AE=E9=A2=98=E4=B8=80=E4=B8=AD=EF=BC=8C?= =?UTF-8?q?=E5=85=B3=E4=BA=8E=E5=B0=8F=E9=A9=AC=E5=93=A5=E6=8F=90=E5=87=BA?= =?UTF-8?q?=E7=9A=84=E5=BB=BA=E8=AE=AE=E9=83=A8=E5=88=86=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8RestTemplate=E5=AE=9E=E7=8E=B0=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=90=BD=E5=9C=B0[mercyblitz-=E8=AE=AD?= =?UTF-8?q?=E7=BB=83=E8=90=A5-=E9=97=AE=E7=AD=94=E4=BA=92=E5=8A=A8](https:?= =?UTF-8?q?//www.yuque.com/mercyblitz/tech-weekly/lmdkv8x1xm4z6n7k)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 8 + .../dynamic-rest-caller/module-info.md | 60 +++++ quick-landing/dynamic-rest-caller/pom.xml | 69 ++++++ .../caller/DynamicServiceApplication.java | 16 ++ .../service/DynamicServiceCaller.java | 89 ++++++++ .../domain/discovery/ServiceDiscovery.java | 45 ++++ .../caller/domain/model/Endpoint.java | 14 ++ .../caller/domain/model/Service.java | 13 ++ .../external/mock/MockServicesController.java | 205 ++++++++++++++++++ .../ServiceDiscoveryAutoConfiguration.java | 50 +++++ .../discovery/SimpleServiceDiscovery.java | 102 +++++++++ .../http/HttpClientFactoryProvider.java | 31 +++ .../http/config/HttpClientConfig.java | 26 +++ .../http/config/RestTemplateConfig.java | 26 +++ .../http/factory/ApacheHttpClientFactory.java | 43 ++++ .../http/factory/HttpClientFactory.java | 19 ++ .../http/factory/OkHttpClientFactory.java | 55 +++++ .../http/request/OkHttpClientHttpRequest.java | 85 ++++++++ .../response/OkHttpClientHttpResponse.java | 53 +++++ .../http/type/HttpClientType.java | 13 ++ .../infrastructure/openapi/OpenApiConfig.java | 39 ++++ .../rest/DynamicServiceController.java | 50 +++++ .../interfaces/rest/ServiceCallRequest.java | 22 ++ ...ot.autoconfigure.AutoConfiguration.imports | 2 + .../src/main/resources/application.yml | 25 +++ .../service/DynamicServiceCallerTest.java | 113 ++++++++++ .../http/HttpClientFactoryProviderTest.java | 49 +++++ .../http/config/RestTemplateConfigTest.java | 49 +++++ ...namicServiceControllerIntegrationTest.java | 92 ++++++++ quick-landing/pom.xml | 15 +- 30 files changed, 1477 insertions(+), 1 deletion(-) create mode 100644 quick-landing/dynamic-rest-caller/module-info.md create mode 100644 quick-landing/dynamic-rest-caller/pom.xml create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/DynamicServiceApplication.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/application/service/DynamicServiceCaller.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/discovery/ServiceDiscovery.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Endpoint.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/domain/model/Service.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/adapter/external/mock/MockServicesController.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/autoconfigure/ServiceDiscoveryAutoConfiguration.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/discovery/SimpleServiceDiscovery.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProvider.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/HttpClientConfig.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfig.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/ApacheHttpClientFactory.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/HttpClientFactory.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/factory/OkHttpClientFactory.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/request/OkHttpClientHttpRequest.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/response/OkHttpClientHttpResponse.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/http/type/HttpClientType.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/infrastructure/openapi/OpenApiConfig.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/DynamicServiceController.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/java/cc/magicjson/caller/interfaces/rest/ServiceCallRequest.java create mode 100644 quick-landing/dynamic-rest-caller/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 quick-landing/dynamic-rest-caller/src/main/resources/application.yml create mode 100644 quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/application/service/DynamicServiceCallerTest.java create mode 100644 quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/HttpClientFactoryProviderTest.java create mode 100644 quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/infrastructure/http/config/RestTemplateConfigTest.java create mode 100644 quick-landing/dynamic-rest-caller/src/test/java/cc/magicjson/caller/interfaces/rest/DynamicServiceControllerIntegrationTest.java diff --git a/pom.xml b/pom.xml index 9c62529..0960df5 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 0000000..b265792 --- /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 0000000..bb3bd17 --- /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 0000000..4dbf289 --- /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 0000000..49d1ed2 --- /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 0000000..ade22a8 --- /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 0000000..da2e8b0 --- /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 0000000..5180f94 --- /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 0000000..6368b96 --- /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 0000000..77048fb --- /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 0000000..4372eef --- /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 0000000..18dc5a0 --- /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 0000000..0e6fd45 --- /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 0000000..7503473 --- /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 0000000..716a1f6 --- /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 0000000..41d1acf --- /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 0000000..4e6d08b --- /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 0000000..a3ebad0 --- /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 0000000..2cfe064 --- /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 0000000..f0f386d --- /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 0000000..5305d8e --- /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 0000000..878cf16 --- /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 0000000..896125e --- /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 0000000..3c5c0bb --- /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 0000000..5e493b5 --- /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 0000000..5837ea0 --- /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 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 0000000..32a31f6 --- /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 0000000..e0a54eb --- /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 0000000..0529638 --- /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 46f86b9..7dfb4ca 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 + + + -- Gitee From 2212dfcc70a57a055856085f553d0829755a4968 Mon Sep 17 00:00:00 2001 From: MagicJson Date: Sat, 7 Sep 2024 11:07:14 +0000 Subject: [PATCH 2/4] add LICENSE. Signed-off-by: MagicJson --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29f81d8 --- /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. -- Gitee From 442305751ccc9b3649af200e9152822c3c48c30b Mon Sep 17 00:00:00 2001 From: MagicJson Date: Sat, 7 Sep 2024 20:21:24 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- minigame-demo-ddd/pom.xml | 6 --- .../src/test/java/cc/magicjson/AppTest.java | 38 --------------- .../PersonSpringBootStarterApplication.java | 13 ----- .../spring/boot/web/PersonController.java | 47 ------------------- .../main/resources/application.properties.bak | 5 -- .../LobeChat.yml | 0 6 files changed, 109 deletions(-) delete mode 100644 minigame-demo-ddd/src/test/java/cc/magicjson/AppTest.java delete mode 100644 person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/PersonSpringBootStarterApplication.java delete mode 100644 person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/web/PersonController.java delete mode 100644 person-spring-boot-starter/src/main/resources/application.properties.bak rename {demo/src/main/resources => uml-adapter-architecture}/LobeChat.yml (100%) diff --git a/minigame-demo-ddd/pom.xml b/minigame-demo-ddd/pom.xml index 7904326..245c8ec 100644 --- a/minigame-demo-ddd/pom.xml +++ b/minigame-demo-ddd/pom.xml @@ -38,11 +38,5 @@ lombok true - - junit - junit - 3.8.1 - test - diff --git a/minigame-demo-ddd/src/test/java/cc/magicjson/AppTest.java b/minigame-demo-ddd/src/test/java/cc/magicjson/AppTest.java deleted file mode 100644 index 5f05c66..0000000 --- a/minigame-demo-ddd/src/test/java/cc/magicjson/AppTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package cc.magicjson; - -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; - -/** - * Unit test for simple App. - */ -public class AppTest - extends TestCase -{ - /** - * Create the test case - * - * @param testName name of the test case - */ - public AppTest( String testName ) - { - super( testName ); - } - - /** - * @return the suite of tests being tested - */ - public static Test suite() - { - return new TestSuite( AppTest.class ); - } - - /** - * Rigourous Test :-) - */ - public void testApp() - { - assertTrue( true ); - } -} diff --git a/person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/PersonSpringBootStarterApplication.java b/person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/PersonSpringBootStarterApplication.java deleted file mode 100644 index e261cdf..0000000 --- a/person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/PersonSpringBootStarterApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -//package cc.magicjson.spring.boot; -// -//import org.springframework.boot.SpringApplication; -//import org.springframework.boot.autoconfigure.SpringBootApplication; -// -//@SpringBootApplication -//public class PersonSpringBootStarterApplication { -// -// public static void main(String[] args) { -// SpringApplication.run(PersonSpringBootStarterApplication.class, args); -// } -// -//} diff --git a/person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/web/PersonController.java b/person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/web/PersonController.java deleted file mode 100644 index a07ec20..0000000 --- a/person-spring-boot-starter/src/main/java/cc/magicjson/spring/boot/web/PersonController.java +++ /dev/null @@ -1,47 +0,0 @@ -//package cc.magicjson.spring.boot.web; -// -//import cc.magicjson.spring.boot.domain.Person; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.util.StringUtils; -//import org.springframework.web.bind.annotation.GetMapping; -//import org.springframework.web.bind.annotation.PathVariable; -//import org.springframework.web.bind.annotation.RestController; -// -//import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -// -///** -// * {@link} -// * -// * @author MagicJson -// * @since 1.0.0 -// */ -//@RestController -//public class PersonController { -// private final Person person; -// -// @Autowired -// private PersonController(Person person){ -// this.person = person; -// } -// -// @GetMapping(value = "/person/{id}",produces = APPLICATION_JSON_VALUE) -// public Person person(@PathVariable("id") String id){ -// if(StringUtils.hasLength(id)){ -// person.setId(Long.parseLong(id)); -// } -// return person; -// } -// -// @GetMapping(value = "/person") -// public Person person(){ -// return person; -// } -// -// @GetMapping(value = "/hello") -// public String hello(){ -// return "hello"; -// } -// -// -// -//} diff --git a/person-spring-boot-starter/src/main/resources/application.properties.bak b/person-spring-boot-starter/src/main/resources/application.properties.bak deleted file mode 100644 index dc20775..0000000 --- a/person-spring-boot-starter/src/main/resources/application.properties.bak +++ /dev/null @@ -1,5 +0,0 @@ -spring.application.name=person-spring-boot-starter -person.id=1 -person.name=magicjson -person.age=25 -person.enable=true \ No newline at end of file diff --git a/demo/src/main/resources/LobeChat.yml b/uml-adapter-architecture/LobeChat.yml similarity index 100% rename from demo/src/main/resources/LobeChat.yml rename to uml-adapter-architecture/LobeChat.yml -- Gitee From 012903edb572e11ce455a80fa02df7dcc9d0fa98 Mon Sep 17 00:00:00 2001 From: MagicJson Date: Mon, 9 Sep 2024 11:33:12 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E7=9A=84=E5=8F=AF=E6=8C=89=E7=85=A7=E9=A2=84=E8=AE=BE=E8=BD=BB?= =?UTF-8?q?=E9=87=8D=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C=E7=9A=84=E7=BA=BF?= =?UTF-8?q?=E7=A8=8B=E6=B1=A0=20=E7=9B=AE=E5=89=8D=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=9C=89=E4=BA=9B=E9=97=AE=E9=A2=98=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E4=B8=8B=E6=89=A7=E8=A1=8C=E5=A4=9A=E4=B8=AA=E6=9D=83=E9=87=8D?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=20=E5=B9=B6=E6=9C=AA=E5=85=A8=E6=8C=89?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=BA=A7=E8=AE=BE=E5=AE=9A=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=20=E9=9C=80=E8=A6=81=E5=90=8E=E7=BB=AD=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E5=90=8E=E8=BF=9B=E8=A1=8C=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/config/EventConfig.java | 16 ++ .../dynamic-priority-executors/pom.xml | 31 +++ .../core/DynamicPriorityThreadPool.java | 207 ++++++++++++++++++ .../priority/task/DefaultTaskClassifier.java | 34 +++ .../priority/task/PriorityFutureTask.java | 39 ++++ .../priority/task/PriorityTask.java | 132 +++++++++++ .../priority/task/TaskClassifier.java | 16 ++ .../concurrent/priority/task/TaskType.java | 80 +++++++ .../test/DynamicPriorityThreadPoolTest.java | 131 +++++++++++ quick-landing/pom.xml | 1 + 10 files changed, 687 insertions(+) create mode 100644 minigame-demo-ddd/src/main/java/cc/magicjson/minigame/demo/ddd/infrastructure/config/EventConfig.java create mode 100644 quick-landing/dynamic-priority-executors/pom.xml create mode 100644 quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/core/DynamicPriorityThreadPool.java create mode 100644 quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/DefaultTaskClassifier.java create mode 100644 quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityFutureTask.java create mode 100644 quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityTask.java create mode 100644 quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskClassifier.java create mode 100644 quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskType.java create mode 100644 quick-landing/dynamic-priority-executors/src/test/java/cc/magicjson/concurrent/priority/test/DynamicPriorityThreadPoolTest.java diff --git a/minigame-demo-ddd/src/main/java/cc/magicjson/minigame/demo/ddd/infrastructure/config/EventConfig.java b/minigame-demo-ddd/src/main/java/cc/magicjson/minigame/demo/ddd/infrastructure/config/EventConfig.java new file mode 100644 index 0000000..b995070 --- /dev/null +++ b/minigame-demo-ddd/src/main/java/cc/magicjson/minigame/demo/ddd/infrastructure/config/EventConfig.java @@ -0,0 +1,16 @@ +package cc.magicjson.minigame.demo.ddd.infrastructure.config; + +import cc.magicjson.minigame.demo.ddd.infrastructure.event.AntiCorruptionDomainEventPublisher; +import cc.magicjson.minigame.demo.ddd.infrastructure.event.GameEventListener; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class EventConfig { + private final AntiCorruptionDomainEventPublisher eventPublisher; + private final GameEventListener gameEventListener; + + public EventConfig(AntiCorruptionDomainEventPublisher eventPublisher, GameEventListener gameEventListener) { + this.eventPublisher = eventPublisher; + this.gameEventListener = gameEventListener; + } +} \ No newline at end of file diff --git a/quick-landing/dynamic-priority-executors/pom.xml b/quick-landing/dynamic-priority-executors/pom.xml new file mode 100644 index 0000000..e8eae5a --- /dev/null +++ b/quick-landing/dynamic-priority-executors/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + cc.magicjson + quick-landing + ${revision} + ../pom.xml + + + dynamic-priority-executors + jar + + dynamic-priority-executors + + + + + + org.springframework.boot + spring-boot-starter-test + + + + + org.projectlombok + lombok + true + + + diff --git a/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/core/DynamicPriorityThreadPool.java b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/core/DynamicPriorityThreadPool.java new file mode 100644 index 0000000..640e7ae --- /dev/null +++ b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/core/DynamicPriorityThreadPool.java @@ -0,0 +1,207 @@ +package cc.magicjson.concurrent.priority.core; + +import cc.magicjson.concurrent.priority.task.*; +import jakarta.validation.constraints.NotNull; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 动态优先级线程池,扩展了ThreadPoolExecutor,实现了自动关闭功能。 + * 这个线程池能够根据任务类型动态调整执行优先级,并保证最小数量的线程用于处理轻量级任务。 + */ +public class DynamicPriorityThreadPool extends ThreadPoolExecutor implements AutoCloseable { + + // 追踪当前正在执行轻量级任务的线程数 + private final AtomicInteger lightTaskThreads = new AtomicInteger(0); + // 保证处理轻量级任务的最小线程数 + private final int minLightTaskThreads; + // 用于分类提交的任务 + private final TaskClassifier taskClassifier; + + /** + * 构造函数 + * @param corePoolSize 核心线程数 + * @param maximumPoolSize 最大线程数 + * @param keepAliveTime 空闲线程存活时间 + * @param unit 时间单位 + * @param workQueue 工作队列 + * @param threadFactory 线程工厂 + * @param handler 拒绝策略 + * @param minLightTaskThreads 最小轻量级任务线程数 + * @param taskClassifier 任务分类器 + */ + public DynamicPriorityThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, @NotNull TimeUnit unit, + @NotNull BlockingQueue workQueue, @NotNull ThreadFactory threadFactory, + @NotNull RejectedExecutionHandler handler, int minLightTaskThreads, + @NotNull TaskClassifier taskClassifier) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + this.minLightTaskThreads = minLightTaskThreads; + this.taskClassifier = taskClassifier; + } + + /** + * 在任务执行前调用,用于追踪轻量级任务的执行情况 + */ + @Override + protected void beforeExecute(@NotNull Thread t, @NotNull Runnable r) { + super.beforeExecute(t, r); + if (r instanceof PriorityTask task && task.getTaskType().isLightWeight()) { + lightTaskThreads.incrementAndGet(); + } + } + + /** + * 在任务执行后调用,用于更新轻量级任务的计数 + */ + @Override + protected void afterExecute(@NotNull Runnable r, Throwable t) { + super.afterExecute(r, t); + if (r instanceof PriorityTask task && task.getTaskType().isLightWeight()) { + lightTaskThreads.decrementAndGet(); + } + } + + /** + * 执行提交的任务,确保轻量级任务得到优先处理 + */ + @Override + public void execute(Runnable command) { + PriorityTask task; + if (command instanceof PriorityTask) { + task = (PriorityTask) command; + } else { + TaskType taskType = taskClassifier.classifyTask(command); + task = PriorityTask.createRunnableTask(command, taskType); + } + + if (task.getTaskType().isLightWeight() && lightTaskThreads.get() < minLightTaskThreads) { + super.execute(() -> { + lightTaskThreads.incrementAndGet(); + try { + task.run(); + } finally { + lightTaskThreads.decrementAndGet(); + } + }); + } else { + super.execute(task); + } + } + + /** + * 创建新的RunnableFuture任务 + */ + @NotNull + @Override + protected RunnableFuture newTaskFor(@NotNull Runnable runnable, T value) { + if (runnable instanceof PriorityTask) { + @SuppressWarnings("unchecked") + PriorityTask castedTask = (PriorityTask) runnable; + return new PriorityFutureTask<>(castedTask); + } + TaskType taskType = taskClassifier.classifyTask(runnable); + return new PriorityFutureTask<>(PriorityTask.createCallableTask(() -> { + runnable.run(); + return value; + }, taskType)); + } + + /** + * 创建新的RunnableFuture任务(针对Callable) + */ + @NotNull + @Override + protected RunnableFuture newTaskFor(@NotNull Callable callable) { + if (callable instanceof PriorityTask priorityTask) { + @SuppressWarnings("unchecked") + PriorityTask castedTask = (PriorityTask) priorityTask; + return new PriorityFutureTask<>(castedTask); + } + TaskType taskType = taskClassifier.classifyTask(() -> { + try { + callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return new PriorityFutureTask<>(new PriorityTask<>(callable, taskType)); + } + + /** + * 关闭线程池,实现AutoCloseable接口 + */ + @Override + public void close() { + shutdown(); + try { + if (!awaitTermination(60, TimeUnit.SECONDS)) { + shutdownNow(); + if (!awaitTermination(60, TimeUnit.SECONDS)) { + System.err.println("Pool did not terminate"); + } + } + } catch (InterruptedException ie) { + shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + public static class Builder { + private int corePoolSize = Runtime.getRuntime().availableProcessors(); + private int maximumPoolSize = corePoolSize * 2; + private long keepAliveTime = 60L; + private TimeUnit unit = TimeUnit.SECONDS; + private BlockingQueue workQueue = new PriorityBlockingQueue<>(); + private ThreadFactory threadFactory = Executors.defaultThreadFactory(); + private RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); + private int minLightTaskThreads = 1; + private TaskClassifier taskClassifier = new DefaultTaskClassifier(); + + public Builder corePoolSize(int corePoolSize) { + this.corePoolSize = corePoolSize; + return this; + } + + public Builder maximumPoolSize(int maximumPoolSize) { + this.maximumPoolSize = maximumPoolSize; + return this; + } + + public Builder keepAliveTime(long keepAliveTime, TimeUnit unit) { + this.keepAliveTime = keepAliveTime; + this.unit = unit; + return this; + } + + public Builder workQueue(BlockingQueue workQueue) { + this.workQueue = workQueue; + return this; + } + + public Builder threadFactory(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + return this; + } + + public Builder rejectedExecutionHandler(RejectedExecutionHandler handler) { + this.handler = handler; + return this; + } + + public Builder minLightTaskThreads(int minLightTaskThreads) { + this.minLightTaskThreads = minLightTaskThreads; + return this; + } + + public Builder taskClassifier(TaskClassifier taskClassifier) { + this.taskClassifier = taskClassifier; + return this; + } + + public DynamicPriorityThreadPool build() { + return new DynamicPriorityThreadPool(corePoolSize, maximumPoolSize, keepAliveTime, unit, + workQueue, threadFactory, handler, minLightTaskThreads, taskClassifier); + } + } +} diff --git a/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/DefaultTaskClassifier.java b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/DefaultTaskClassifier.java new file mode 100644 index 0000000..5780760 --- /dev/null +++ b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/DefaultTaskClassifier.java @@ -0,0 +1,34 @@ +package cc.magicjson.concurrent.priority.task; + +/** + * DefaultTaskClassifier 是 TaskClassifier 接口的默认实现。 + * 它通过检查任务类名中的关键字来确定任务类型。 + */ +public class DefaultTaskClassifier implements TaskClassifier { + + /** + * 根据任务的类名对任务进行分类。 + * + * @param task 需要分类的 Runnable 任务 + * @return 分类后的 TaskType + */ + @Override + public TaskType classifyTask(Runnable task) { + // 获取任务的简单类名 + String taskName = task.getClass().getSimpleName(); + + // 根据类名中的关键字判断任务类型 + if (taskName.contains("FileRead")) { + return TaskType.FILE_READ; + } else if (taskName.contains("DNS")) { + return TaskType.DNS_LOOKUP; + } else if (taskName.contains("Database")) { + return TaskType.DATABASE_QUERY; + } else if (taskName.contains("Network")) { + return TaskType.NETWORK_IO; + } + + // 如果没有匹配到特定类型,默认为计算任务 + return TaskType.COMPUTATION; + } +} diff --git a/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityFutureTask.java b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityFutureTask.java new file mode 100644 index 0000000..87b2094 --- /dev/null +++ b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityFutureTask.java @@ -0,0 +1,39 @@ +package cc.magicjson.concurrent.priority.task; + +import java.util.concurrent.FutureTask; + +/** + * PriorityFutureTask 类扩展了 FutureTask,并实现了 Comparable 接口。 + * 这个类用于在线程池中包装 PriorityTask,使其可以根据优先级进行排序。 + * + * @param 任务结果的类型 + */ +public class PriorityFutureTask extends FutureTask implements Comparable> { + + /** + * 被包装的 PriorityTask 实例 + */ + private final PriorityTask priorityTask; + + /** + * 构造一个新的 PriorityFutureTask + * + * @param task 要包装的 PriorityTask + */ + public PriorityFutureTask(PriorityTask task) { + super(task); + this.priorityTask = task; + } + + /** + * 比较此 PriorityFutureTask 与另一个 PriorityFutureTask 的优先级 + * + * @param o 要比较的其他 PriorityFutureTask + * @return 负数表示此任务优先级更高,正数表示其他任务优先级更高,0 表示优先级相同 + */ + @Override + public int compareTo(PriorityFutureTask o) { + // 委托给内部 PriorityTask 的 compareTo 方法 + return this.priorityTask.compareTo(o.priorityTask); + } +} diff --git a/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityTask.java b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityTask.java new file mode 100644 index 0000000..01381a9 --- /dev/null +++ b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/PriorityTask.java @@ -0,0 +1,132 @@ +package cc.magicjson.concurrent.priority.task; + +import java.util.concurrent.Callable; + + +/** + * PriorityTask 类表示一个具有优先级的任务。 + * 它实现了 Callable 接口,同时也实现了 Comparable 接口以支持优先级比较。 + * + * @param 任务执行结果的类型 + */ +public class PriorityTask implements Runnable, Callable, Comparable> { + + // 存储 Callable 类型的任务 + private final Callable task; + + // 任务的类型,用于确定任务的特性(如是否为轻量级任务) + private final TaskType taskType; + + // 任务的优先级,数值越小优先级越高 + private final int priority; + + // 任务创建的时间戳,用于在优先级相同时进行比较 + private final long creationTime; + + public PriorityTask(Callable task, TaskType taskType) { + this.task = task; + this.taskType = taskType; + this.priority = taskType.getPriority(); + this.creationTime = System.nanoTime(); + } + + /** + * 实现 Callable 接口的 call 方法 + * + * @return 任务执行的结果 + * @throws Exception 如果任务执行过程中抛出异常 + */ + @Override + public V call() throws Exception { + return task.call(); + } + + /** + * 实现 Runnable 接口的 run 方法 + * + * @throws Exception 如果任务执行过程中抛出异常 + */ + @Override + public void run() { + try { + call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取任务类型 + * + * @return 任务类型 + */ + public TaskType getTaskType() { + return taskType; + } + + /** + * 获取任务优先级 + * + * @return 任务优先级 + */ + public int getPriority() { + return priority; + } + + /** + * 获取任务创建时间 + * + * @return 任务创建时间(纳秒) + */ + public long getCreationTime() { + return creationTime; + } + + /** + * 实现 Comparable 接口的 compareTo 方法 + * 首先比较优先级,优先级相同时比较创建时间 + * + * @param other 要比较的其他 PriorityTask + * @return 比较结果:负数表示此任务优先级更高,正数表示其他任务优先级更高,0表示优先级相同 + */ + @Override + public int compareTo(PriorityTask other) { + // 优先级数字越小,优先级越高 + int priorityCompare = Integer.compare(this.getTaskType().getPriority(), other.getTaskType().getPriority()); + if (priorityCompare != 0) { + return priorityCompare; + } + // 如果优先级相同,比较创建时间 + return Long.compare(this.getCreationTime(), other.getCreationTime()); + } + + /** + * 创建 Callable 版本的 PriorityTask 的工厂方法 + * + * @param callable Callable 任务 + * @param taskType 任务类型 + * @param Callable 的返回类型 + * @return 包装了 Callable 的 PriorityTask + */ + public static PriorityTask createCallableTask(Callable callable, TaskType taskType) { + return new PriorityTask<>(callable, taskType); + } + + /** + * 创建 Runnable 版本的 PriorityTask 的工厂方法 + * + * @param runnable Runnable 任务 + * @param taskType 任务类型 + * @return 包装了 Runnable 的 PriorityTask + */ + public static PriorityTask createRunnableTask(Runnable runnable, TaskType taskType) { + return new PriorityTask<>(() -> { + runnable.run(); + return null; + }, taskType); + } + + public Callable asCallable() { + return this; + } +} diff --git a/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskClassifier.java b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskClassifier.java new file mode 100644 index 0000000..9ecd966 --- /dev/null +++ b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskClassifier.java @@ -0,0 +1,16 @@ +package cc.magicjson.concurrent.priority.task; + +/** + * TaskClassifier 接口定义了任务分类器的行为。 + * 实现此接口的类负责将提交的 Runnable 任务分类为预定义的 TaskType。 + */ +public interface TaskClassifier { + + /** + * 对给定的任务进行分类。 + * + * @param task 需要分类的 Runnable 任务 + * @return 分类后的 TaskType + */ + TaskType classifyTask(Runnable task); +} diff --git a/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskType.java b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskType.java new file mode 100644 index 0000000..29025cf --- /dev/null +++ b/quick-landing/dynamic-priority-executors/src/main/java/cc/magicjson/concurrent/priority/task/TaskType.java @@ -0,0 +1,80 @@ +package cc.magicjson.concurrent.priority.task; + +import lombok.Getter; + +/** + * TaskType 枚举定义了系统中不同类型的任务及其优先级和轻量级标志。 + * 这个枚举用于在动态优先级线程池中对任务进行分类和优先级排序。 + */ +public enum TaskType { + /** + * 文件读取任务 + * 优先级最高(1),被视为轻量级任务 + * 通常用于快速的文件I/O操作 + */ + FILE_READ(1, true), + + /** + * DNS查询任务 + * 优先级最低(5),不被视为轻量级任务 + * 用于网络相关的DNS解析操作 + */ + DNS_LOOKUP(5, false), + + /** + * 数据库查询任务 + * 优先级中等(3),不被视为轻量级任务 + * 用于数据库操作,可能涉及复杂查询或事务 + */ + DATABASE_QUERY(3, false), + + /** + * 计算任务 + * 优先级较低(4),不被视为轻量级任务 + * 用于CPU密集型的计算操作 + */ + COMPUTATION(4, false), + + /** + * 网络I/O任务 + * 优先级较高(2),不被视为轻量级任务 + * 用于一般的网络通信操作 + */ + NETWORK_IO(2, false); + + /** + * 任务的优先级 + * 数值越小,优先级越高 + * -- GETTER -- + * 获取任务的优先级 + * + */ + @Getter + private final int priority; + + /** + * 标志是否为轻量级任务 + * 轻量级任务可能会得到特殊处理,如优先执行 + */ + private final boolean isLightWeight; + + /** + * TaskType 的构造函数 + * + * @param priority 任务的优先级,数值越小优先级越高 + * @param isLightWeight 是否为轻量级任务的标志 + */ + TaskType(int priority, boolean isLightWeight) { + this.priority = priority; + this.isLightWeight = isLightWeight; + } + + /** + * 判断任务是否为轻量级任务 + * + * @return 如果是轻量级任务返回 true,否则返回 false + */ + public boolean isLightWeight() { + return isLightWeight; + } +} diff --git a/quick-landing/dynamic-priority-executors/src/test/java/cc/magicjson/concurrent/priority/test/DynamicPriorityThreadPoolTest.java b/quick-landing/dynamic-priority-executors/src/test/java/cc/magicjson/concurrent/priority/test/DynamicPriorityThreadPoolTest.java new file mode 100644 index 0000000..b7e343f --- /dev/null +++ b/quick-landing/dynamic-priority-executors/src/test/java/cc/magicjson/concurrent/priority/test/DynamicPriorityThreadPoolTest.java @@ -0,0 +1,131 @@ +package cc.magicjson.concurrent.priority.test; + +import cc.magicjson.concurrent.priority.core.DynamicPriorityThreadPool; +import cc.magicjson.concurrent.priority.task.PriorityTask; +import cc.magicjson.concurrent.priority.task.TaskType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +public class DynamicPriorityThreadPoolTest { + + private record TaskExecution(TaskType type, Instant startTime, Instant endTime, int executionOrder) { + } + + @Test + @Timeout(60) + public void testTaskPriorities() { + try (var pool = new DynamicPriorityThreadPool.Builder() + .corePoolSize(2) + .maximumPoolSize(4) + .minLightTaskThreads(1) + .build()) { + + var executionOrder = new AtomicInteger(0); + var startSignal = new CompletableFuture(); + + System.out.println("Submitting tasks..."); + List> taskFutures = Arrays.stream(TaskType.values()) + .map(taskType -> { + System.out.println("Submitting task: " + taskType); + return createTask(taskType, executionOrder, startSignal, pool); + }) + .toList(); + + System.out.println("Starting all tasks"); + startSignal.complete(null); + + List results = CompletableFuture.allOf(taskFutures.toArray(CompletableFuture[]::new)) + .thenApply(v -> taskFutures.stream() + .map(CompletableFuture::join) + .toList()) + .orTimeout(30, TimeUnit.SECONDS) + .exceptionally(ex -> { + fail("Task execution failed or timed out: " + ex.getMessage()); + return List.of(); + }) + .join(); + + System.out.println("All tasks completed"); + verifyExecutionOrder(results); + verifyLightWeightTaskExecution(results); + printExecutionStatistics(results); + } + } + + private CompletableFuture createTask(TaskType taskType, AtomicInteger executionOrder, CompletableFuture startSignal, DynamicPriorityThreadPool pool) { + return startSignal.thenCompose(ignored -> { + PriorityTask task = PriorityTask.createCallableTask(() -> { + var start = Instant.now(); + try { + // 使用固定的睡眠时间,以避免执行时间影响优先级顺序 + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + var end = Instant.now(); + var order = executionOrder.incrementAndGet(); + System.out.printf("Task %s completed. Order: %d%n", taskType, order); + return new TaskExecution(taskType, start, end, order); + }, taskType); + + return CompletableFuture.supplyAsync(() -> { + try { + return pool.submit((Callable) task).get(); + } catch (Exception e) { + throw new CompletionException(e); + } + }); + }); + } + + private void verifyExecutionOrder(List results) { + for (int i = 0; i < results.size() - 1; i++) { + var current = results.get(i); + var next = results.get(i + 1); + assertTrue(current.type().getPriority() <= next.type().getPriority(), + () -> String.format("Tasks should be executed in priority order. %s (priority %d) " + + "should be executed before or at the same time as %s (priority %d)", + current.type(), current.type().getPriority(), + next.type(), next.type().getPriority())); + } + + var firstTask = results.get(0); + assertTrue(firstTask.type().isLightWeight(), + () -> "The first executed task should be a light-weight task. Actual: " + firstTask.type()); + } + + private void verifyLightWeightTaskExecution(List results) { + var lightWeightTasks = results.stream() + .filter(task -> task.type().isLightWeight()) + .toList(); + + assertFalse(lightWeightTasks.isEmpty(), "There should be at least one light-weight task"); + + var firstTaskStartTime = lightWeightTasks.get(0).startTime(); + for (var task : lightWeightTasks) { + var executionDelay = Duration.between(firstTaskStartTime, task.startTime()); + assertTrue(executionDelay.toMillis() < 50, + () -> String.format("Light-weight task %s should start within 50ms of the first task. " + + "Actual delay: %d ms", task.type(), executionDelay.toMillis())); + } + } + + private void printExecutionStatistics(List results) { + System.out.println("\nTask Execution Statistics:"); + results.forEach(task -> System.out.printf("%s: Order=%d, Duration=%d ms%n", + task.type(), task.executionOrder(), + Duration.between(task.startTime(), task.endTime()).toMillis())); + } +} diff --git a/quick-landing/pom.xml b/quick-landing/pom.xml index 7dfb4ca..ea18cf2 100644 --- a/quick-landing/pom.xml +++ b/quick-landing/pom.xml @@ -15,6 +15,7 @@ https://gitee.com/MagicJson/learning-training.git dynamic-rest-caller + dynamic-priority-executors -- Gitee