优化 POST 参数解析,使动态解析参数

当 POST 方法试图再次读取请求体,但是请求体已经被 Spring 的 @RequestBody 注解消费了。

我们需要在 @RequestBody 的同时,优化参数解析方法,使其能够动态地从请求中读取其他的额外参数,而不需要预先定义固定的字段。

以下是一个改进的方案:

  1. 首先,我们创建一个自定义的注解来标记我们想要处理的请求:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProcessRequestParams {
}
  1. 然后,我们创建一个 RequestBodyWrapper 类来包装原始的请求体:
public class RequestBodyWrapper {
    private final Map<String, Object> body;

    public RequestBodyWrapper(Map<String, Object> body) {
        this.body = body;
    }

    public Map<String, Object> getBody() {
        return body;
    }
}
  1. 为了确保 ContentCachingRequestWrapper 被正确应用,你需要在你的应用中添加一个 Filter:
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ContentCachingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        filterChain.doFilter(wrappedRequest, response);
    }
}
  1. 接下来,我们创建一个自定义的 HandlerMethodArgumentResolver
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netease.mdas.system.bean.RequestBodyWrapper;
import org.springframework.core.MethodParameter;
import org.springframework.util.StreamUtils;
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;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class RequestBodyWrapperArgumentResolver implements HandlerMethodArgumentResolver {

    private final ObjectMapper objectMapper;

    public RequestBodyWrapperArgumentResolver(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(RequestBodyWrapper.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest nativeRequest = (HttpServletRequest) webRequest.getNativeRequest();
        String body = getRequestBody(nativeRequest);

        if (body != null && !body.isEmpty()) {
            Map<String, Object> map = objectMapper.readValue(body, Map.class);
            return new RequestBodyWrapper(map);
        }

        return new RequestBodyWrapper(null);
    }

    private String getRequestBody(HttpServletRequest request) throws IOException {
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                return new String(buf, StandardCharsets.UTF_8);
            }
        }

        // 如果不是 ContentCachingRequestWrapper,则直接读取输入流
        return StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
    }
}
  1. 在 Spring 配置中注册这个 ArgumentResolver
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new RequestBodyWrapperArgumentResolver(objectMapper));
    }
}
  1. 修改 SearchController
@ApiOperation(value = "邮件列表", notes = "邮件列表,根据条件名模糊查询")
@PostMapping("/findList")
@ProcessRequestParams
public ResponseBean findList(@RequestHeader(value = "systemproduct") String orgId, 
                             @RequestBody FeedbackSearchVO dto,
                             RequestBodyWrapper wrapper) {
    // ... 其他代码保持不变 ...

    dto.setOtherInfoQuery(initOtherInfoQuery(wrapper.getBody()));

    // ... 其他代码保持不变 ...
}
  1. 最后,修改 initOtherInfoQuery 方法:
public static Map<String, List<JSONObject>> initOtherInfoQuery(Map<String, Object> requestBody) {
    Map<String, List<JSONObject>> otherInfoQuery = new HashMap<>();
    for (Map.Entry<String, Object> entry : requestBody.entrySet()) {
        String key = entry.getKey();
        Object value = entry.getValue();
        
        if (value instanceof List) {
            List<JSONObject> jsonList = new ArrayList<>();
            for (Object item : (List<?>) value) {
                jsonList.add(new JSONObject(item));
            }
            otherInfoQuery.put(key, jsonList);
        } else {
            List<JSONObject> jsonList = new ArrayList<>();
            jsonList.add(new JSONObject(value));
            otherInfoQuery.put(key, jsonList);
        }
    }
    return otherInfoQuery;
}

这种方法的优点是:

  1. 它可以动态处理所有请求参数,不需要预先定义固定的字段。
  2. 它不会干扰 @RequestBody 的正常工作。
  3. 它允许你同时访问解析后的 DTO 和原始请求体。

这个解决方案应该能够处理动态的参数,而不需要在代码中硬编码这些参数名。