Backend Development 20 min read

Refactor Spring Controllers for Unified Responses and Robust Validation

This article explains why traditional Spring MVC Controllers become cumbersome, demonstrates how to unify response structures with a Result wrapper, leverages ResponseBodyAdvice for automatic packaging, resolves String conversion issues, and shows best‑practice parameter validation using JSR‑303, custom validators, and global exception handling to keep controller code clean and maintainable.

macrozheng
macrozheng
macrozheng
Refactor Spring Controllers for Unified Responses and Robust Validation

In Spring MVC the

Controller

layer acts as an indispensable façade that receives requests, delegates to services, handles exceptions and returns responses, but its code often mixes business logic with request handling.

Current Problems

The typical responsibilities of a controller are:

Receive and parse request parameters

Call a

Service

to execute business logic (including validation)

Catch business exceptions and provide feedback

Return a successful response

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

Developing controllers exactly as above leads to three main issues:

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

Repeated exception handling causes code duplication.

Inconsistent success and error response formats make client integration unfriendly.

Refactor Controller Logic

Unified Return Structure

A consistent response type is essential for both monolithic and front‑end/back‑end separated projects. Using a status code and message makes the result clear.

<code>//Define return data structure
public interface IResult {
    Integer getCode();
    String getMessage();
}

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

    private Integer code;
    private String message;
    // getters, constructors omitted
}

//Unified response wrapper
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    public static <T> Result<T> 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 <T> Result<T> instance(Integer code, String message, T data) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}
</code>

After defining the wrapper, each controller can simply return

Result.success(...)

. However, writing this in every method would be repetitive, so we move the packaging logic to a global advice.

Unified Wrapper with ResponseBodyAdvice

Spring provides

ResponseBodyAdvice

, which intercepts the response before the

HttpMessageConverter

writes it. By implementing this interface we can automatically wrap any return value.

<code>public interface ResponseBodyAdvice<T> {
    boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
    @Nullable
    T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
                     Class<? extends HttpMessageConverter<?>> selectedConverterType,
                     ServerHttpRequest request, ServerHttpResponse response);
}
</code>
<code>@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // always wrap
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}
</code>

When the controller returns a

String

, the default

StringHttpMessageConverter

would try to cast the

Result

object and fail. The advice can detect a

String

return type and manually convert the

Result

to JSON.

<code>@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                            Class<? extends HttpMessageConverter<?>> 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);
}
</code>

Below are the screenshots that illustrate the problem with

String

vs other types:

String conversion issue
String conversion issue
Other type conversion
Other type conversion

Parameter Validation

JSR‑303 defines a standard validation API. Spring’s

spring‑validation

module wraps Hibernate Validator, allowing declarative validation of request parameters without mixing validation logic into business code.

@PathVariable and @RequestParam Validation

<code>@RestController(value = "prettyTestController")
@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 testDTO = new TestDTO();
        testDTO.setEmail(email);
        return testDTO;
    }

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}
</code>

If validation fails, Spring throws

MethodArgumentNotValidException

(for

@RequestBody

) or

ConstraintViolationException

(for

@PathVariable

/

@RequestParam

).

@RequestBody Validation

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

Custom Validation Rules

<code>//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<? extends Payload>[] payload() default {};
}

//Validator implementation
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
    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(final CharSequence str) {
        return pattern.matcher(str).matches();
    }
}
</code>

Using JSR‑303 together with custom validators completely decouples validation from business logic, keeping the code clean and adhering to the Single Responsibility Principle.

Custom Exceptions and Global Exception Handling

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

Conclusion

After applying the above refactorings, controller code becomes concise, each parameter’s validation rules are explicit, and response formats are uniform. This separation lets developers focus on business logic while the framework handles validation, packaging, and error handling consistently.

WeChat QR code
WeChat QR code
backendJavaSpringvalidationControllerResponseBodyAdvice
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

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.