java后台数据校验 JSR303规范

JSR303

是一种用于检查数据完整性的标准,javax.validation.constraints提供了一部分实现

使用方法:

  1. 在 bean 中添加约束注解,并添加message
  2. 使用 bean 的地方使用@Valid注解开启校验
  3. 在要校验的目标参数后紧跟一个BindingResult 就可以获得校验的结果,其位于org.springframework.validation.BindingResult
@Data
public class Brand {

	private Long brandId;

	@NotBlank(message = "品牌名不能为空") // 用于校验的注解
	private String name;
}

 

    @PostMapping
    public Result save(@Valid @RequestBody Brand brand, BindingResult result){
        // 通过BindingResult,获取校验结果
        if (result.hasErrors()) {
            Map<String, String> map = new HashMap<>();
            result.getFieldErrors().forEach((fieldError) -> {
                String field = fieldError.getField();
                String message = fieldError.getDefaultMessage();
                map.put(field, message);
            });
            Result<Map> error = new Result<Map>().error(400, "提交数据不合法");
            error.setData(map);
            return error;
        }
        
        brandService.save(brand);
        return new Result();
    }

 

统一异常处理

使用 @ControllerAdvice,在 controller 中出现数据校验错误时不在自行处理,而是抛出异常,由异常处理类统一处理。在controller中不接收 BindingResult 异常就会抛出。

@Slf4j
@RestControllerAdvice(basePackages = "org.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    // 指处理方法参数错误的特定异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result<Map> handleVaildException(MethodArgumentNotValidException e) {
        log.error("数据校验错误 {} {}", e.getMessage(), e.getClass());

        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError) -> {
            map.put(fieldError.getField(), fieldError.getDefaultMessage());
        });

        Result<Map> error = new Result<Map>()
                // 将状态码与错误消息封装到一个枚举类中
                .error(BizCodeEnum.VAILD_EXCEPTION.getCode(), BizCodeEnum.VAILD_EXCEPTION.getMsg());
        error.setData(map);
        return error;
    }

    // 处理所有异常
    @ExceptionHandler
    public Result handleException(Throwable throwable) {
        Result<Object> result = new Result<>()
                .error(BizCodeEnum.UNKNOW_EXCEPTION.getCode(), BizCodeEnum.UNKNOW_EXCEPTION.getMsg());
        result.setData(throwable);
        return result;
    }
}

 分组校验

 当新增一个对象时自动生成id,故无需携带id;修改对象时必须提交对象的id。但是校验条件注解都是在实体类中定义。

解决方法:注解有一个 groups 属性。通过这个属性指定不同组,从而实现在不同条件下使用不同的约束。
                然后再在数据校验处指定分组

public @interface NotNull {
    String message() default "{javax.validation.constraints.NotNull.message}"; // 这条消息默认从 validation.properties 文件中获取,可以在自己的项目中自定义

    Class<?>[] groups() default {}; // 通过实现的接口进行分组,接口不需要有方法

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

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        NotNull[] value();
    }
}

 1. 创建用于区分场景的接口

 

// 空接口即可,无需实现
package org.gulimall.common.valid;

public interface AddGroup {
}


package org.gulimall.common.valid;

public interface UpdateGroup {
}

 2. 在实体中注明分组

通过分组能够实现不同的策略。注意:在使用分组的情况下,不指定分组的属性校验注解默认不起作用。

@Data
public class BrandDTO {

	@NotNull(message = "修改时id不能为空", groups = {UpdateGroup.class})
	@Null(message = "新增时id必须为空", groups = {AddGroup.class})
	private Long brandId;
}

3.  在数据校验处指定分组

通过@Validated指定分组。注意:在使用分组的情况下,不指定分组的属性校验注解默认不起作用。

@RestController
@RequestMapping("product/brand")
@Api(tags="品牌")
public class BrandController {
    @Autowired
    private BrandService brandService;
    
    @PostMapping
    public Result save(@Validated({AddGroup.class}) @RequestBody Brand brand){
        brandService.save(brand);
        return new Result();
    }
    
    @PutMapping
    public Result update(@Validated({UpdateGroup.class}) @RequestBody Brand brand){
        brandService.update(dto);
        return new Result();
    }
    
    
}

 自定义校验

  1.  编写一个自定义的注解
  2. 编写一个自定义的校验器
  3. 二者关联

 1. 参考 @NotNULL 写出一个自定义校验器

package org.gulimall.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = { StatusValueConstraintValidator.class }) // 指定具体的校验类,将二者关联。可以指定多个校验器,适配不同属性
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//@Repeatable(List.class)
public @interface StatusValue {

    String message() default "{org.gulimall.common.valid.StatusValue.message}";

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

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

    int[] vals() default {};
}

 其中校验类validatedBy要求是指定类型子类的数组

@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {

	/**
	 * {@link ConstraintValidator} classes implementing the constraint. The given classes
	 * must reference distinct target types for a given {@link ValidationTarget}. If two
	 * {@code ConstraintValidator}s refer to the same type, an exception will occur.
	 * <p>
	 * At most one {@code ConstraintValidator} targeting the array of parameters of
	 * methods or constructors (aka cross-parameter) is accepted. If two or more
	 * are present, an exception will occur.
	 *
	 * @return array of {@code ConstraintValidator} classes implementing the constraint
	 */
	Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}

2. 编写一个自定义的校验器,要实现ConstraintValidator<注解类型, 修饰的属性类型>

 这里写的校验器只能处理int类型数据,如果要处理其他类型,新建一个校验器再添加到修饰注解的数组中

package org.gulimall.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
 * 要求实现 ConstraintValidator 接口,要有两个泛型参数,一个是注解类型,另一个是注解修饰的类型
 */
public class StatusValueConstraintValidator implements ConstraintValidator<StatusValue, Integer> {

    private Set<Integer> set = new HashSet<>();
    /**
     * 初始化方法,可以获得初始化信息
     * @param constraintAnnotation annotation instance for a given constraint declaration
     */
    @Override
    public void initialize(StatusValue constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        for (int val : constraintAnnotation.vals()) {
            set.add(val);
        }
    }
    /**
     *
     * @param value object to validate
     * @param context context in which the constraint is evaluated
     *
     * @return 是否通过校验
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}

3. 在注解的注解中将二者关联

使用:

@Data
public class Brand {
    
//	@Max(value = 1, message = "显示状态只能为0或1", groups = {UpdateGroup.class, AddGroup.class})
//	@Min(value = 0, message = "显示状态只能为0或1", groups = {UpdateGroup.class, AddGroup.class})
	@StatusValue(vals={0, 1}, groups = {UpdateGroup.class, AddGroup.class}) // 自定义注解,属性值只能为指定的 0 或 1
	@ApiModelProperty(value = "显示状态[0-不显示;1-显示]")
	private Integer showStatus;   
}

 

posted @   某某人8265  阅读(283)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示