Backend Development 19 min read

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.

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

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.

<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>

Implementation example:

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

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.

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

Custom Validation Rules

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

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

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.

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

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.

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

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.