Best Practices for Designing a Robust Controller Layer in Spring Backend Applications
This article explains how to build a clean, maintainable Controller layer in Spring by separating responsibilities, unifying response structures, applying global response wrapping, leveraging JSR‑303 validation for parameters, creating custom validation rules, and handling exceptions uniformly, all illustrated with practical Java code examples.
In this article, a senior architect shares guidelines for writing an excellent Controller layer in a Spring backend, emphasizing that Controllers act as indispensable yet lightweight entry points that should only handle request reception, parameter parsing, service delegation, exception capture, and response generation.
Problems with Conventional Controller Implementations
Typical issues include excessive parameter validation logic that violates the Single Responsibility Principle, duplicated exception handling across services, and inconsistent response formats that make client integration difficult.
Refactoring the Controller Layer
Unified Response Structure
A common response wrapper containing a status code, message, and data simplifies client-side handling and makes success/failure determination explicit.
// Define unified response interface
public interface IResult {
Integer getCode();
String getMessage();
}
// Enum for common results
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
// ... getters and constructor omitted
}
// Generic result class
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result
{
private Integer code;
private String message;
private T data;
// static factory methods omitted for brevity
}After defining the wrapper, Controllers can return Result.success(data) or Result.failed(message) without repeating boilerplate.
Global Response Wrapping with ResponseBodyAdvice
Spring's ResponseBodyAdvice intercepts the response before it is written by HttpMessageConverter , allowing automatic wrapping of any returned object.
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice
{
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
return true; // apply to all responses
}
@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);
}
}This eliminates the need to manually wrap responses in each Controller method.
Parameter Validation
Spring Validation (JSR‑303) provides declarative validation for request parameters and request bodies, keeping validation logic out of business code.
Validation of @PathVariable and @RequestParam
@RestController
@RequestMapping("/pretty")
public class TestController {
@GetMapping("/{num}")
public Integer detail(@PathVariable @Min(1) @Max(20) Integer num) {
return num * num;
}
@GetMapping("/getByEmail")
public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
TestDTO dto = new TestDTO();
dto.setEmail(email);
return dto;
}
}Validation failures throw MethodArgumentNotValidException or ConstraintViolationException , which can be handled globally.
Validation of @RequestBody
// DTO with validation annotations
@Data
public class TestDTO {
@NotBlank
private String userName;
@NotBlank @Length(min = 6, max = 20)
private String password;
@NotNull @Email
private String email;
}
// Controller method
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
testService.save(testDTO);
}Custom Validation Rules
When built‑in constraints are insufficient, developers can create custom annotations and validators.
// Custom annotation
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "不是一个手机号码格式";
boolean required() default true;
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 annotation) { this.required = annotation.required(); }
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext ctx) {
if (required && StringUtils.hasText(value)) {
return pattern.matcher(value).matches();
}
return true;
}
}Custom Exceptions and Global Exception Handling
Define specific runtime exceptions for business errors and permission issues, then handle them uniformly with @RestControllerAdvice .
// Custom exceptions
public class ForbiddenException extends RuntimeException { public ForbiddenException(String msg){ super(msg); } }
public class BusinessException extends RuntimeException { public BusinessException(String msg){ super(msg); } }
// Global exception advice
@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) {
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
sb.append(fe.getField()).append(":").append(fe.getDefaultMessage()).append(", ");
}
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), sb.toString());
}
@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()); }
}Conclusion
After applying these refactorings, Controller code becomes concise, validation rules are clearly expressed in DTOs, responses are uniformly structured, and exception handling is centralized, allowing developers to focus on core business logic.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.