From af55b36ced86a12e52280afe65e2b60ee9a6a194 Mon Sep 17 00:00:00 2001 From: laoma Date: Tue, 21 Jul 2026 14:17:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20JDK=2025=20+=20Spring=20Boot=204=20?= =?UTF-8?q?=E9=80=82=E9=85=8D=20-=20=E5=BA=9F=E5=BC=83API=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=8E=E6=96=B0=E7=89=B9=E6=80=A7=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mica-core/pom.xml | 6 +- .../java/net/dreamlu/mica/core/result/R.java | 7 - .../dreamlu/mica/core/result/SystemCode.java | 2 - .../net/dreamlu/mica/core/utils/JsonUtil.java | 14 +- .../mica/core/utils/ScopedValuesUtil.java | 172 ++++++++++++++++++ .../mica/http/HttpLoggingInterceptor.java | 28 ++- .../http/JsonPointerMethodInterceptor.java | 2 +- .../lite/jackson/JsonViewResultAdvice.java | 40 ++-- .../mica/redis/cache/MicaRedisCache.java | 10 +- pom.xml | 12 +- 10 files changed, 229 insertions(+), 64 deletions(-) create mode 100644 mica-core/src/main/java/net/dreamlu/mica/core/utils/ScopedValuesUtil.java diff --git a/mica-core/pom.xml b/mica-core/pom.xml index 3afa5f0a..569b74c2 100644 --- a/mica-core/pom.xml +++ b/mica-core/pom.xml @@ -33,11 +33,7 @@ jakarta.validation-api provided - - io.swagger - swagger-annotations - provided - + io.swagger.core.v3 swagger-annotations diff --git a/mica-core/src/main/java/net/dreamlu/mica/core/result/R.java b/mica-core/src/main/java/net/dreamlu/mica/core/result/R.java index 09aee42c..6d792de4 100644 --- a/mica-core/src/main/java/net/dreamlu/mica/core/result/R.java +++ b/mica-core/src/main/java/net/dreamlu/mica/core/result/R.java @@ -17,8 +17,6 @@ package net.dreamlu.mica.core.result; import com.fasterxml.jackson.annotation.JsonInclude; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.Getter; @@ -42,25 +40,20 @@ import java.util.function.Predicate; @Getter @Setter @ToString -@ApiModel(description = "返回信息") @Schema(description = "返回信息") @NoArgsConstructor public class R implements Serializable { @Serial private static final long serialVersionUID = -1160662278280275915L; - @ApiModelProperty(value = "code值", required = true) @Schema(description = "code值", requiredMode = Schema.RequiredMode.REQUIRED) private int code; - @ApiModelProperty(value = "是否成功", required = true) @Schema(description = "是否成功", requiredMode = Schema.RequiredMode.REQUIRED) private boolean success; - @ApiModelProperty(value = "消息", required = true) @Schema(description = "消息", requiredMode = Schema.RequiredMode.REQUIRED) private String msg; @Nullable @Valid - @ApiModelProperty("返回对象") @Schema(description = "返回对象") @JsonInclude(JsonInclude.Include.NON_NULL) private T data; diff --git a/mica-core/src/main/java/net/dreamlu/mica/core/result/SystemCode.java b/mica-core/src/main/java/net/dreamlu/mica/core/result/SystemCode.java index 2327376a..8701f453 100644 --- a/mica-core/src/main/java/net/dreamlu/mica/core/result/SystemCode.java +++ b/mica-core/src/main/java/net/dreamlu/mica/core/result/SystemCode.java @@ -16,7 +16,6 @@ package net.dreamlu.mica.core.result; -import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -28,7 +27,6 @@ import lombok.RequiredArgsConstructor; */ @Getter @RequiredArgsConstructor -@ApiModel(description = "系统内置code") @Schema(description = "系统内置code") public enum SystemCode implements IResultCode { /** diff --git a/mica-core/src/main/java/net/dreamlu/mica/core/utils/JsonUtil.java b/mica-core/src/main/java/net/dreamlu/mica/core/utils/JsonUtil.java index 29b792c5..03575193 100644 --- a/mica-core/src/main/java/net/dreamlu/mica/core/utils/JsonUtil.java +++ b/mica-core/src/main/java/net/dreamlu/mica/core/utils/JsonUtil.java @@ -20,7 +20,7 @@ import net.dreamlu.mica.core.function.CheckedFunction; import net.dreamlu.mica.core.jackson.MicaJavaTimeModule; import org.jspecify.annotations.Nullable; import tools.jackson.core.JsonParser; -import tools.jackson.core.TreeNode; +import tools.jackson.databind.JsonNode; import tools.jackson.core.json.JsonReadFeature; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.*; @@ -754,25 +754,25 @@ public class JsonUtil { /** * tree 转对象 * - * @param treeNode TreeNode + * @param jsonNode JsonNode * @param valueType valueType * @param 泛型标记 * @return 转换结果 */ - public static T treeToValue(TreeNode treeNode, Class valueType) { - return getInstance().treeToValue(treeNode, valueType); + public static T treeToValue(JsonNode jsonNode, Class valueType) { + return getInstance().treeToValue(jsonNode, valueType); } /** * tree 转对象 * - * @param treeNode TreeNode + * @param jsonNode JsonNode * @param valueType valueType * @param 泛型标记 * @return 转换结果 */ - public static T treeToValue(TreeNode treeNode, JavaType valueType) { - return getInstance().treeToValue(treeNode, valueType); + public static T treeToValue(JsonNode jsonNode, JavaType valueType) { + return getInstance().treeToValue(jsonNode, valueType); } /** diff --git a/mica-core/src/main/java/net/dreamlu/mica/core/utils/ScopedValuesUtil.java b/mica-core/src/main/java/net/dreamlu/mica/core/utils/ScopedValuesUtil.java new file mode 100644 index 00000000..45d51efa --- /dev/null +++ b/mica-core/src/main/java/net/dreamlu/mica/core/utils/ScopedValuesUtil.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dreamlu.mica.core.utils; + +import java.lang.ScopedValue; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Scoped Values 工具类(JDK 25+ JEP 506) + *

+ * 基于 ScopedValue 实现的上下文传递,替代 ThreadLocalUtil。 + * Scoped Values 核心优势: + *

    + *
  • 不可变性:绑定后不可修改,避免可变共享状态陷阱
  • + *
  • 继承安全:子线程自动继承父线程值,无需手动 capture-restore
  • + *
  • 虚拟线程友好:无 pinning 风险
  • + *
  • 内存效率:无需每个线程维护独立副本
  • + *
+ * + * @author L.cm + * @since 4.1.1 + */ +@SuppressWarnings("unchecked") +public final class ScopedValuesUtil { + + private ScopedValuesUtil() {} + + /** + * ScopedValue 实例,用于存储上下文 Map + */ + private static final ScopedValue> CONTEXT = ScopedValue.newInstance(); + + /** + * 获取当前上下文中的所有值(只读视图) + * + * @return 不可变的上下文 Map,如果无上下文则返回空 Map + */ + public static Map getAll() { + return CONTEXT.orElse(Collections.emptyMap()); + } + + /** + * 获取当前上下文中的所有值(可修改副本,用于传递给子线程) + *

+ * 注意:返回的是可修改的副本,用于在跨线程场景中捕获当前上下文。 + * 在 Scoped Values 模式下,子线程自动继承父线程的绑定值, + * 此方法主要用于兼容需要显式传递上下文的旧代码。 + * + * @return 上下文 Map 的可修改副本 + */ + public static Map getAllCopy() { + Map map = CONTEXT.orElse(Collections.emptyMap()); + return new HashMap<>(map); + } + + /** + * 从上下文中获取值 + * + * @param key 键 + * @param 值泛型 + * @return 值,不存在则返回 null + */ + @org.jspecify.annotations.Nullable + public static T get(String key) { + Map map = CONTEXT.orElse(Collections.emptyMap()); + return (T) map.get(key); + } + + /** + * 从上下文中获取值,不存在时使用提供者创建默认值 + * + * @param key 键 + * @param supplierOnNull 值不存在时的提供者 + * @param 值泛型 + * @return 值 + */ + @org.jspecify.annotations.Nullable + public static T getIfAbsent(String key, Supplier supplierOnNull) { + Map map = getAll(); + Object value = map.get(key); + if (value != null) { + return (T) value; + } + return supplierOnNull.get(); + } + + /** + * 在指定上下文中执行有返回值操作(核心 API) + *

+ * 替代 ThreadLocalUtil 的 put() → 操作 → clear() 模式, + * Scoped Values 保证操作结束后上下文自动清理,无需手动 clear。 + * + * @param context 上下文 Map + * @param action 要执行的操作(ScopedValue.CallableOp) + * @param 返回值类型 + * @param 异常类型 + * @return 操作的返回值 + * @throws X 操作执行过程中可能抛出的异常 + */ + public static T runWith(Map context, ScopedValue.CallableOp action) throws X { + return ScopedValue.where(CONTEXT, context).call(action); + } + + /** + * 在指定上下文中执行无返回值操作 + * + * @param context 上下文 Map + * @param action 要执行的 Runnable + */ + public static void runWith(Map context, Runnable action) { + ScopedValue.where(CONTEXT, context).run(action); + } + + /** + * 在当前上下文中追加值并执行有返回值操作 + *

+ * 基于 Scoped Values 的不可变性,此方法会创建新的绑定, + * 包含原有上下文 + 新增键值对,操作结束后自动恢复。 + * + * @param key 键 + * @param value 值 + * @param action 要执行的操作(ScopedValue.CallableOp) + * @param 返回值类型 + * @param 异常类型 + * @return 操作的返回值 + * @throws X 操作执行过程中可能抛出的异常 + */ + public static T runWith(String key, Object value, ScopedValue.CallableOp action) throws X { + Map newContext = new HashMap<>(getAll()); + newContext.put(key, value); + return ScopedValue.where(CONTEXT, Collections.unmodifiableMap(newContext)).call(action); + } + + /** + * 在当前上下文中追加值并执行无返回值操作 + * + * @param key 键 + * @param value 值 + * @param action 要执行的 Runnable + */ + public static void runWith(String key, Object value, Runnable action) { + Map newContext = new HashMap<>(getAll()); + newContext.put(key, value); + ScopedValue.where(CONTEXT, Collections.unmodifiableMap(newContext)).run(action); + } + + /** + * 判断当前是否存在绑定的上下文 + * + * @return 如果存在绑定值则返回 true + */ + public static boolean isBound() { + return CONTEXT.isBound(); + } +} \ No newline at end of file diff --git a/mica-http/src/main/java/net/dreamlu/mica/http/HttpLoggingInterceptor.java b/mica-http/src/main/java/net/dreamlu/mica/http/HttpLoggingInterceptor.java index c79fe8e6..72345858 100644 --- a/mica-http/src/main/java/net/dreamlu/mica/http/HttpLoggingInterceptor.java +++ b/mica-http/src/main/java/net/dreamlu/mica/http/HttpLoggingInterceptor.java @@ -17,7 +17,6 @@ package net.dreamlu.mica.http; import okhttp3.*; -import okhttp3.internal.http.HttpHeaders; import okio.Buffer; import okio.BufferedSource; import okio.GzipSource; @@ -154,7 +153,7 @@ public final class HttpLoggingInterceptor implements Interceptor { logger.log(headers.name(i) + ": " + headers.value(i)); } - if (!logBody || !HttpHeaders.hasBody(response)) { + if (!logBody || !hasBody(response)) { logger.log("<-- END HTTP"); } else if (bodyHasUnknownEncoding(response.headers())) { logger.log("<-- END HTTP (encoded body omitted)"); @@ -233,4 +232,29 @@ public final class HttpLoggingInterceptor implements Interceptor { && !"identity".equalsIgnoreCase(contentEncoding) && !"gzip".equalsIgnoreCase(contentEncoding); } + + /** + * Returns true if the response body is expected to be non-empty. + * Replaces okhttp3.internal.http.HttpHeaders.hasBody() which is an internal API. + */ + private static boolean hasBody(Response response) { + int code = response.code(); + // RFC 7230: 1xx, 204, and 304 responses don't have bodies + if (((code >= 100) && (code < 200)) || (code == 204) || (code == 304)) { + return false; + } + long contentLength = response.body().contentLength(); + if (contentLength > 0L) { + return true; + } + if (contentLength == 0L) { + return false; + } + // "Transfer-Encoding: chunked" means a body + if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) { + return true; + } + // No Content-Length and no Transfer-Encoding: treat as having a body + return true; + } } diff --git a/mica-http/src/main/java/net/dreamlu/mica/http/JsonPointerMethodInterceptor.java b/mica-http/src/main/java/net/dreamlu/mica/http/JsonPointerMethodInterceptor.java index 772148b9..9769ebb3 100644 --- a/mica-http/src/main/java/net/dreamlu/mica/http/JsonPointerMethodInterceptor.java +++ b/mica-http/src/main/java/net/dreamlu/mica/http/JsonPointerMethodInterceptor.java @@ -133,7 +133,7 @@ public class JsonPointerMethodInterceptor implements MethodInterceptor { if (jsonNode == null) { return null; } - return jsonNode.asText(); + return jsonNode.stringValue(); } private Collection newColl(Class returnType) { diff --git a/mica-lite/src/main/java/net/dreamlu/mica/lite/jackson/JsonViewResultAdvice.java b/mica-lite/src/main/java/net/dreamlu/mica/lite/jackson/JsonViewResultAdvice.java index 5d8b4e0e..ca5b161a 100644 --- a/mica-lite/src/main/java/net/dreamlu/mica/lite/jackson/JsonViewResultAdvice.java +++ b/mica-lite/src/main/java/net/dreamlu/mica/lite/jackson/JsonViewResultAdvice.java @@ -17,11 +17,9 @@ package net.dreamlu.mica.lite.jackson; import com.fasterxml.jackson.annotation.JsonView; -import net.dreamlu.mica.core.result.R; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -29,6 +27,12 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * 使 jackson view 在使用 R 了之后生效 + *

+ * Spring Framework 7.0+ 已通过 SmartHttpMessageConverter hints 机制原生支持 @JsonView, + * JacksonJsonHttpMessageConverter 会自动将 @JsonView 注解值作为 hint 传递 + * (key: "com.fasterxml.jackson.annotation.JsonView")。 + * MappingJacksonValue 已标记 @Deprecated(since="7.0", forRemoval=true), + * 本 Advice 不再需要 MappingJacksonValue 包装,@JsonView 由框架自动处理。 * * @author L.cm */ @@ -41,29 +45,13 @@ public class JsonViewResultAdvice implements ResponseBodyAdvice { } @Override - public Object beforeBodyWrite(Object body, - MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServerHttpRequest request, - ServerHttpResponse response) { - // 如果返回的是 mica 中的 R - if (body instanceof R) { - JsonView jsonView = returnType.getMethodAnnotation(JsonView.class); - // 理论上是不会出现 null 的,为了编辑器的代码检查添加了判断 - if (jsonView == null) { - return body; - } - // 没有标记 jsonView class - Class[] jsonViewClassArray = jsonView.value(); - if (jsonViewClassArray == null || jsonViewClassArray.length == 0) { - return body; - } - // 处理 json view - MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(body); - mappingJacksonValue.setSerializationView(jsonView.value()[0]); - return mappingJacksonValue; - } + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + // Spring Framework 7.0+ 通过 SmartHttpMessageConverter hints 机制原生处理 @JsonView, + // JacksonJsonHttpMessageConverter 自动从方法注解获取 JsonView 并作为 hint 传递, + // 不再需要 MappingJacksonValue 包装(已废弃,forRemoval=true)。 return body; } -} +} \ No newline at end of file diff --git a/mica-redis/src/main/java/net/dreamlu/mica/redis/cache/MicaRedisCache.java b/mica-redis/src/main/java/net/dreamlu/mica/redis/cache/MicaRedisCache.java index 99f2aada..c5ecd73c 100644 --- a/mica-redis/src/main/java/net/dreamlu/mica/redis/cache/MicaRedisCache.java +++ b/mica-redis/src/main/java/net/dreamlu/mica/redis/cache/MicaRedisCache.java @@ -167,7 +167,7 @@ public class MicaRedisCache { * 如果 key 已经存在, SETEX 命令将覆写旧值。 */ public void setEx(String key, T value, Long seconds) { - valueOps.set(key, value, seconds, TimeUnit.SECONDS); + valueOps.set(key, value, Duration.ofSeconds(seconds)); } /** @@ -180,7 +180,7 @@ public class MicaRedisCache { * @see Redis Documentation: SETEX */ public void setEx(String key, T value, long timeout, TimeUnit unit) { - valueOps.set(key, value, timeout, unit); + valueOps.set(key, value, Duration.ofMillis(unit.toMillis(timeout))); } /** @@ -208,7 +208,7 @@ public class MicaRedisCache { */ @Nullable public Boolean setNx(String key, T value, long time, TimeUnit unit) { - return valueOps.setIfAbsent(key, value, time, unit); + return valueOps.setIfAbsent(key, value, Duration.ofMillis(unit.toMillis(time))); } /** @@ -829,7 +829,7 @@ public class MicaRedisCache { */ @Nullable public Boolean expire(String key, long seconds) { - return redisTemplate.expire(key, seconds, TimeUnit.SECONDS); + return redisTemplate.expire(key, Duration.ofSeconds(seconds)); } /** @@ -862,7 +862,7 @@ public class MicaRedisCache { */ @Nullable public Boolean pExpire(String key, long milliseconds) { - return redisTemplate.expire(key, milliseconds, TimeUnit.MILLISECONDS); + return redisTemplate.expire(key, Duration.ofMillis(milliseconds)); } /** diff --git a/pom.xml b/pom.xml index 1a106b31..4bbb4f36 100644 --- a/pom.xml +++ b/pom.xml @@ -33,20 +33,19 @@ - 4.1.1-SNAPSHOT + 4.1.1-boot4-SNAPSHOT - 17 + 25 UTF-8 1.7.3 3.15.0 3.5.0 false - 4.0.1 + 4.0.2-boot4-SNAPSHOT 4.1.0 2025.1.2 - 1.6.16 2.2.52 3.0.3 1.22.2 @@ -132,11 +131,6 @@ pom import - - io.swagger - swagger-annotations - ${swagger.version} - io.swagger.core.v3 swagger-annotations -- Gitee