diff --git a/src/main/java/com/imyeyu/spring/annotation/RequestBodyValue.java b/src/main/java/com/imyeyu/spring/annotation/RequestBodyValue.java new file mode 100644 index 0000000..5b6b89d --- /dev/null +++ b/src/main/java/com/imyeyu/spring/annotation/RequestBodyValue.java @@ -0,0 +1,43 @@ +package com.imyeyu.spring.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 读取 JSON 请求体中的单个字段并绑定到接口参数。 + * + *
默认使用方法参数名作为 JSON 字段名,也可以手动指定字段名。 + * + *
+ * public void run(@RequestBodyValue String data) {
+ * }
+ *
+ * public void run(@RequestBodyValue("value") Long id) {
+ * }
+ *
+ *
+ * @author 夜雨
+ * @version 2026-04-08 11:00
+ */
+@Documented
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequestBodyValue {
+
+ /**
+ * JSON 字段名,为空时使用方法参数名。
+ *
+ * @return JSON 字段名
+ */
+ String value() default "";
+
+ /**
+ * 是否必须存在该字段。
+ *
+ * @return true 表示必须存在
+ */
+ boolean required() default true;
+}
diff --git a/src/main/java/com/imyeyu/spring/annotation/RequestBodyValueArgumentResolver.java b/src/main/java/com/imyeyu/spring/annotation/RequestBodyValueArgumentResolver.java
new file mode 100644
index 0000000..49b6df9
--- /dev/null
+++ b/src/main/java/com/imyeyu/spring/annotation/RequestBodyValueArgumentResolver.java
@@ -0,0 +1,82 @@
+package com.imyeyu.spring.annotation;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiCode;
+import com.imyeyu.java.bean.timi.TimiException;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.core.MethodParameter;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+/**
+ * {@link RequestBodyValue} 参数解析器。
+ *
+ * @author 夜雨
+ * @version 2026-04-08 11:00
+ */
+@Component
+public class RequestBodyValueArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private static final String REQUEST_BODY_JSON_NODE_ATTR = RequestBodyValueArgumentResolver.class.getName() + ".REQUEST_BODY_JSON_NODE";
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * 创建 {@link RequestBodyValue} 参数解析器。
+ *
+ * @param objectMapper Jackson 对象映射器
+ */
+ public RequestBodyValueArgumentResolver(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ }
+
+ @Override
+ public boolean supportsParameter(@NonNull MethodParameter parameter) {
+ return parameter.hasParameterAnnotation(RequestBodyValue.class);
+ }
+
+ @Override
+ public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
+ RequestBodyValue bodyValue = parameter.getParameterAnnotation(RequestBodyValue.class);
+ if (bodyValue == null) {
+ return null;
+ }
+ HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
+ TimiException.required(request, "not found request");
+ assert request != null;
+
+ JsonNode requestBody;
+ {
+ Object cached = request.getAttribute(REQUEST_BODY_JSON_NODE_ATTR);
+ if (cached instanceof JsonNode jsonNode) {
+ return jsonNode;
+ }
+ byte[] bodyBytes = request.getInputStream().readAllBytes();
+ TimiException.requiredTrue(1 < bodyBytes.length, "empty request body");
+ requestBody = objectMapper.readTree(bodyBytes);
+ TimiException.requiredTrue(requestBody != null && requestBody.isObject(), "not object request body");
+ request.setAttribute(REQUEST_BODY_JSON_NODE_ATTR, requestBody);
+ }
+ String fieldName = bodyValue.value();
+ {
+ if (TimiJava.isEmpty(fieldName)) {
+ fieldName = parameter.getParameterName();
+ }
+ TimiException.required(fieldName, "not found @RequestBodyValue parameter name");
+ }
+ JsonNode fieldNode = requestBody.get(fieldName);
+ if (fieldNode == null || fieldNode.isMissingNode() || fieldNode.isNull()) {
+ if (!bodyValue.required()) {
+ return null;
+ }
+ throw new TimiException(TimiCode.ARG_MISS, "not found json field: %s".formatted(fieldName));
+ }
+ return objectMapper.treeToValue(fieldNode, parameter.getParameterType());
+ }
+}