当前位置: 首页 > news >正文

Spring Boot 数据校验与全局异常处理最佳实践

Spring Boot 数据校验与全局异常处理最佳实践

引言

数据校验是保障应用程序数据完整性和安全性的关键环节。Spring Boot 提供了强大的数据校验支持,结合全局异常处理机制,可以构建健壮的错误处理体系。本文将深入探讨数据校验的各种技术手段和全局异常处理的最佳实践。

一、数据校验基础

1.1 JSR-303/JSR-380 规范

JSR-303(Bean Validation 1.0)和 JSR-380(Bean Validation 2.0)定义了 Java Bean 校验的标准规范。Spring Boot 默认集成了 Hibernate Validator,这是 JSR-380 的参考实现。

1.2 常用校验注解

注解说明示例
@NotNull字段不能为空@NotNull(message = "ID不能为空")
@NotBlank字符串不能为空且长度大于0@NotBlank(message = "名称不能为空")
@NotEmpty集合/数组不能为空@NotEmpty(message = "列表不能为空")
@Size字符串/集合长度范围@Size(min=2, max=50)
@Min/@Max数值最小值/最大值@Min(1) @Max(100)
@Positive/@Negative正数/负数@Positive(message = "数量必须为正数")
@Email邮箱格式校验@Email(message = "邮箱格式不正确")
@Pattern正则表达式校验@Pattern(regexp="^\d{11}$")
@Valid级联校验@Valid private Address address

二、请求参数校验

2.1 添加依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>

2.2 请求体校验

import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.*; import java.math.BigDecimal; import java.util.List; @Data public class OrderCreateRequest { @NotBlank(message = "客户ID不能为空") @Size(max = 50, message = "客户ID长度不能超过50") private String customerId; @NotNull(message = "订单金额不能为空") @Positive(message = "订单金额必须大于0") private BigDecimal amount; @Size(max = 200, message = "备注长度不能超过200") private String remark; @NotEmpty(message = "订单项不能为空") @Size(max = 100, message = "订单项数量不能超过100") @Valid private List<OrderItemRequest> items; @Data public static class OrderItemRequest { @NotBlank(message = "商品ID不能为空") private String productId; @NotNull(message = "数量不能为空") @Positive(message = "数量必须大于0") private Integer quantity; @NotNull(message = "单价不能为空") @Positive(message = "单价必须大于0") private BigDecimal unitPrice; } }

2.3 Controller 层校验

import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import javax.validation.constraints.Min; import javax.validation.constraints.Pattern; @RestController @RequestMapping("/api/orders") @Validated public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping public ResponseEntity<OrderResponse> createOrder( @Valid @RequestBody OrderCreateRequest request) { OrderResponse response = orderService.createOrder(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @GetMapping("/{orderId}") public ResponseEntity<OrderResponse> getOrderById( @Pattern(regexp = "^ord-\\d+$", message = "订单ID格式不正确") @PathVariable String orderId) { OrderResponse response = orderService.getOrderById(orderId); return ResponseEntity.ok(response); } @GetMapping public ResponseEntity<PageResponse<OrderResponse>> listOrders( @Min(value = 0, message = "页码不能小于0") @RequestParam(defaultValue = "0") int page, @Min(value = 1, message = "每页大小不能小于1") @Max(value = 100, message = "每页大小不能超过100") @RequestParam(defaultValue = "10") int size) { Page<OrderResponse> orders = orderService.listOrders(page, size); return ResponseEntity.ok(PageResponse.from(orders)); } }

三、自定义校验注解

3.1 创建自定义注解

import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; @Documented @Constraint(validatedBy = PhoneValidator.class) @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Phone { String message() default "手机号格式不正确"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; boolean required() default true; }

3.2 实现校验器

import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.regex.Pattern; public class PhoneValidator implements ConstraintValidator<Phone, String> { private static final Pattern PHONE_PATTERN = Pattern.compile( "^1[3-9]\\d{9}$" ); private boolean required; @Override public void initialize(Phone constraintAnnotation) { this.required = constraintAnnotation.required(); } @Override public boolean isValid(String phone, ConstraintValidatorContext context) { if (!required && (phone == null || phone.isEmpty())) { return true; } if (phone == null || phone.isEmpty()) { return false; } return PHONE_PATTERN.matcher(phone).matches(); } }

3.3 使用自定义注解

@Data public class UserCreateRequest { @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 50, message = "用户名长度必须在2-50之间") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, message = "密码长度不能少于6位") private String password; @Phone(message = "手机号格式不正确") private String phone; @Email(message = "邮箱格式不正确") private String email; }

四、全局异常处理

4.1 创建全局异常处理器

import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationException( MethodArgumentNotValidException ex) { Map<String, String> errors = ex.getBindingResult() .getFieldErrors() .stream() .collect(Collectors.toMap( FieldError::getField, FieldError::getDefaultMessage )); ErrorResponse response = ErrorResponse.builder() .code(HttpStatus.BAD_REQUEST.value()) .message("请求参数校验失败") .timestamp(LocalDateTime.now()) .details(errors) .build(); return ResponseEntity.badRequest().body(response); } @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<ErrorResponse> handleConstraintViolationException( ConstraintViolationException ex) { Map<String, String> errors = ex.getConstraintViolations() .stream() .collect(Collectors.toMap( violation -> violation.getPropertyPath().toString(), ConstraintViolation::getMessage )); ErrorResponse response = ErrorResponse.builder() .code(HttpStatus.BAD_REQUEST.value()) .message("请求参数校验失败") .timestamp(LocalDateTime.now()) .details(errors) .build(); return ResponseEntity.badRequest().body(response); } @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity<ErrorResponse> handleMissingParameterException( MissingServletRequestParameterException ex) { Map<String, String> details = new HashMap<>(); details.put(ex.getParameterName(), "必填参数缺失"); ErrorResponse response = ErrorResponse.builder() .code(HttpStatus.BAD_REQUEST.value()) .message("请求参数缺失") .timestamp(LocalDateTime.now()) .details(details) .build(); return ResponseEntity.badRequest().body(response); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<ErrorResponse> handleTypeMismatchException( MethodArgumentTypeMismatchException ex) { Map<String, String> details = new HashMap<>(); details.put(ex.getName(), "参数类型不正确,期望类型: " + (ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown")); ErrorResponse response = ErrorResponse.builder() .code(HttpStatus.BAD_REQUEST.value()) .message("请求参数类型错误") .timestamp(LocalDateTime.now()) .details(details) .build(); return ResponseEntity.badRequest().body(response); } }

4.2 错误响应模型

import lombok.Builder; import lombok.Data; import java.time.LocalDateTime; import java.util.Map; @Data @Builder public class ErrorResponse { private int code; private String message; private LocalDateTime timestamp; private Map<String, String> details; private String path; }

4.3 自定义业务异常

import lombok.Getter; @Getter public class BusinessException extends RuntimeException { private final int code; private final String message; public BusinessException(int code, String message) { super(message); this.code = code; this.message = message; } public static BusinessException notFound(String message) { return new BusinessException(HttpStatus.NOT_FOUND.value(), message); } public static BusinessException badRequest(String message) { return new BusinessException(HttpStatus.BAD_REQUEST.value(), message); } public static BusinessException forbidden(String message) { return new BusinessException(HttpStatus.FORBIDDEN.value(), message); } public static BusinessException conflict(String message) { return new BusinessException(HttpStatus.CONFLICT.value(), message); } }

4.4 处理自定义业务异常

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusinessException( BusinessException ex) { ErrorResponse response = ErrorResponse.builder() .code(ex.getCode()) .message(ex.getMessage()) .timestamp(LocalDateTime.now()) .build(); return ResponseEntity.status(ex.getCode()).body(response); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) { ErrorResponse response = ErrorResponse.builder() .code(HttpStatus.INTERNAL_SERVER_ERROR.value()) .message("服务器内部错误") .timestamp(LocalDateTime.now()) .build(); // 记录日志 log.error("Unexpected error occurred", ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } }

五、Service 层校验

5.1 使用 Validator 进行手动校验

import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.util.Set; @Service @Validated public class OrderService { private final Validator validator; private final OrderRepository orderRepository; public OrderService(Validator validator, OrderRepository orderRepository) { this.validator = validator; this.orderRepository = orderRepository; } public OrderResponse createOrder(OrderCreateRequest request) { // 手动校验 Set<ConstraintViolation<OrderCreateRequest>> violations = validator.validate(request); if (!violations.isEmpty()) { String errorMessage = violations.stream() .map(v -> v.getPropertyPath() + ": " + v.getMessage()) .collect(Collectors.joining(", ")); throw BusinessException.badRequest(errorMessage); } // 业务校验 validateOrderRequest(request); // 创建订单逻辑 Order order = Order.builder() .customerId(request.getCustomerId()) .amount(request.getAmount()) .remark(request.getRemark()) .status(OrderStatus.PENDING) .build(); order = orderRepository.save(order); // 保存订单项 // ... return OrderResponse.from(order); } private void validateOrderRequest(OrderCreateRequest request) { // 检查客户是否存在 if (!customerRepository.existsById(request.getCustomerId())) { throw BusinessException.badRequest("客户不存在"); } // 检查库存 for (OrderItemRequest item : request.getItems()) { Product product = productRepository.findById(item.getProductId()) .orElseThrow(() -> BusinessException.badRequest("商品不存在: " + item.getProductId())); if (product.getStock() < item.getQuantity()) { throw BusinessException.badRequest("商品库存不足: " + product.getName()); } } } }

六、校验分组

6.1 定义校验分组

public interface ValidationGroups { interface Create {} interface Update {} interface Delete {} }

6.2 使用校验分组

import javax.validation.constraints.*; import javax.validation.groups.Default; @Data public class UserUpdateRequest { @NotNull(message = "用户ID不能为空", groups = {ValidationGroups.Update.class}) private Long id; @Size(min = 2, max = 50, message = "用户名长度必须在2-50之间", groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}) private String username; @Email(message = "邮箱格式不正确", groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}) private String email; @Phone(message = "手机号格式不正确", groups = Default.class) private String phone; }

6.3 Controller 层指定校验分组

@RestController @RequestMapping("/api/users") public class UserController { @PutMapping public ResponseEntity<UserResponse> updateUser( @Validated(ValidationGroups.Update.class) @RequestBody UserUpdateRequest request) { UserResponse response = userService.updateUser(request); return ResponseEntity.ok(response); } }

七、国际化校验消息

7.1 配置消息源

spring: messages: basename: messages encoding: UTF-8

7.2 创建国际化资源文件

# messages.properties validation.phone=手机号格式不正确 validation.email=邮箱格式不正确 validation.notBlank=不能为空 validation.size=长度必须在{min}-{max}之间 validation.min=最小值为{value} validation.max=最大值为{value} validation.positive=必须为正数
# messages_zh_CN.properties validation.phone=手机号格式不正确 validation.email=邮箱格式不正确 validation.notBlank=不能为空 validation.size=长度必须在{min}-{max}之间 validation.min=最小值为{value} validation.max=最大值为{value} validation.positive=必须为正数

7.3 使用国际化消息

@Data public class UserCreateRequest { @NotBlank(message = "{validation.notBlank}") @Size(min = 2, max = 50, message = "{validation.size}") private String username; @Email(message = "{validation.email}") private String email; @Phone(message = "{validation.phone}") private String phone; }

八、最佳实践

8.1 校验层次策略

┌─────────────────────────────────────────────────────────────┐ │ Controller 层 │ │ - 请求参数格式校验(类型、格式、基本约束) │ ├─────────────────────────────────────────────────────────────┤ │ Service 层 │ │ - 业务规则校验(业务逻辑约束、状态校验) │ │ - 数据完整性校验(关联数据存在性检查) │ ├─────────────────────────────────────────────────────────────┤ │ Repository 层 │ │ - 数据库约束校验(唯一约束、外键约束) │ └─────────────────────────────────────────────────────────────┘

8.2 错误响应规范

  1. 统一响应格式:使用统一的 ErrorResponse 结构
  2. 明确错误码:使用 HTTP 状态码 + 业务错误码
  3. 详细错误信息:提供字段级别的错误详情
  4. 国际化支持:支持多语言错误消息

8.3 性能优化建议

  1. 避免重复校验:在 Controller 层校验后,Service 层只需做业务校验
  2. 异步校验:对于复杂校验逻辑,考虑异步处理
  3. 缓存校验结果:对于高频校验规则,缓存校验结果

8.4 安全考虑

  1. 输入过滤:对用户输入进行严格校验,防止注入攻击
  2. 敏感信息保护:错误响应中不暴露敏感信息
  3. 日志脱敏:记录日志时对敏感数据进行脱敏处理

结语

数据校验和异常处理是构建健壮应用的重要组成部分。通过合理使用 JSR-380 校验注解、自定义校验器和全局异常处理器,可以实现全面的数据校验和统一的错误处理机制。在实际项目中,应根据业务需求设计分层校验策略,结合国际化和安全考虑,构建高质量的错误处理体系。

http://www.jsqmd.com/news/795191/

相关文章:

  • Fooocus:3分钟从AI绘画小白到专业创作者的秘密武器
  • 国内余氯电极十大品牌排名 - 仪表人小余
  • AI生成专著神器来袭!一键打造20万字专著,开启写作新体验!
  • 3步重塑开发工作流:Ctool一站式工具集突破效率瓶颈
  • 护发精油品牌测评:暨护发精油推荐的6款产品 - 速递信息
  • 如何快速批量下载抖音视频:免费开源工具完整指南
  • 2026 年度 GEO 服务行业影响力榜单:技术实力与市场口碑双维度权威评定 - 速递信息
  • StreamCap终极指南:如何轻松录制40+直播平台的免费开源工具
  • 题解:P5306 [COCI 2018/2019 #5] Transport
  • 欢客互动赋能泛家居全链路,让获客成交更简单的数智生态平台 - 速递信息
  • 广州白蚁防治公司哪家好?——广州市白蚁防治中心/越秀区/天河区/荔湾区/海珠区/白云区/番禺区 - 品牌推荐大师
  • Steam创意工坊终极下载指南:WorkshopDL让你免费获取1000+游戏模组
  • 丽水金价高悬,福正美变现为何成最优解? - 福正美黄金回收
  • 哈尔滨家政保姆行业解析:靠谱服务的核心判定标准 - 奔跑123
  • Linux Deadline 调度器的 put_prev_task:前一个 Deadline 任务处理
  • 终极Zotero Style插件:三步打造你的智能文献管理神器
  • [理论篇-14]大模型评估与可观测性——如何知道你的 AI 到底行不行
  • AI写专著解决方案:AI专著写作工具,高效产出20万字专业专著!
  • 添加公众号附件链接的工具软件(政企云文档小程序)终身免费使用. - 政企云文档
  • Excel智能革命:用自然语言对话实现数据处理自动化
  • 太原高端水漆定制认准客来福 十年三千户业主口碑之选 - 速递信息
  • NI PXI-5922数字化仪:高精度动态信号采集技术解析
  • 2026企业级CRM综合实力榜单:5大标杆产品驱动行业数字化升级 - Blue_dou
  • 深岩银河存档编辑器终极指南:快速掌握DRG游戏存档修改技巧 [特殊字符]
  • 模拟文件打开写入关闭的过程
  • 2026年中山GEO优化服务商推荐:五家实力机构综合选型分析参考 - 产业观察网
  • 银泰百货卡回收技巧分享:常见回收问题解答! - 团团收购物卡回收
  • 免费LLM API集成实战:从选型到构建高可用AI服务
  • 华为光猫配置解密工具终极技术指南:深度解析AES加密与XML/CFG文件处理
  • 2026年中山五金配件定制厂家怎么选?工程装修采购避坑指南与靠谱供应商对标 - 优质企业观察收录