在Web开发中,Controller层作为请求入口,负责接收和响应客户端请求,其设计质量直接影响API的易用性和维护性。本文将探讨如何通过统一响应结构、参数校验和异常处理来优化后端API接口设计。
常见问题分析
典型的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;
}
}
这种实现方式存在几个明显问题:
- 参数校验与业务逻辑高度耦合,违反单一职责原则
- 异常处理重复且不一致
- 响应格式不统一,给接口对接带来困难
Controller层优化方案
统一响应结构设计
标准化返回值对于前后端协作至关重要,通过状态码和消息能够清晰表达接口执行结果。
// 返回结果接口定义
public interface IResult {
Integer getCode();
String getMessage();
}
// 常用结果枚举
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
private Integer code;
private String message;
// 省略get、set方法和构造方法
}
// 统一返回数据结构
@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);
}
public static <T> Result<T> instance(Integer code, String message, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
}
响应统一包装
Spring Boot框架提供了ResponseBodyAdvice接口,可以在消息转换前对Controller返回值进行统一处理。
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);
}
supports: 判断是否执行处理逻辑
beforeBodyWrite: 对响应体进行具体处理
@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);
}
}
字符串类型处理优化
直接使用ResponseBodyAdvice处理字符串类型时会遇到类型转换异常,需要通过配置调整解决。
@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) {
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter =
(MappingJackson2HttpMessageConverter) converters.get(i);
converters.set(i, converters.get(0));
converters.set(0, mappingJackson2HttpMessageConverter);
break;
}
}
}
}
参数校验实现
JSR303规范定义了参数校验标准,Spring Validation对其进行了封装,支持自动校验。
PathVariable和RequestParam校验
@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 testDTO = new TestDTO();
testDTO.setEmail(email);
return testDTO;
}
}
RequestBody参数校验
// 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
@RequestMapping("/pretty")
public class TestController {
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
this.testService.save(testDTO);
}
}
自定义校验规则
当标准校验规则不满足需求时,可以扩展自定义校验器。
// 自定义注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR, 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, CharSequence> {
private boolean required = false;
private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
@Override
public void initialize(Mobile constraintAnnotation) {
this.required = constraintAnnotation.required();
}
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if (this.required) {
return isMobile(value);
}
if (StringUtils.hasText(value)) {
return isMobile(value);
}
return true;
}
private boolean isMobile(final CharSequence str) {
Matcher m = pattern.matcher(str);
return m.matches();
}
}
异常处理机制
统一异常处理确保API返回格式一致性,同时细化业务异常分类。
// 自定义异常
public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message);
}
}
public class BusinessException extends RuntimeException {
public BusinessException(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) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
if (StringUtils.hasText(msg)) {
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
}
return Result.failed(ResultEnum.VALIDATE_FAILED);
}
@ExceptionHandler({ConstraintViolationException.class})
public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
if (StringUtils.hasText(ex.getMessage())) {
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
}
return Result.failed(ResultEnum.VALIDATE_FAILED);
}
@ExceptionHandler({Exception.class})
public Result<?> handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}
总结
通过上述优化措施,Controller层代码变得更加简洁清晰。参数校验规则明确可见,异常处理统一规范,响应结构标准化。这种设计让开发者能够更专注于业务逻辑实现,提升代码质量和维护性,是现代后端API接口开发的推荐实践。