Spring Boot控制器如何仅记录JSON请求的键(含嵌套对象)且无需解析迭代
解决方案:无需手动迭代即可记录JSON请求的所有键(含嵌套)
嘿,这个需求我刚好在项目里处理过,给你两个简便的实现思路,完全不用自己手动写递归迭代逻辑来解析嵌套对象,利用Spring和Jackson的原生能力就能搞定:
方案一:使用@ControllerAdvice结合Jackson JsonParser(推荐)
这个方案贴合Spring的请求处理流程,不用侵入控制器代码,在请求体转换成Item对象之前就完成键的提取:
1. 编写RequestBodyAdvice类
@ControllerAdvice(basePackages = "com.yourpackage.controller") // 替换成你的控制器所在包 public class RequestKeyLoggingAdvice implements RequestBodyAdvice { private static final Logger logger = LoggerFactory.getLogger(RequestKeyLoggingAdvice.class); private final ObjectMapper objectMapper = new ObjectMapper(); @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { // 只拦截目标控制器的addItem方法 return "addItem".equals(methodParameter.getMethod().getName()) && methodParameter.getContainingClass().isAssignableFrom(YourController.class); // 替换成你的控制器类名 } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { // 读取请求体字节 byte[] bodyBytes = StreamUtils.copyToByteArray(inputMessage.getBody()); String requestBody = new String(bodyBytes, StandardCharsets.UTF_8); // 用Jackson的JsonParser自动遍历所有字段名(含嵌套) Set<String> allRequestKeys = new LinkedHashSet<>(); try (JsonParser parser = objectMapper.getFactory().createParser(requestBody)) { JsonToken token; while ((token = parser.nextToken()) != null) { if (token == JsonToken.FIELD_NAME) { allRequestKeys.add(parser.getCurrentName()); } } } // 记录所有键 logger.info("Received request keys for addItem: {}", String.join(", ", allRequestKeys)); // 包装输入流,保证后续流程能正常读取请求体 return new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(bodyBytes); } @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return body; } @Override public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return body; } }
方案二:使用拦截器+请求体包装
如果更喜欢用拦截器的方式,也可以通过包装请求体来读取并提取键:
1. 实现请求体包装类(解决请求体只能读一次的问题)
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private final byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() throws IOException { return new CachedBodyServletInputStream(cachedBody); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(cachedBody))); } public byte[] getCachedBody() { return cachedBody; } private static class CachedBodyServletInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; public CachedBodyServletInputStream(byte[] cachedBody) { this.inputStream = new ByteArrayInputStream(cachedBody); } @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException(); } } }
2. 编写拦截器
@Component public class RequestKeyLoggingInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(RequestKeyLoggingInterceptor.class); private final ObjectMapper objectMapper = new ObjectMapper(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 只拦截目标接口 if ("/v1/myapi".equals(request.getRequestURI()) && HttpMethod.POST.matches(request.getMethod())) { CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request); String requestBody = new String(cachedRequest.getCachedBody(), StandardCharsets.UTF_8); Set<String> allRequestKeys = new LinkedHashSet<>(); try (JsonParser parser = objectMapper.getFactory().createParser(requestBody)) { JsonToken token; while ((token = parser.nextToken()) != null) { if (token == JsonToken.FIELD_NAME) { allRequestKeys.add(parser.getCurrentName()); } } } logger.info("Received request keys: {}", String.join(", ", allRequestKeys)); // 替换请求为包装后的实例,保证后续流程正常读取 request = cachedRequest; } return true; } }
3. 注册拦截器
@Configuration public class WebConfig implements WebMvcConfigurer { private final RequestKeyLoggingInterceptor loggingInterceptor; public WebConfig(RequestKeyLoggingInterceptor loggingInterceptor) { this.loggingInterceptor = loggingInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loggingInterceptor); } }
关键说明
- 两种方案都利用Jackson的
JsonParser自动遍历JSON的所有字段,不管嵌套多少层,都会提取出所有键,完全不用自己写递归迭代逻辑。 - 使用
LinkedHashSet可以保持键的顺序和请求体中的顺序一致。 - 都处理了请求体只能读取一次的问题,通过包装输入流保证后续流程正常工作。
内容的提问来源于stack exchange,提问作者drunkenfist
相关产品推荐
相关产品推荐

