How to Refactor SpringBoot Controllers for Unified Responses and Robust Validation

This article shows how to refactor SpringBoot controller code by introducing a unified Result wrapper, ResponseBodyAdvice, proper HttpMessageConverter ordering, JSR‑303 validation (including custom validators), and centralized exception handling, resulting in concise, maintainable backend code with consistent API responses.

Architect's Guide
Architect's Guide
Architect's Guide
How to Refactor SpringBoot Controllers for Unified Responses and Robust Validation

An Excellent Controller Layer Logic

Controller is an indispensable part of three‑tier or COLA architecture, responsible for receiving requests, invoking services, handling exceptions, and returning responses.

Identify Problems

Parameter validation is tightly coupled with business logic, violating single‑responsibility principle.

Same exception may be thrown by many services, causing code duplication.

Inconsistent error and success response formats make API integration unfriendly.

Refactor Controller Layer Logic

Unified Return Structure

Define a generic result interface and enum for common status codes, then implement a generic Result<T> class with static factory methods.

public interface IResult {
    Integer getCode();
    String getMessage();
}

public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");
    // fields and constructor omitted
}

public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    public static <T> Result<T> success(T data) { /* ... */ }
    public static Result<?> failed() { /* ... */ }
    // other factory methods omitted
}

Use ResponseBodyAdvice to wrap every controller response automatically.

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                 MediaType selectedContentType,
                                 Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                 ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            return body;
        }
        if (body instanceof String) {
            try {
                return objectMapper.writeValueAsString(Result.success(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return Result.success(body);
    }
}

Handling String Conversion Issue

When the selected converter is StringHttpMessageConverter, the wrapper must be converted to JSON manually, or adjust the converter order so that MappingJackson2HttpMessageConverter is evaluated first.

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new MappingJackson2HttpMessageConverter());
    }
}

Parameter Validation

Spring MVC supports JSR‑303 validation. Use @Validated on controller classes and validation annotations such as @NotBlank, @Min, @Max, @Email on method parameters or DTO fields.

@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
    @GetMapping("/{num}")
    public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
        return num * num;
    }
    @PostMapping("/test-validation")
    public void testValidation(@RequestBody @Validated TestDTO dto) {
        testService.save(dto);
    }
}

Validation failures raise MethodArgumentNotValidException (for body) or ConstraintViolationException (for path/query parameters).

Custom Validation Rules

Create a custom annotation and a corresponding validator.

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    boolean required() default true;
    String message() default "不是一个手机号码格式";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
    private boolean required;
    private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
    @Override
    public void initialize(Mobile annotation) {
        this.required = annotation.required();
    }
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext ctx) {
        if (required) {
            return value != null && pattern.matcher(value).matches();
        }
        return value == null || pattern.matcher(value).matches();
    }
}

Custom Exceptions and Unified Exception Handling

Define business‑specific exceptions and handle them centrally with @RestControllerAdvice to return the same Result structure.

public class BusinessException extends RuntimeException {
    public BusinessException(String message) { super(message); }
}

public class ForbiddenException extends RuntimeException {
    public ForbiddenException(String message) { super(message); }
}

@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }
    @ExceptionHandler(ForbiddenException.class)
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        return Result.failed(ResultEnum.FORBIDDEN);
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult br = ex.getBindingResult();
        String msg = br.getFieldErrors().stream()
                .map(e -> e.getField() + ":" + e.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
    }
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
    }
    @ExceptionHandler(Exception.class)
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

Summary

By introducing a unified Result wrapper, a ResponseBodyAdvice implementation, proper converter ordering, JSR‑303 validation (including custom validators), and centralized exception handling, controller code becomes concise, responsibilities are clearly separated, and API responses are consistent, allowing developers to focus on business logic.

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.

validationControllerexceptionhandlingresponsebodyadvice
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.