JSR 303全解析:如何在Java项目中实施高效数据校验

1. JSR 303是什么?

JSR 303(Java Specification Request 303),也称为Bean Validation,是Java中的一个规范,用于定义Java对象的校验规则。

1.1 JSR 303的主要功能

  • 注解驱动:通过注解直接在Java类上定义校验规则。
  • 内置约束:如@NotNull、@Size、@Min、@Max等。
  • 自定义约束:可以定义自定义的校验注解和逻辑。
  • 分组校验:支持对不同场景(如创建和更新)进行分组校验。

1.2 常用注解

  • @NotNull:验证注解的元素值不是null。
  • @Size:验证注解的元素的大小在指定范围内。
  • @Min和@Max:验证注解的元素值在指定范围内。
  • @Email:验证注解的元素是一个合法的电子邮件地址。

2. 使用步骤

JSR 303 是一个规范,所以需要具体的实现。Hibernat Bean Validator 就是Bean Validator的实现

2.1.引入库

        
            org.springframework.boot
            spring-boot-starter-validation
        

        
            javax.validation
            validation-api
            2.0.1.Final
        

        
            org.hibernate
            hibernate-validator
            8.0.1.Final
        

2.2 实体类编写校验规则

代码如下(示例):

@Data
public class BrandEntity implements Serializable {
  
    /**
     * 品牌ID,用于标识品牌。
     * 
     * 此字段通过注解进行了不同的验证逻辑配置,以适应不同的业务场景。
     * 在更新操作(UpdateGroup)中,要求此字段不为空,确保了更新操作有明确的目标品牌ID。
     * 在新增操作(AddGroup)中,要求此字段为空,因为新增品牌时不应该预先指定ID。
     * 这种通过注解进行验证的方式,提高了代码的灵活性和可维护性,避免了在业务逻辑中硬编码验证逻辑。
     */
    @NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
    @Null(message = "新增不能指定id",groups = {AddGroup.class})
    private Long brandId;


    /**
     * 品牌名称字段。
     * 
     * 该字段是必填的,不允许为空字符串,这在添加和更新品牌信息时都必须遵守。
     * 使用@NotBlank注解来强制验证品牌名的非空性,如果为空,则会触发验证失败,
     * 返回相应的错误消息。
     */
    @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
    private String name;


  
    /**
     * 品牌logo地址
     * 
     * 此字段在添加(AddGroup)和更新(UpdateGroup)时都必须是一个合法的URL地址,
     * 以确保公司徽标的链接是有效和可访问的。使用@URL注解进行验证,
     * 如果不符合URL格式,则会提示指定的错误信息。
     * 
     * 使用@NotBlank注解确保在添加时该字段不为空,为空则认为是无效的输入。
     * 这是因为在更新时,如果用户没有提供新的徽标URL,可以保留旧的URL,
     * 所以在更新组(UpdateGroup)中,@NotBlank约束被移除,允许为空。
     */
    @NotBlank(groups = {AddGroup.class})
    @URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
    private String logo;

    
    /**
     * 展示状态字段,用于标记对象的展示状态。
     * 显示状态[0-不显示;1-显示]
     * 
     * 此字段受到两个验证组(AddGroup, UpdateStatusGroup)的约束。
     * 在这两个组中,该字段不能为空(@NotNull)且其值必须在预定义的列表中(@ListValue)。
     * 这样的设计确保了在添加对象和更新状态操作时,展示状态的值是有效且受控的。
     * 
     * @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
     * 表明在AddGroup和UpdateStatusGroup验证组中,此字段不能为空。
     * @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
     * 表明在AddGroup和UpdateStatusGroup验证组中,此字段的值必须是0或1。
     */
    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;

    /**
     * 字段firstLetter用于存储实体的首字母。
     * 该字段的验证有以下规则:
     * 1. 在添加(AddGroup)时,不能为空,确保数据完整性。
     * 2. 在添加(AddGroup)和更新(UpdateGroup)时,必须是一个字母,确保数据的格式符合预期。
     * 这些验证规则通过注解的方式进行声明,以在运行时对数据进行校验。
     */
    @NotEmpty(groups={AddGroup.class})
    @Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
    private String firstLetter;

 
    /**
     * 排序字段,用于控制元素的显示顺序。
     * 
     * @NotNull 标注指示该字段在添加(AddGroup)时不能为空,确保了排序值的有效性。
     * @Min 标注指定了排序值必须大于等于0,适用于添加(AddGroup)和更新(UpdateGroup)操作,保证了排序的逻辑正确性。
     */
    @NotNull(groups={AddGroup.class})
    @Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
    private Integer sort;
    
}

2.3 自定义约束

通过上面的代码可以看出,如果要指定注解作用的范围,就要自己添加分组。

AddGroup

public interface AddGroup {
}

UpdateGroup

public interface UpdateGroup {
}

UpdateStatusGroup

public interface UpdateStatusGroup {
}

上述代码中为了对状态取值进行验证,我们采用了自定义验证器的方式

ListValue

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {

    String message() default "{com.xunqi.common.valid.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    int[] vals() default { };
}

ListValueConstraintValidator

/**
 * 实现了ListValue注解的验证器,用于验证一个整数是否在预定义的整数列表中。
 * 该验证器在验证阶段检查给定的整数是否存在于初始化时指定的整数集合中。
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {

    /**
     * 用于存储验证器初始化时指定的整数列表的集合。
     */
    private Set<Integer> set = new HashSet<>();

    /**
     * 初始化验证器,加载注解中指定的整数列表到集合中。
     *
     * @param constraintAnnotation ListValue注解实例,其中包含需要验证的整数列表。
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] value = constraintAnnotation.vals();
        for (int val : value) {
            set.add(val);
        }
    }

    /**
     * 验证给定的整数是否在集合中。
     *
     * @param value 待验证的整数。
     * @param context 验证上下文,提供关于验证操作的上下文信息。
     * @return 如果给定的整数存在于集合中,则返回true;否则返回false。
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}

3. 业务层使用


/**
 * 

* 商品品牌 前端控制器 *

* * @author shiqi * @version 1.0.0 * @createTime 2024-06-26 */
@RestController @RequestMapping("product/brand") public class BrandController { /** * 保存品牌信息。 *

* 该方法通过@RequestMapping注解映射了"/save"的HTTP请求,用于保存BrandEntity对象。 * 使用@Validated注解对brandEntity参数进行验证,确保添加或修改品牌时数据的合法性。 * BindingResult参数用于接收验证后的错误信息,可以进一步处理和反馈给前端。 *

* 方法返回一个R对象,通常表示操作的成功或失败状态。 * * @param brandEntity 品牌实体对象,包含待保存的品牌信息。 * @param bindingResult 验证结果对象,用于存储brandEntity验证过程中产生的错误信息。 * @return 返回一个表示操作结果的对象,通常是一个包含成功状态和相关消息的R对象。 */ @RequestMapping("/save") public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brandEntity, BindingResult bindingResult) { // 方法体中应包含保存品牌信息的具体逻辑,此处省略。 return R.ok(); } }

4. 封装统一异常

4.1 业务异常状态枚举类

/**
 * 

* 描述:业务异常枚举类 *

* * @author shiqi * @version 1.0.0 * @createTime 2024-06-26 */
public enum BizCodeEnum { UNKNOWN_EXCEPTION(10000,"系统未知异常"), VALID_EXCEPTION(10001,"参数格式校验失败"), TO_MANY_REQUEST(10002,"请求流量过大,请稍后再试"), SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"), PRODUCT_UP_EXCEPTION(11000,"商品上架异常"), USER_EXIST_EXCEPTION(15001,"存在相同的用户"), PHONE_EXIST_EXCEPTION(15002,"存在相同的手机号"), NO_STOCK_EXCEPTION(21000,"商品库存不足"), LOGIN_ACCOUNT_PASSWORD_EXCEPTION(15003,"账号或密码错误"); private int code; private String message; BizCodeEnum(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } }

4.2 封装统一返回结果


/**
 * 

* 统一返回结果 *

* * @author shiqi * @version 1.0.0 * @createTime 2024-06-26 */
public class R extends HashMap<String, Object> { private static final long serialVersionUID = 1L; public R setData(Object data) { put("data",data); return this; } //利用fastjson进行反序列化 public <T> T getData(TypeReference<T> typeReference) { Object data = get("data"); //默认是map String jsonString = JSON.toJSONString(data); T t = JSON.parseObject(jsonString, typeReference); return t; } //利用fastjson进行反序列化 public <T> T getData(String key,TypeReference<T> typeReference) { Object data = get(key); //默认是map String jsonString = JSON.toJSONString(data); T t = JSON.parseObject(jsonString, typeReference); return t; } public R() { put("code", 0); put("msg", "success"); } public static R error() { return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员"); } public static R error(String msg) { return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg); } public static R error(int code, String msg) { R r = new R(); r.put("code", code); r.put("msg", msg); return r; } public static R ok(String msg) { R r = new R(); r.put("msg", msg); return r; } public static R ok(Map<String, Object> map) { R r = new R(); r.putAll(map); return r; } public static R ok() { return new R(); } public R put(String key, Object value) { super.put(key, value); return this; } public Integer getCode() { return (Integer) this.get("code"); } }

自定义校验异常处理器

/**
 * 

* 集中处理所有异常 *

* * @author shiqi * @version 1.0.0 * @createTime 2024-06-26 */
@Slf4j @RestControllerAdvice(basePackages = {"com.shiqi.jsr303demo"}) public class CustomExceptionControllerAdvice { /** * 处理方法参数不合法异常。 * 当方法参数不满足验证条件时,Spring MVC会抛出MethodArgumentNotValidException异常。 * 该异常处理器专门捕获此类异常,以统一的方式处理参数验证失败的情况。 * * @param e MethodArgumentNotValidException异常实例,包含验证失败的详细信息。 * @return 返回一个包含错误信息的响应对象。 */ @ExceptionHandler(value = MethodArgumentNotValidException.class) public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e){ // 获取验证结果对象,其中包含了具体的验证错误信息。 BindingResult bindingResult = e.getBindingResult(); // 初始化一个映射,用于存储字段名和对应的错误信息。 HashMap<String,String> errMap=new HashMap<String,String>(); // 检查是否有验证错误,如果有,则遍历所有字段错误,并将字段名和错误信息添加到errMap中。 if (bindingResult.hasErrors()){ bindingResult.getFieldErrors().forEach((item)->{ errMap.put(item.getField(),item.getDefaultMessage()); }); } // 返回一个包含错误代码、错误消息和具体错误详情的响应对象。 // 错误代码为400,表示客户端请求错误,错误消息为"参数校验不合法"。 // errMap作为数据部分的一部分,包含了所有验证失败的字段和对应的错误信息。 return R.error(400,"参数校验不合法").put("data",errMap); } /** * 处理所有异常的控制器异常处理器。 *

* 该方法旨在捕获控制器层抛出的任何异常,无论是预期的业务异常还是未预期的运行时异常。 * 它的目的是统一异常的处理方式,向客户端返回一个标准的响应体,而不是直接暴露服务器内部错误信息。 * * @param throwable 抛出的异常对象,无论异常类型为何。 * @return 返回一个表示错误响应的R对象。这个响应体可以帮助客户端识别请求处理过程中发生了什么错误。 */ @ExceptionHandler(value = Throwable.class) private R handleValidException(Throwable throwable) { // 记录异常信息到日志系统,以便后续的问题排查和分析。 log.error("出现异常{},异常类型{}", throwable.getMessage(), throwable.getClass()); // 返回一个通用的错误响应体,通知客户端请求处理过程中发生了错误。 return R.error(); } }

5. 实际业务中的应用

  1. 表单校验:确保用户输入的数据合法,如用户注册、登录、表单提交等。
  2. 数据传输对象(DTO)校验:在进行数据传输时,确保传输的数据符合预期,如API请求和响应。
  3. 领域对象校验:确保业务逻辑中的对象状态合法,如订单处理、支付处理等。

使用JSR 303可以有效减少手动校验代码,简化代码结构,提高代码可读性和维护性。在实际应用中,常结合Spring框架和Hibernate Validator一起使用

6. 扩展知识

6.1 PO(Persistence Object)

定义:持久化对象,通常对应数据库中的一张表,每个实例对应表中的一行。
应用场景:用于数据持久化层,与数据库的表结构一一对应,通过ORM(如Hibernate)进行数据操作。
使用时机:当需要将数据持久化到数据库,或者从数据库中读取数据时使用。
示例:

public class UserPO {
    private Long id;
    private String username;
    private String password;
    // Getters and Setters
}

具体使用场景

  • 在需要进行数据库操作(CRUD)时使用。
  • 与DAO一起使用,以实现数据持久化逻辑。

简单来说就是Java Bean 对应的是数据库中的一张表

6.2. BO(Business Object)

定义:业务对象,封装业务逻辑的Java对象,通常包含业务操作方法。
应用场景:在业务层使用,用于处理业务逻辑,可能会调用多个DAO或与多个PO交互。
使用时机:当需要处理复杂的业务逻辑或操作多个数据对象时使用。
示例:

public class UserBO {
    private Long id;
    private String username;
    private String password;

    public void changePassword(String newPassword) {
        // 业务逻辑:更新密码
        this.password = newPassword;
    }
    // Getters and Setters
}

具体使用场景

  • 在业务层需要进行复杂业务逻辑处理时。
  • 需要将多个数据对象(PO)进行组合或处理时。

6.3. VO(Value Object)

定义:值对象,用于在应用层传递数据,通常是只读的。
应用场景:在视图层(如MVC中的View)使用,用于展示数据,不包含业务逻辑。
使用时机:当需要在视图层展示数据,而不需要修改数据时使用。
示例:

public class UserVO {
    private Long id;
    private String username;
    // 只包含展示所需的字段
    // Getters and Setters
}

具体使用场景

  • 在需要将数据传递给前端进行展示时。
  • 在只读数据场景下使用,如显示用户信息。

简单来说就是对PO进行阉割或者强化,比如我们需要查询用户列表的时候,需要对敏感信息进行加密,不太关心的数据我们也不必返回给前端,造成带宽的浪费。

6.4 DTO(Data Transfer Object)

定义:数据传输对象,用于在不同层之间传输数据,通常是无状态的。
应用场景:在数据传输层使用,如在服务接口之间传递数据,减少远程调用次数。
使用时机:当需要在不同系统或不同层之间传递数据时使用。
示例:

public class UserDTO {
    private Long id;
    private String username;
    private String email;
    // Getters and Setters
}

具体使用场景

  • 在服务之间的远程调用中传输数据。
  • 在Controller与Service之间传递数据。

这个怎么理解了,就是我们进行表单提交的时候,或者更新查询操作时,需要前端传递很多参数,如果每个参数都写在Controller层的参数列表中就很不优雅,而且如果需新增参数的话就需要改动很多地方,会导致容易遗漏,这个时候我们就把这些请求参数封装成一个请求对象,这就是DTO。

6.5. POJO(Plain Old Java Object)

定义:简单的Java对象,不依赖任何特定框架或库。
应用场景:用于定义普通的Java对象,通常作为PO、BO、VO、DTO的基础。
使用时机:在需要创建简单的数据容器而不依赖任何特定框架时使用。
示例:

public class User {
    private Long id;
    private String username;
    private String password;
    // Getters and Setters
}

具体使用场景

  • 创建简单的Java对象用于数据封装。
  • 作为其他对象(PO、BO、VO、DTO)的基础类。

也就是我们自己编写的业务类,无需与数据库打交道的。上述的统一返回结果类,我们就可以理解为POJO

6.6. DAO(Data Access Object)

定义:数据访问对象,提供对数据库的抽象和封装,包含CRUD操作。
应用场景:在数据访问层使用,负责与数据库进行交互。
使用时机:当需要对数据库进行操作(如查询、插入、更新、删除)时使用。
示例:

    public void save(UserPO user) {
        // 保存用户到数据库
    }

    public UserPO findById(Long id) {
        // 根据ID查询用户
        return new UserPO();
    }
    // 其他CRUD操作
}

具体使用场景

  • 在需要与数据库进行直接交互时。
  • 在业务逻辑中需要持久化或检索数据时。

你可能感兴趣的:(java,python,开发语言)