Backend Development 20 min read

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

This article explains how to refactor a Spring MVC Controller layer by separating responsibilities, implementing a unified response structure, handling String response conversion, customizing validation with JSR‑303, and creating centralized exception handling to produce consistent API results.

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

An Excellent Controller Layer Logic

The Controller layer provides data interfaces and acts as an indispensable supporting role in both traditional three‑tier architecture and modern COLA architecture. It receives requests, delegates to Service for business execution, captures exceptions, and returns responses.

Identify Problems in the Current Implementation

Parameter validation is tightly coupled with business logic, violating the Single Responsibility Principle.

Repeated exception throwing across multiple services leads to duplicated code.

Inconsistent error and success response formats make client integration unfriendly.

Refactor the Controller Layer

Unified Return Structure

A unified return type makes it clear whether an API call succeeded, regardless of front‑end or back‑end separation. It uses a status code and message instead of relying on null checks.

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

After refactoring, the Controller code becomes concise, focusing only on request handling while business logic resides in Service.

Unified Response Wrapper

Define a generic result interface and an enum for common status codes.

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

public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");
    private Integer code;
    private String message;
    // getters, 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);
    }
    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);
    }
    public static
Result
instance(Integer code, String message, T data) {
        Result
result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}

Using ResponseBodyAdvice we can automatically wrap any controller return value into Result without modifying each method.

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice
{
    @Override
    public boolean supports(MethodParameter returnType, Class
> converterType) {
        return true; // can add exclusions via custom annotation
    }
    @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 response type is String , the advice converts the Result object to JSON manually to avoid ClassCastException caused by StringHttpMessageConverter .

Handling String Conversion Issues

Two solutions are provided:

Detect String return type in beforeBodyWrite and manually serialize the Result to JSON.

Adjust the order of HttpMessageConverter instances so that MappingJackson2HttpMessageConverter precedes StringHttpMessageConverter , ensuring proper JSON conversion.

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List
> converters) {
        converters.add(0, new MappingJackson2HttpMessageConverter());
    }
}

Parameter Validation

Spring Validation (built on JSR‑303) decouples validation logic from business code. Use annotations such as @Min , @Max , @NotBlank , @Email on DTO fields or method parameters, and annotate the controller with @Validated to trigger automatic checks.

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

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}

Spring’s RequestResponseBodyMethodProcessor parses @RequestBody parameters, performs validation via WebDataBinder , and throws MethodArgumentNotValidException or ConstraintViolationException when validation fails.

Custom Validation Rules

When built‑in constraints are insufficient, define a custom annotation and its validator.

@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 constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (required) {
            return value != null && pattern.matcher(value).matches();
        }
        return value == null || pattern.matcher(value).matches();
    }
}

Applying @Mobile on a field automatically validates phone numbers.

Custom Exceptions and Global Exception Handling

Define specific runtime exceptions for business errors and permission issues, then handle them centrally with @RestControllerAdvice to return the unified Result format.

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());
    }
}

With these changes, Controllers become clean, each method clearly declares its input validation rules and return type, while all responses—successful or error—share a consistent JSON structure.

Conclusion

After applying the refactorings, the Controller code is concise, validation rules are explicit, and exception handling is unified, allowing developers to focus on core business logic while delivering a robust, maintainable API.

JavaException HandlingValidationControllerSpring MVCResponse Wrapping
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.