Backend Development 19 min read

Refactor Spring Boot Controllers for Clean Architecture and Unified Responses

This article explains the essential duties of a Spring MVC Controller, identifies common pitfalls such as duplicated validation and inconsistent responses, and demonstrates how to introduce a unified result wrapper, ResponseBodyAdvice, proper message‑converter ordering, JSR‑303 validation, custom validators, and global exception handling to produce concise, maintainable backend code.

macrozheng
macrozheng
macrozheng
Refactor Spring Boot Controllers for Clean Architecture and Unified Responses

Controller Role

The Controller layer acts as an indispensable façade that receives requests, delegates to Service for business logic (including validation), catches business exceptions, and returns responses.

Main Responsibilities

Parse request parameters.

Invoke Service methods (often with validation).

Handle business‑logic exceptions.

Return successful results.

Typical Implementation

<code>@Data
public class TestDTO {
    private Integer num;
    private String type;
}

@Service
public class TestService {
    public Double service(TestDTO dto) throws Exception {
        if (dto.getNum() <= 0) {
            throw new Exception("输入的数字需要大于0");
        }
        if (dto.getType().equals("square")) {
            return Math.pow(dto.getNum(), 2);
        }
        if (dto.getType().equals("factorial")) {
            double result = 1;
            int num = dto.getNum();
            while (num > 1) {
                result *= num;
                num--;
            }
            return result;
        }
        throw new Exception("未识别的算法");
    }
}

@RestController
public class TestController {
    private TestService testService;

    @PostMapping("/test")
    public Double test(@RequestBody TestDTO dto) {
        try {
            return testService.service(dto);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

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

Problems with the Current Approach

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

Identical exceptions are thrown from many services, causing code duplication.

Response formats for errors and successes are inconsistent, making client integration difficult.

Unified Return Structure

<code>// Define a common result interface
public interface IResult {
    Integer getCode();
    String getMessage();
}

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

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

ResponseBodyAdvice for Automatic Wrapping

<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 selected converter is

StringHttpMessageConverter

, the wrapper must be converted to JSON manually; otherwise

MappingJackson2HttpMessageConverter

handles the conversion automatically.

Adjusting Message Converter Order

<code>@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new MappingJackson2HttpMessageConverter()); // ensure JSON converter runs first
    }
}
</code>

Parameter Validation (JSR‑303)

Spring Validation builds on the JSR‑303

validation-api

and Hibernate Validator. Use annotations such as

@NotBlank

,

@Email

,

@Min

,

@Max

on method parameters or DTO fields, and annotate the controller with

@Validated

to trigger automatic validation.

<code>@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
    @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;
    }
}
</code>

Custom Validation Annotation

<code>@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<? 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) return pattern.matcher(value).matches();
        return !StringUtils.hasText(value) || pattern.matcher(value).matches();
    }
}
</code>

Custom Exceptions and Global Exception Handling

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

Conclusion

By introducing a unified result wrapper, ResponseBodyAdvice, proper message‑converter ordering, comprehensive JSR‑303 validation, custom validators, and a global exception handler, Controller code becomes concise, responsibilities are clearly separated, and both success and error responses share a consistent structure, greatly improving maintainability.

JavaException HandlingValidationSpring BootControllerResponseBodyAdvice
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.