在Spring Boot或Spring MVC应用中,Controller层负责接收和响应请求,其定位如同“不可或缺的配角”。它虽不处理核心业务逻辑,却是系统内外交互的关键枢纽。一个设计优良的Controller层能极大提升代码的可维护性与接口的友好性。
常见的Controller层问题
通常,Controller层需要完成以下几项工作:
- 接收请求并解析参数
- 调用Service执行业务逻辑(可能包含参数校验)
- 捕获业务异常并反馈
- 业务执行成功后做出响应
参考以下传统代码示例:
// 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;
}
}
上述实现方式存在几个明显问题:
- 职责耦合:参数校验与业务逻辑混杂在Service中,违背单一职责原则。
- 代码重复:相同的异常可能在多处抛出,导致冗余代码。
- 响应不统一:成功与失败的响应格式不一致,给接口调用方带来不便。
Controller层的优化实践
1. 统一响应结构
统一的返回结构对于前后端分离项目至关重要,它能明确告知调用方接口的执行状态(成功/失败)及具体信息。
首先,定义状态码接口和常用枚举:
// 定义返回状态接口
public interface IResult {
Integer getCode();
String getMessage();
}
// 常用结果枚举
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
// 省略构造方法和getter
}
接着,定义统一的响应包装类:
@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 <T> Result<T> success(String message, T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), message, 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);
}
}
2. 利用ResponseBodyAdvice实现全局包装
若在每个Controller方法中手动返回Result对象,则会产生大量重复代码。可以利用Spring MVC提供的ResponseBodyAdvice接口进行全局响应包装。
ResponseBodyAdvice允许我们在HttpMessageConverter进行类型转换前,对Controller返回值进行拦截处理。
// 注意:若项目引入了Swagger或Knife4j,basePackages需指定为自己的项目包路径
@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;
}
// 统一包装为成功结果
return Result.success(body);
}
}
3. 处理String类型返回值异常
直接使用上述ResponseAdvice处理String类型返回值时,可能抛出类型转换异常。这是因为StringHttpMessageConverter的顺序优先于MappingJackson2HttpMessageConverter。
解决方案一:在beforeBodyWrite中手动处理String类型。
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public Object beforeBodyWrite(Object body, ...) {
if (body instanceof Result) {
return body;
}
// 对String类型特殊处理
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return Result.success(body);
}
}
// Controller方法需指定produces
@GetMapping(value = "/returnString", produces = "application/json; charset=UTF-8")
public String returnString() {
return "success";
}
解决方案二(推荐):调整HttpMessageConverter的顺序,将MappingJackson2HttpMessageConverter置前。
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (int i = 0; i < converters.size(); i++) {
if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
// 将Jackson转换器与首位交换
MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converters.get(i);
converters.set(i, converters.get(0));
converters.set(0, jacksonConverter);
break;
}
}
}
}
4. 参数校验解耦
借助JSR 303(Bean Validation)标准及其实现(如Hibernate Validator),可以将参数校验逻辑从业务代码中彻底解耦。
4.1 @PathVariable 与 @RequestParam 校验
在Controller类上标注@Validated,并在参数上直接添加校验注解。
@RestController
@RequestMapping("/example")
@Validated // 开启方法级参数校验
public class ExampleController {
@GetMapping("/{id}")
public Integer getDetail(@PathVariable("id") @Min(1) @Max(100) Integer id) {
return id * id;
}
@GetMapping("/search")
public TestDTO search(@RequestParam @NotBlank @Email String email) {
TestDTO dto = new TestDTO();
dto.setEmail(email);
return dto;
}
}
校验失败将抛出ConstraintViolationException。
4.2 @RequestBody 参数校验
在DTO字段上定义校验规则,并在Controller参数前添加@Validated或@Valid注解。
// 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
public class TestController {
@PostMapping("/register")
public void register(@RequestBody @Validated TestDTO testDTO) {
// 参数校验通过后,才会执行此方法
userService.register(testDTO);
}
}
校验失败将抛出MethodArgumentNotValidException。
4.3 自定义校验规则
当标准注解无法满足复杂业务时,可以自定义校验器。
- 定义自定义注解:
@Target({ElementType.FIELD, 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 {};
}
-
实现校验逻辑:
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private boolean required;
private final Pattern pattern = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public void initialize(Mobile constraintAnnotation) {
this.required = constraintAnnotation.required();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (!required && !StringUtils.hasText(value)) {
return true; // 非必填且为空,跳过校验
}
return pattern.matcher(value).matches();
}
}
- 使用自定义注解:
@Data
public class UserDTO {
@Mobile
private String phoneNumber;
}
5. 统一异常处理
自定义业务异常并结合@RestControllerAdvice进行全局异常拦截,可以返回统一结构的错误信息,并保证HTTP状态码为200(由业务码区分状态)。
- 定义业务异常:
// 业务异常
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 GlobalExceptionHandler {
@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);
}
// 处理@RequestBody参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("参数校验失败:");
for (FieldError error : bindingResult.getFieldErrors()) {
sb.append(error.getField()).append(": ").append(error.getDefaultMessage()).append("; ");
}
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), sb.toString());
}
// 处理@PathVariable和@RequestParam参数校验异常
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
}
// 兜底异常处理
@ExceptionHandler(Exception.class)
public Result<?> handleOtherException(Exception ex) {
// 生产环境可记录日志,返回更通用的错误信息
return Result.failed("系统繁忙,请稍后重试");
}
}
通过Spring AOP机制,@ExceptionHandler能够优雅地拦截并处理Controller层及其下层抛出的特定异常。
总结
经过上述改造,Controller层的职责变得清晰而纯粹:
- 参数校验:通过声明式注解完成,与业务逻辑解耦。
- 响应包装:通过全局切面自动完成,格式统一。
- 异常处理:通过全局处理器分类处理,反馈友好。
这样的Controller层设计遵循了单一职责原则,提升了代码的可读性与可维护性,使开发者能更专注于核心业务逻辑的实现。