Refactor Spring Boot Controllers for Cleaner Code and Unified Responses

This article explains the role of Controllers in Spring Boot, identifies common pitfalls such as duplicated validation and inconsistent responses, and demonstrates how to refactor them using a unified response structure, ResponseBodyAdvice, proper message converter ordering, JSR‑303 validation, custom validators, and centralized exception handling.

macrozheng
macrozheng
macrozheng
Refactor Spring Boot Controllers for Cleaner Code and Unified Responses

Understanding the Controller Layer

Controllers act as an indispensable yet supporting role in both traditional three‑layer and modern COLA architectures, handling request reception, delegating to services, catching business exceptions, and returning responses.

Current Problems with Controllers

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

Repeatedly throwing the same exceptions across multiple services leads to code duplication.

Inconsistent error and success response formats make API integration unfriendly.

Refactoring the Controller Logic

The mall project (SpringBoot3 + JDK 17 + Vue) provides a real‑world example of these improvements.

Unified Return Structure

A common response type with a status code and message helps clients quickly determine success or failure, regardless of whether the front‑end and back‑end are separated.

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

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

@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);
    }
    // other factory methods omitted
}

After defining this structure, each Controller can simply return Result.success(...) or Result.failed(...), eliminating repetitive wrapping code.

Unified Wrapper with ResponseBodyAdvice

Spring provides ResponseBodyAdvice to intercept the response before the HttpMessageConverter processes it.

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

Implementation example:

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true; // apply to all responses
    }
    @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 new ObjectMapper().writeValueAsString(Result.success(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return Result.success(body);
    }
}

Handling String Conversion Issues

When the response type is String, Spring uses StringHttpMessageConverter, which cannot convert a Result object, causing a ClassCastException. Two solutions are presented:

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

Reorder the message converters so that MappingJackson2HttpMessageConverter precedes StringHttpMessageConverter, allowing automatic conversion.

String vs Integer converter
String vs Integer converter

Parameter Validation with JSR‑303

Spring’s validation framework builds on the JSR‑303 validation-api and Hibernate Validator. By annotating DTO fields with constraints such as @NotBlank, @Length, @Email, and using @Validated on controllers, validation is performed automatically without mixing business logic.

@Data
public class TestDTO {
    @NotBlank private String userName;
    @NotBlank @Length(min = 6, max = 20) private String password;
    @NotNull @Email private String email;
}

@RestController
@RequestMapping("/pretty")
public class TestController {
    @PostMapping("/test-validation")
    public void testValidation(@RequestBody @Validated TestDTO testDTO) {
        testService.save(testDTO);
    }
}

Custom Validation Rules

When built‑in constraints are insufficient, developers can create custom annotations and validators.

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    String message() default "不是一个手机号码格式";
    boolean required() default true;
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
    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 ctx) {
        if (required && !StringUtils.hasText(value)) return false;
        return !StringUtils.hasText(value) || pattern.matcher(value).matches();
    }
}

Custom Exceptions and Global Exception Handling

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

public class BusinessException extends RuntimeException { public BusinessException(String msg) { super(msg); } }
public class ForbiddenException extends RuntimeException { public ForbiddenException(String msg) { super(msg); } }

@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) {
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fe : ex.getBindingResult().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());
    }
}

Conclusion

By applying a unified response wrapper, leveraging ResponseBodyAdvice, ordering message converters correctly, and using JSR‑303 validation with custom rules and centralized exception handling, Controller code becomes concise, maintainable, and consistent, allowing developers to focus on business logic.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Exception HandlingSpring BootParameter ValidationControllerresponsebodyadviceUnified response
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

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.