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