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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Zero‑Intrusion Spring Boot Unified Response: 4 Low‑Level Solutions

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootResponseBodyAdviceUnified ResponseHandlerMethodReturnValueHandlerCustom Message Converter
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.