Backend Development 17 min read

Best Practices for Designing a Robust Spring MVC Controller Layer

This article explains the essential responsibilities of a Spring MVC controller, identifies common pitfalls such as duplicated validation and inconsistent responses, and demonstrates how to unify response structures, handle String conversion issues, apply JSR‑303 validation, create custom validators, and implement centralized exception handling using ResponseBodyAdvice and @RestControllerAdvice.

Top Architecture Tech Stack
Top Architecture Tech Stack
Top Architecture Tech Stack
Best Practices for Designing a Robust Spring MVC Controller Layer

In a typical three‑tier or COLA architecture, the Controller layer acts as an indispensable façade that receives requests, delegates business logic to services, handles parameter validation, captures exceptions, and returns responses.

The current implementation often suffers from excessive coupling of validation logic, repeated exception handling, and inconsistent response formats, which makes the code hard to maintain.

To address these issues, a unified response structure is introduced using a generic Result class and an IResult interface that encapsulate a status code, message, and data payload. Helper methods such as Result.success() and Result.failed() simplify response creation.

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

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

@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);
    }
    // ... other factory methods omitted
}

Because Spring’s default ResponseBodyAdvice processes the response before the HttpMessageConverter , a custom ResponseAdvice is implemented to wrap all controller return values with Result.success() . Special handling is added for String return types to avoid class‑cast errors caused by StringHttpMessageConverter .

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

Parameter validation is performed using JSR‑303 (Hibernate Validator) annotations such as @NotBlank , @Min , @Max , and custom constraints. Spring’s MethodValidationPostProcessor and MethodValidationInterceptor apply AOP‑based validation to method arguments and return values.

@RestController
@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 testDTO) {
        testService.save(testDTO);
    }
}

@Data
public class TestDTO {
    @NotBlank private String userName;
    @NotBlank @Length(min = 6, max = 20) private String password;
    @NotNull @Email private String email;
}

Custom validation annotations can be created by defining an annotation (e.g., @Mobile ) and a corresponding ConstraintValidator implementation that checks the value against a regular expression.

@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
[] payload() default {};
}

public class MobileValidator implements ConstraintValidator
{
    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 context) {
        if (!required && !StringUtils.hasText(value)) return true;
        return pattern.matcher(value).matches();
    }
}

Centralized exception handling is achieved with @RestControllerAdvice , defining handlers for business exceptions, forbidden access, validation failures, and a generic fallback. Each handler returns a Result object with appropriate error codes and messages, ensuring that HTTP status remains 200 while the payload conveys the error.

@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(Exception.class)
    public Result
handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

By applying these patterns, controller code becomes concise, validation logic is cleanly separated, responses are consistent, and error handling is uniform, allowing developers to focus on business logic while maintaining high code quality.

ValidationControllerSpring MVCExceptionHandlingResponseBodyAdvice
Top Architecture Tech Stack
Written by

Top Architecture Tech Stack

Sharing Java and Python tech insights, with occasional practical development tool tips.

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.