Zero‑Intrusion Spring Boot Unified Response: 4 Low‑Level Solutions
The article explains why inconsistent API responses hurt front‑end/back‑end projects, introduces a unified result object, and demonstrates four zero‑intrusion, low‑level techniques—custom ResponseBodyAdvice, return‑value handler, HandlerMethod adapter, and message converter—each with code samples and verification screenshots.
In front‑end/back‑end separated projects, inconsistent API response formats and non‑uniform status codes cause parsing overhead and error‑handling duplication.
To standardize responses, a unified result object containing fields such as code, message, data, and timestamp is introduced.
Traditional approach manually wraps each controller method, which couples business logic and reduces reuse.
The article presents four low‑level, zero‑intrusion techniques for Spring Boot 3.5.0.
2.1 Custom ResponseBodyAdvice
Implementing @ControllerAdvice that intercepts the response before writing to the client. The beforeBodyWrite method checks for String responses, sets JSON content type, and wraps non‑ Result bodies with Result.success(...). The annotation @NoWrapper can exclude specific methods.
@ControllerAdvice
public class ResultResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper;
public ResultResponseBodyAdvice(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.hasMethodAnnotation(NoWrapper.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
try {
response.getHeaders().add("Content-Type", "application/json;charset=utf-8");
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
return body;
}
}
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}Verification screenshots show the JSON output after applying the advice.
2.2 Custom Return‑Value Handler
Implement HandlerMethodReturnValueHandler to wrap any return value. The handler is registered as the first element in the MVC return‑value handler list.
@Component
public class ResultMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
private final ObjectMapper objectMapper;
public ResultMethodReturnValueHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return !Result.class.isAssignableFrom(returnType.getParameterType())
&& !returnType.hasMethodAnnotation(NoWrapper.class);
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.getWriter().print(objectMapper.writeValueAsString(Result.success(returnValue)));
mavContainer.setRequestHandled(true);
}
} @Component
public class RegisterReturnValueHandler implements SmartInitializingSingleton {
private final RequestMappingHandlerAdapter adapter;
private final ResultMethodReturnValueHandler handler;
public RegisterReturnValueHandler(RequestMappingHandlerAdapter adapter,
ResultMethodReturnValueHandler handler) {
this.adapter = adapter;
this.handler = handler;
}
@Override
public void afterSingletonsInstantiated() {
List<HandlerMethodReturnValueHandler> handlers = this.adapter.getReturnValueHandlers();
List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>(handlers);
newHandlers.add(0, handler);
this.adapter.setReturnValueHandlers(newHandlers);
}
}Verification screenshots confirm the unified JSON format.
2.3 Custom HandlerMethod and Adapter
By extending ServletInvocableHandlerMethod, the return value is wrapped after the original method invocation. A custom RequestMappingHandlerAdapter creates this handler unless the method is annotated with @NoWrapper. Finally, the adapter is registered via WebMvcRegistrations.
public class ResultHandlerMethod extends ServletInvocableHandlerMethod {
public ResultHandlerMethod(HandlerMethod handlerMethod) {
super(handlerMethod);
}
@Override
public Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object ret = super.invokeForRequest(request, mavContainer, providedArgs);
return Result.success(ret);
}
} public class ResultHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
if (handlerMethod.hasMethodAnnotation(NoWrapper.class)) {
return super.createInvocableHandlerMethod(handlerMethod);
}
return new ResultHandlerMethod(handlerMethod);
}
} @Component
public class RegisterHandlerAdapter implements WebMvcRegistrations {
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new ResultHandlerAdapter();
}
}Verification screenshots demonstrate the effect.
2.4 Custom Message Converter
Extending AbstractHttpMessageConverter<Object> allows wrapping the body before the default JSON conversion. The converter skips Result and Void types, writes the wrapped JSON with proper content type, and propagates serialization errors.
@Component
public class ResultMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper;
public ResultMessageConverter(ObjectMapper objectMapper) {
super(MediaType.APPLICATION_JSON, MediaType.TEXT_HTML);
this.objectMapper = objectMapper;
}
@Override
protected boolean supports(Class<?> clazz) {
return !Result.class.isAssignableFrom(clazz) && !Void.TYPE.equals(clazz);
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
protected void writeInternal(Object body, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
outputMessage.getHeaders().add("Content-Type", "application/json;charset=utf-8");
Result<Object> response = Result.success(body);
try {
String json = objectMapper.writeValueAsString(response);
outputMessage.getBody().write(json.getBytes(StandardCharsets.UTF_8));
} catch (JsonProcessingException e) {
throw new HttpMessageNotWritableException("Error writing response", e);
}
}
}Verification screenshots show the unified JSON response produced by the converter.
All four approaches achieve zero‑intrusion unified response handling, allowing developers to choose the level of integration that best fits their project architecture.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
