Backend Development 17 min read

Improving Spring Controller Layer: Unified Response Structure, Validation, and Exception Handling

This article explains how to refactor a Spring MVC Controller layer by defining its responsibilities, introducing a unified response format, using ResponseBodyAdvice for automatic wrapping, applying JSR‑303 validation for request parameters, and implementing custom exceptions with centralized handling to achieve cleaner, more maintainable backend code.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Improving Spring Controller Layer: Unified Response Structure, Validation, and Exception Handling

Controllers in Spring MVC act as the indispensable layer that receives requests, delegates to services, handles exceptions, and returns responses.

Typical responsibilities of a Controller include receiving and parsing parameters, invoking service methods, catching business exceptions, and returning success responses.

//DTO
@Data
public class TestDTO {
    private Integer num;
    private String type;
}

//Service
@Service
public class TestService {
    public Double service(TestDTO testDTO) throws Exception {
        if (testDTO.getNum() <= 0) {
            throw new Exception("输入的数字需要大于0");
        }
        if (testDTO.getType().equals("square")) {
            return Math.pow(testDTO.getNum(), 2);
        }
        if (testDTO.getType().equals("factorial")) {
            double result = 1;
            int num = testDTO.getNum();
            while (num > 1) {
                result = result * num;
                num -= 1;
            }
            return result;
        }
        throw new Exception("未识别的算法");
    }
}

//Controller
@RestController
public class TestController {
    private TestService testService;

    @PostMapping("/test")
    public Double test(@RequestBody TestDTO testDTO) {
        try {
            Double result = this.testService.service(testDTO);
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}

Problems with the naive implementation include validation logic mixed with business code (violating single‑responsibility), duplicated exception handling, and inconsistent response formats.

Refactoring the Controller Layer

Unified Return Structure

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

//Define return data structure
public interface IResult {
    Integer getCode();
    String getMessage();
}

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

//Unified return data structure
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result
{
    private Integer code;
    private String message;
    private T data;
    public static
Result
success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }
    public static Result
failed() {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
    }
    public static Result
failed(String message) {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
    }
    public static Result
failed(IResult errorResult) {
        return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
    }
    public static
Result
instance(Integer code, String message, T data) {
        Result
result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}

Using this structure, each controller method can simply return Result.success(data) or Result.failed(...) .

Automatic Wrapping with ResponseBodyAdvice

Spring's ResponseBodyAdvice can intercept the response before it is written and wrap it into the unified Result object, eliminating repetitive wrapping code.

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice
{
    @Override
    public boolean supports(MethodParameter returnType, Class
> converterType) {
        // Add custom logic to exclude certain controllers if needed
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class
> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}

Parameter Validation

Leverage JSR‑303 (Hibernate Validator) and Spring's @Validated to automatically validate @RequestBody , @PathVariable and @RequestParam parameters. Validation failures throw MethodArgumentNotValidException or ConstraintViolationException .

@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
@Validated
public class TestController {
    private TestService testService;

    @GetMapping("/{num}")
    public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
        return num * num;
    }

    @GetMapping("/getByEmail")
    public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
        TestDTO testDTO = new TestDTO();
        testDTO.setEmail(email);
        return testDTO;
    }

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}

The validation process is performed by RequestResponseBodyMethodProcessor and MethodValidationPostProcessor , which invoke the validator before method execution.

Custom Validation Rules

Create a custom annotation and validator (e.g., @Mobile ) to handle business‑specific checks.

//Custom annotation
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    boolean required() default true;
    String message() default "不是一个手机号码格式";
    Class
[] groups() default {};
    Class
[] payload() default {};
}

//Validator implementation
public class MobileValidator implements ConstraintValidator
{
    private boolean required;
    private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
    @Override
    public void initialize(Mobile constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (required) {
            return value != null && pattern.matcher(value).matches();
        }
        if (StringUtils.hasText(value)) {
            return pattern.matcher(value).matches();
        }
        return true;
    }
}

Custom Exceptions and Global Exception Handling

Define specific runtime exceptions such as BusinessException and ForbiddenException , then use @RestControllerAdvice to map them to the unified Result format.

public class ForbiddenException extends RuntimeException {
    public ForbiddenException(String message) { super(message); }
}
public class BusinessException extends RuntimeException {
    public BusinessException(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 bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (StringUtils.hasText(msg)) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    @ExceptionHandler(ConstraintViolationException.class)
    public Result
handleConstraintViolationException(ConstraintViolationException ex) {
        if (StringUtils.hasText(ex.getMessage())) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    @ExceptionHandler(Exception.class)
    public Result
handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

All exceptions, including validation errors, are converted to a consistent JSON response with HTTP status 200, allowing the client to distinguish success from failure via the result code.

Conclusion

After applying these refactorings, the Controller code becomes concise, each endpoint clearly declares its input validation, and responses are uniformly structured, improving maintainability and adherence to the single‑responsibility principle.

BackendSpringvalidationControllerExceptionHandlingResponseBodyAdvice
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login 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.