Backend Development 20 min read

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

This article explains how to refactor Spring MVC Controller code by introducing a unified response format, handling String and JSON return types with ResponseBodyAdvice, applying parameter validation using JSR‑303, and implementing custom exceptions with centralized exception handling to simplify business logic and improve code maintainability.

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

The article starts by describing the typical responsibilities of a Controller in a Spring MVC application: receiving requests, parsing parameters, delegating to Service, handling exceptions, and returning responses.

"Controller is an indispensable supporting role; it does not contain business logic but is responsible for request reception and response handling."

It then lists the four main tasks a Controller should perform and shows a sample implementation with DTO, Service, and Controller classes.

@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 (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("未识别的算法");
    }
}

@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;
    }
}

From this baseline, three problems are identified: excessive parameter validation coupling, duplicated exception handling, and inconsistent response formats.

Unified Response Structure

A generic result interface IResult and an enum ResultEnum are defined to standardize the response payload with a code, message, and optional data.

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

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

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result
{
    private Integer code;
    private String message;
    private T data;
    // static factory methods omitted
}

With this structure, Controllers can simply return Result.success(...) without repeating boilerplate.

ResponseBodyAdvice for Automatic Wrapping

The ResponseBodyAdvice interface is used to intercept the response before it is written. The implementation checks whether the body is already a Result ; if not, it wraps it. Special handling is added for String return types to avoid type‑conversion errors.

@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 this.objectMapper.writeValueAsString(Result.success(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return Result.success(body);
    }
}

Two solutions for the String conversion issue are presented: converting the Result to JSON manually inside beforeBodyWrite , or reordering the MappingJackson2HttpMessageConverter to have higher priority than StringHttpMessageConverter in the MVC configuration.

Parameter Validation with JSR‑303

The article introduces Spring’s validation support built on JSR‑303 (Hibernate Validator). It shows how to annotate DTO fields with constraints such as @NotBlank , @Email , @Min , @Max , and how to trigger validation with @Validated on controller methods.

@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
    @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 dto = new TestDTO();
        dto.setEmail(email);
        return dto;
    }
}

When validation fails, Spring throws MethodArgumentNotValidException or ConstraintViolationException . The article explains the internal processing flow via RequestResponseBodyMethodProcessor and MethodValidationPostProcessor .

Custom Validation Rules

For business‑specific checks, a custom annotation @Mobile and its validator MobileValidator are defined, demonstrating how to plug additional logic into the JSR‑303 pipeline.

@Target({ElementType.METHOD, 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) {
            return value != null && pattern.matcher(value).matches();
        }
        return value == null || !StringUtils.hasText(value) || pattern.matcher(value).matches();
    }
}

Custom Exceptions and Global Exception Handling

Two domain‑specific runtime exceptions ( ForbiddenException and BusinessException ) are created. A @RestControllerAdvice class centralizes handling of these exceptions as well as validation‑related ones, always returning a Result with 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) {
        BindingResult br = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fe : br.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());
    }
}

Finally, the article summarizes that after applying these refactorings, Controller code becomes concise, validation rules are explicit, responses are uniform, and error handling is centralized, allowing developers to focus on business logic.

SpringvalidationControllerExceptionHandlingResponseBodyAdvice
Top Architect
Written by

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.

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.