Improving Controller Layer Logic: Unified Response Wrapping, Validation, and Exception Handling in Spring MVC
This article explains how to design a clean Controller layer in Spring MVC by implementing unified response structures, handling String response issues with ResponseBodyAdvice, applying parameter validation with JSR‑303, and creating custom exceptions with centralized exception handling to simplify business logic.
In Spring MVC the Controller is a crucial but often overlooked component that should only handle request reception, parameter parsing, delegating to services, and returning responses. Directly writing business logic in Controllers leads to coupling, duplicated error handling, and inconsistent response formats.
To address these problems the article introduces a unified response wrapper Result<T> and an enum ResultEnum that define a standard JSON structure with a code, message, and data field. Example DTO, Service, and Controller implementations are shown:
@Data
public class TestDTO {
private Integer num;
private String type;
}
@Service
public class TestService {
public Double service(TestDTO testDTO) throws Exception {
if (testDTO.getNum() <= 0) {
throw new Exception("输入的数字需要大于0");
}
if ("square".equals(testDTO.getType())) {
return Math.pow(testDTO.getNum(), 2);
}
// ... other logic ...
throw new Exception("未识别的算法");
}
}
@RestController
public class TestController {
@Autowired
private TestService testService;
@PostMapping("/test")
public Double test(@RequestBody TestDTO testDTO) {
return testService.service(testDTO);
}
}To avoid repetitive wrapping code, a ResponseBodyAdvice implementation is added. It intercepts the response before the HttpMessageConverter writes it and wraps non‑Result objects into Result.success(...) . Special handling for String return types converts the wrapped object to JSON manually.
@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) {
try {
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return Result.success(body);
}
}When the StringHttpMessageConverter is placed before MappingJackson2HttpMessageConverter in the converter list, the wrapper fails for String responses. Two solutions are presented: (1) detect String in beforeBodyWrite and convert manually, and (2) reorder the converters so that Jackson is first, ensuring proper JSON conversion.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List
> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}Parameter validation is covered using JSR‑303 (Hibernate Validator) with Spring’s @Validated . Examples show validation of @PathVariable , @RequestParam , and @RequestBody parameters, and the resulting MethodArgumentNotValidException or ConstraintViolationException are handled uniformly.
@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 testDTO) {
// business logic
}
}Custom validation annotations are demonstrated with a @Mobile annotation and its corresponding MobileValidator implementing ConstraintValidator . This shows how to extend validation beyond the built‑in constraints.
@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 && value != null) {
return pattern.matcher(value).matches();
}
return true;
}
}Finally, custom runtime exceptions ( ForbiddenException , BusinessException ) and a global @RestControllerAdvice are introduced to map exceptions to the unified Result format, ensuring that all error responses share the same structure and HTTP status 200.
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) {
// build message from field errors
return Result.failed(ResultEnum.VALIDATE_FAILED);
}
@ExceptionHandler(Exception.class)
public Result
handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}By applying these techniques the Controller code becomes concise, validation rules are declarative, responses are consistent, and error handling is centralized, allowing developers to focus on 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.