Best Practices for Designing an Excellent Controller Layer in Spring MVC
This article explains how to design a clean, maintainable Spring MVC controller layer by unifying response structures, applying centralized exception handling, and leveraging JSR‑303 validation with custom rules, illustrated with comprehensive code examples and best‑practice guidelines.
Excellent Controller Layer Logic
When we talk about the Controller layer, it is a familiar component that conveniently provides data interfaces. It is an indispensable supporting role because it exists in both traditional three‑tier architecture and modern COLA architecture.
The Controller itself does not implement business logic; it mainly receives requests, delegates to services, and returns responses.
Identifying Problems in the Current Implementation
The main responsibilities of a Controller are:
Receive requests and parse parameters
Invoke Service to execute business code (including parameter validation)
Capture business exceptions and provide feedback
Return successful 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;
}
}Developing Controllers strictly according to the above responsibilities leads to several issues:
Parameter validation is tightly coupled with business code, violating the Single Responsibility Principle.
Repeated throwing of the same exception across multiple services causes code duplication.
Inconsistent error and success response formats make API integration unfriendly.
Refactoring the Controller Layer
Unified Return Structure
A consistent response type is essential for both front‑end and back‑end teams. Using a status code and message makes it clear whether an API call succeeded.
// Define the return data structure
public interface IResult {
Integer getCode();
String getMessage();
}
// Common result enumeration
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
private Integer code;
private String message;
// getters and constructors omitted
}
// Unified response wrapper
@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);
}
}After defining this wrapper, each Controller can simply return Result.success(data) . However, writing the wrapping code in every Controller is still repetitive, so we move the logic to a global advice.
Centralized Response Wrapping with ResponseBodyAdvice
Spring provides ResponseBodyAdvice to intercept the body before it is written by HttpMessageConverter . By implementing this interface we can automatically wrap all responses.
public interface ResponseBodyAdvice
{
boolean supports(MethodParameter returnType, Class
> converterType);
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class
> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}Implementation example:
@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; // already wrapped
}
return Result.success(body);
}
}This approach unifies the response format without modifying existing Controllers.
Parameter Validation
JSR‑303 defines a standard validation API; Hibernate Validator is a popular implementation. Spring Validation builds on it to automatically validate method parameters in Spring MVC.
1️⃣ Validation of @PathVariable and @RequestParam
Use constraint annotations directly on method parameters. For more than five parameters, prefer using a DTO.
@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
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 prettyTestService) {
this.testService = prettyTestService;
}
}If validation fails, Spring throws MethodArgumentNotValidException .
2️⃣ Validation of @RequestBody
Annotate the DTO with validation constraints and add @Validated on the controller method.
//DTO
@Data
public class TestDTO {
@NotBlank
private String userName;
@NotBlank
@Length(min = 6, max = 20)
private String password;
@NotNull
@Email
private String email;
}
//Controller
@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {
private TestService testService;
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
this.testService.save(testDTO);
}
@Autowired
public void setTestService(TestService testService) {
this.testService = testService;
}
}Failed validation results in a ConstraintViolationException .
3️⃣ Custom Validation Rules
When built‑in constraints are insufficient, define a custom annotation and its validator.
// 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 = false;
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 (this.required) {
return isMobile(value);
}
if (StringUtils.hasText(value)) {
return isMobile(value);
}
return true;
}
private boolean isMobile(CharSequence str) {
Matcher m = pattern.matcher(str);
return m.matches();
}
}Custom validators allow complex business rules to be expressed cleanly.
Custom Exceptions and Unified Exception Handling
Original code threw generic Exception , making it hard to differentiate error types. Define specific runtime exceptions and handle them centrally.
// Custom exceptions
public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) { super(message); }
}
public class BusinessException extends RuntimeException {
public BusinessException(String message) { super(message); }
}
// 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) {
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());
}
}Conclusion
After applying these changes, Controller code becomes concise, each parameter’s validation rules are explicit, and response structures are uniform. This separation of concerns lets developers focus on business logic while keeping the code clean and functional.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.