How to Refactor Spring Controllers for Clean, Unified Responses

This article explains how to improve Spring MVC controller logic by separating responsibilities, introducing a unified response structure, handling String return types with ResponseBodyAdvice, applying JSR‑303 validation, creating custom validators, and using custom exceptions with global exception handling to produce concise and maintainable backend code.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Refactor Spring Controllers for Clean, Unified Responses

What Makes a Good Controller Layer

A controller should receive requests, parse parameters, delegate to a service for business logic, handle exceptions, and return a response without containing core business code.

Problems with Naïve Implementation

Parameter validation is tightly coupled with business logic, violating the single‑responsibility principle.

Repeatedly throwing the same exception across different services leads to duplicated code.

Inconsistent success and error response formats make client integration difficult.

Unified Response Structure

Define a common result interface and enum, then a generic Result<T> class to wrap all responses.

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

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

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

ResponseBodyAdvice for Automatic Wrapping

Implement ResponseBodyAdvice to intercept controller return values and wrap them with Result.success(...). The supports method always returns true, and beforeBodyWrite checks if the body is already a Result before wrapping.

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        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;
        }
        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 Return Types

When a controller returns String, the default StringHttpMessageConverter prevents JSON conversion, causing a ClassCastException. Two solutions are provided:

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

Reorder the message converters so that MappingJackson2HttpMessageConverter is evaluated before StringHttpMessageConverter.

Parameter Validation with JSR‑303

Use Spring’s @Validated together with annotations such as @Min, @Max, @NotBlank, @Email, etc., to automatically validate @PathVariable, @RequestParam, and @RequestBody parameters. Validation failures throw MethodArgumentNotValidException or ConstraintViolationException.

Custom Validation Annotation

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    boolean required() default true;
    String message() default "不是一个手机号码格式";
    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 context) {
        if (required && !StringUtils.hasText(value)) return false;
        return !StringUtils.hasText(value) || pattern.matcher(value).matches();
    }
}

Custom Exceptions and Global Exception Handling

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

public class ForbiddenException extends RuntimeException {
    public ForbiddenException(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) {
        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 separating concerns, using a unified Result wrapper, leveraging ResponseBodyAdvice, applying JSR‑303 validation (including custom validators), and handling exceptions globally, controller code becomes concise, maintainable, and consistent, allowing developers to focus on core 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.

JavaspringControllerexceptionhandlingresponsebodyadvice
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.