SpringBoot参数校验和返回值封装
Spring Boot 简化了配置,但日志管理依然需要重视。日志配置、链路追踪、排查思路都是日常开发中会遇到的问题。本文讲实际项目中的日志管理经验。
参数校验
#
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
|
#
常用校验注解
| 注解 |
说明 |
| @NotNull |
不能为 null |
| @NotBlank |
不能为 null 且不为空字符串 |
| @NotEmpty |
不能为 null 且不为空(字符串/集合) |
| @Min / @Max |
数字范围 |
| @Size |
字符串长度/集合大小 |
| @Email |
邮箱格式 |
| @Pattern |
正则匹配 |
| @Positive |
正数 |
| @Future / @Past |
未来/过去日期 |
#
请求参数校验
##
@Valid + @RequestBody
@Data public class CreateUserRequest { @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 20, message = "用户名长度2-20") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, message = "密码至少6位") private String password; @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; @Min(value = 1, message = "年龄不能小于1") @Max(value = 150, message = "年龄不能大于150") private Integer age; }
|
@RestController @RequestMapping("/users") public class UserController { @PostMapping public Result<User> createUser(@Valid @RequestBody CreateUserRequest request) { } }
|
##
@Validated + @RequestParam / @PathVariable
@RestController @Validated public class UserController { @GetMapping("/users/{id}") public Result<User> getUser(@PathVariable @Min(1) Long id) { } @GetMapping("/users") public Result<List<User>> listUsers( @RequestParam @Min(1) @Max(100) Integer pageSize) { } }
|
##
分组校验
public interface CreateGroup {} public interface UpdateGroup {}
@Data public class UserRequest { @NotNull(groups = UpdateGroup.class) private Long id; @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}) private String username; @NotBlank(groups = CreateGroup.class) private String password; }
|
@PostMapping public Result<User> create(@Validated(CreateGroup.class) @RequestBody UserRequest request) { }
@PutMapping public Result<User> update(@Validated(UpdateGroup.class) @RequestBody UserRequest request) { }
|
##
嵌套校验
@Data public class CreateOrderRequest { @NotBlank private String orderNo; @Valid @NotEmpty private List<OrderItemRequest> items; }
@Data public class OrderItemRequest { @NotBlank private String skuId; @Min(1) private Integer quantity; }
|
#
自定义校验注解
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneValidator.class) public @interface Phone { String message() default "手机号格式不正确"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
public class PhoneValidator implements ConstraintValidator<Phone, String> { private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; } return PHONE_PATTERN.matcher(value).matches(); } }
|
统一返回值封装
#
统一响应结构
@Data public class Result<T> { private Integer code; private String message; private T data; private Long timestamp; public Result() { this.timestamp = System.currentTimeMillis(); } public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> fail(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } public static <T> Result<T> fail(String message) { return fail(500, message); } }
|
#
全局响应封装
@RestControllerAdvice(basePackages = "com.example.controller") public class ResponseAdvice implements ResponseBodyAdvice<Object> { @Autowired private ObjectMapper objectMapper; @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return !returnType.getParameterType().equals(Result.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof String) { try { return objectMapper.writeValueAsString(Result.success(body)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } return Result.success(body); } }
|
全局异常处理
@RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(MethodArgumentNotValidException.class) public Result<Void> handleValidation(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); return Result.fail(400, message); } @ExceptionHandler(ConstraintViolationException.class) public Result<Void> handleConstraintViolation(ConstraintViolationException e) { String message = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining(", ")); return Result.fail(400, message); } @ExceptionHandler(BusinessException.class) public Result<Void> handleBusiness(BusinessException e) { log.warn("业务异常: {}", e.getMessage()); return Result.fail(e.getCode(), e.getMessage()); } @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { log.error("系统异常", e); return Result.fail(500, "系统繁忙,请稍后再试"); } }
|
分页结果封装
@Data public class PageResult<T> { private List<T> list; private Long total; private Integer pageNum; private Integer pageSize; private Integer totalPages; public static <T> PageResult<T> of(PageInfo<T> pageInfo) { PageResult<T> result = new PageResult<>(); result.setList(pageInfo.getList()); result.setTotal(pageInfo.getTotal()); result.setPageNum(pageInfo.getPageNum()); result.setPageSize(pageInfo.getPageSize()); result.setTotalPages(pageInfo.getPages()); return result; } }
|
最佳实践
#
1. 校验消息统一
# messages.properties javax.validation.constraints.NotBlank.message={0}不能为空 javax.validation.constraints.Size.message={0}长度必须在{min}到{max}之间
|
#
2. 控制器保持简洁
@RestController @RequiredArgsConstructor public class UserController { private final UserService userService; @PostMapping("/users") public Result<Long> create(@Valid @RequestBody CreateUserRequest request) { return Result.success(userService.create(request)); } }
|
#
3. 异常不暴露内部信息
@ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { log.error("系统异常", e); return Result.fail(500, "系统繁忙"); }
|
总结
| 场景 |
方案 |
| 请求体校验 |
@Valid + @RequestBody |
| URL 参数校验 |
@Validated + @RequestParam |
| 分组校验 |
@Validated(Group.class) |
| 嵌套校验 |
@Valid |
| 自定义规则 |
自定义注解 + Validator |
| 返回值封装 |
ResponseBodyAdvice |
| 异常处理 |
@RestControllerAdvice |
规范的参数校验和返回值封装,是构建高质量 REST API 的基础。
核心要点
日志级别设置:根据环境设置合适的级别
日志格式配置:添加 traceId 便于链路追踪
日志输出:控制台输出和文件输出的配置
日志归档:设置滚动策略和保留时间
总结
日志是排查问题的生命线,合理配置日志可以提升排查效率。在实际项目中,结合 ELK 等工具搭建日志系统,可以更好地管理和分析日志。