Java参数校验案例如何实操

wen java案例 28

本文目录导读:

Java参数校验案例如何实操

  1. 基础环境搭建
  2. 实体类参数校验
  3. Controller层校验
  4. 全局异常处理
  5. 自定义校验注解
  6. Service层校验
  7. 复杂场景校验
  8. 单元测试

我来详细介绍Java参数校验的实操案例,涵盖基础到高级的用法。

基础环境搭建

Maven依赖

<!-- Spring Boot Starter Validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 非Spring Boot项目 -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>

实体类参数校验

用户注册DTO

import javax.validation.constraints.*;
import java.time.LocalDate;
public class UserRegisterDTO {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
    private String username;
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^&*]).{6,20}$", 
             message = "密码必须包含数字、字母和特殊字符")
    private String password;
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    @Min(value = 18, message = "年龄必须大于18岁")
    @Max(value = 100, message = "年龄必须小于100岁")
    private Integer age;
    @Past(message = "生日必须是在过去")
    private LocalDate birthday;
    // getters and setters 省略
}

分组校验

import javax.validation.groups.Default;
public class ValidationGroups {
    // 新增分组
    public interface Create extends Default {}
    // 更新分组
    public interface Update extends Default {}
    // 查询分组
    public interface Query extends Default {}
}
// 实体类使用分组
public class ProductDTO {
    @Null(groups = Create.class, message = "新增时ID必须为空")
    @NotNull(groups = Update.class, message = "更新时ID不能为空")
    private Long id;
    @NotBlank(groups = {Create.class, Update.class}, message = "商品名称不能为空")
    private String name;
    @Positive(groups = {Create.class, Update.class}, message = "价格必须为正数")
    private BigDecimal price;
    @NotNull(groups = Query.class, message = "查询条件不能为空")
    private String keyword;
    // getters and setters
}

Controller层校验

基础Controller校验

import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import org.springframework.validation.annotation.Validated;
@RestController
@RequestMapping("/api/users")
@Validated  // 类级别注解,支持方法参数校验
public class UserController {
    // 使用@Valid触发校验
    @PostMapping("/register")
    public Result<String> register(@Valid @RequestBody UserRegisterDTO userDTO) {
        // 如果校验失败,会抛出MethodArgumentNotValidException异常
        return Result.success("注册成功");
    }
    // 使用@Validated触发分组校验
    @PutMapping("/update")
    public Result<String> update(@Validated(Update.class) @RequestBody ProductDTO product) {
        return Result.success("更新成功");
    }
}

路径参数和查询参数校验

@RestController
@RequestMapping("/api/products")
@Validated
public class ProductController {
    // 路径参数校验
    @GetMapping("/{id}")
    public Result<Product> getProduct(
            @PathVariable 
            @Min(value = 1, message = "ID必须大于0") Long id) {
        return Result.success(productService.getById(id));
    }
    // 查询参数校验
    @GetMapping("/search")
    public Result<List<Product>> searchProducts(
            @RequestParam 
            @NotBlank(message = "关键词不能为空") 
            @Size(min = 2, max = 50, message = "关键词长度需在2-50之间") String keyword,
            @RequestParam(required = false)
            @Min(value = 1, message = "页码必须大于0")
            @Max(value = 100, message = "页码不能大于100") Integer page,
            @RequestParam(required = false)
            @Positive(message = "每页数量必须为正数") Integer size) {
        return Result.success(productService.search(keyword, page, size));
    }
}

全局异常处理

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
    // 处理@Valid校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }
    // 处理@Validated和参数校验失败
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<Void> handleConstraintViolationException(ConstraintViolationException ex) {
        String message = ex.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }
}

自定义校验注解

创建自定义注解

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface ValidPhone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    // 是否允许空值
    boolean nullable() default false;
}

实现校验器

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class PhoneNumberValidator implements ConstraintValidator<ValidPhone, String> {
    private static final Pattern PHONE_PATTERN = 
            Pattern.compile("^1[3-9]\\d{9}$");
    private boolean nullable;
    @Override
    public void initialize(ValidPhone annotation) {
        this.nullable = annotation.nullable();
    }
    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        // 如果允许为空且值为空,返回true
        if (nullable && phone == null) {
            return true;
        }
        // 如果不允许为空且值为空,返回false
        if (!nullable && (phone == null || phone.trim().isEmpty())) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("手机号不能为空")
                   .addConstraintViolation();
            return false;
        }
        return PHONE_PATTERN.matcher(phone).matches();
    }
}

使用自定义注解

public class ContactDTO {
    @ValidPhone(nullable = false)
    private String phone;
    @ValidPhone(nullable = true)
    private String backupPhone;
}

Service层校验

import org.springframework.stereotype.Service;
import javax.validation.Validator;
import org.springframework.beans.factory.annotation.Autowired;
@Service
@Validated
public class UserService {
    @Autowired
    private Validator validator;
    // 方法级别校验
    public User getUserById(@Min(value = 1, message = "ID必须大于0") Long id) {
        return userRepository.findById(id).orElseThrow();
    }
    // 手动触发校验
    public void createUser(UserRegisterDTO userDTO) {
        // 手动校验
        Set<ConstraintViolation<UserRegisterDTO>> violations = 
                validator.validate(userDTO);
        if (!violations.isEmpty()) {
            String message = violations.stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(", "));
            throw new IllegalArgumentException(message);
        }
        // 业务逻辑
        userRepository.save(userDTO);
    }
}

复杂场景校验

条件校验(使用@ScriptAssert)

import org.hibernate.validator.constraints.ScriptAssert;
@ScriptAssert(lang = "javascript", 
              script = "_this.password.equals(_this.confirmPassword)", 
              message = "两次输入的密码不一致")
public class PasswordChangeDTO {
    @NotBlank(message = "原密码不能为空")
    private String oldPassword;
    @NotBlank(message = "新密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度需要在6-20之间")
    private String password;
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
    // getters and setters
}

集合参数校验

@RestController
@Validated
public class BatchController {
    // 批量删除
    @DeleteMapping("/batch-delete")
    public Result<Void> batchDelete(
            @NotEmpty(message = "ID列表不能为空")
            @Size(min = 1, max = 100, message = "批量删除数量在1-100之间")
            @RequestBody List<@Min(value = 1, message = "ID必须大于0") Long> ids) {
        return Result.success();
    }
    // 批量添加
    @PostMapping("/batch-add")
    public Result<Void> batchAdd(
            @NotEmpty(message = "列表不能为空")
            @Size(max = 50, message = "批量添加不能超过50条")
            @RequestBody List<@Valid UserRegisterDTO> users) {
        return Result.success();
    }
}

单元测试

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class UserRegisterDTOTest {
    @Autowired
    private Validator validator;
    @Test
    public void testValidation() {
        // 准备测试数据
        UserRegisterDTO user = new UserRegisterDTO();
        user.setUsername("ab");  // 太短
        user.setPassword("123456");  // 不包含字母和特殊字符
        user.setEmail("invalid-email");  // 格式错误
        user.setPhone("1234567890");  // 格式错误
        // 执行校验
        Set<ConstraintViolation<UserRegisterDTO>> violations = 
                validator.validate(user);
        // 验证结果
        assertFalse(violations.isEmpty());
        assertEquals(4, violations.size());
        // 验证具体错误信息
        assertTrue(violations.stream()
                .anyMatch(v -> v.getMessage().contains("用户名长度")));
        assertTrue(violations.stream()
                .anyMatch(v -> v.getMessage().contains("密码必须包含")));
        assertTrue(violations.stream()
                .anyMatch(v -> v.getMessage().contains("邮箱格式")));
        assertTrue(violations.stream()
                .anyMatch(v -> v.getMessage().contains("手机号格式")));
    }
}

Java参数校验的最佳实践:

  1. 使用标准注解:利用 @NotBlank@Size@Pattern 等内置注解
  2. 自定义注解:处理特定业务规则
  3. 分组校验:同一实体在不同场景下使用不同规则
  4. 全局异常处理:统一格式化错误信息
  5. 日志记录:记录校验失败信息便于排查
  6. 前端联动:前后端使用相同的校验规则
  7. 性能考虑:避免过度校验,合理设置校验范围

这样一套完整的参数校验方案可以大大提高代码的健壮性和可维护性。

抱歉,评论功能暂时关闭!