SpringBoot实战 - 自定义注解实现枚举值校验
前言
在 Spring 项目中,校验参数功能通常使用 hibernate validator 或者 javax.validation,我们的项目中也是使用它们来进行校验的,省去了很多难看的校验逻辑,使代码的可读性也大大增加,本章将带你使用自定义注解功能实现一个枚举值校验的逻辑。
需求
我们先明确下我们的需求,在程序开发过程中,我们经常会有一个对象的属性值只能出现在一组常量中的校验需求,例如:用户性别字段 gender 只能等于 MALE/FEMALE 这两个其中一个值,用户账号的状态 status 只能等于:NORMAL/DISABLED/DELETED 其中一个等等,那么我们怎么能更好的校验这个参数呢?我们想拥有一个 java 注解,把它标记在所要校验的字段上,当开启 hibernate validator 校验时,就可以校验其字段值是否正确。
实现方案
上面提到的一组常量值,我们第一反应应该是定义一个枚举类,尽量不要放在一个统一的 constants 类下,这样当系统一旦庞大起来,常量是很难维护和查找的,所以前期代码也应该有一些规范性约束,这里我们约定一组常量值时使用枚举,并把该枚举类放在对应的类对象里(以上述所说的用户功能为例,我们应该把 GenerEnum、UserStatusEnum枚举放在User.java下,方便查找)
这里我们定义一个叫 EnumValue.java 的注解类,其下有两个主要参数一个是 enumClass 用于指定枚举类,enumMethod 指定要校验的方法,下面我们看代码实现。
自定义注解
package cn.notemi.demo.annotations;
import cn.notemi.demo.util.StringUtil;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @desc 校验枚举值有效性
*/
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValue.Validator.class)
public @interface EnumValue {
String message() default "无效的值";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
String enumMethod();
class Validator implements ConstraintValidator<EnumValue, Object> {
private Class<? extends Enum<?>> enumClass;
private String enumMethod;
@Override
public void initialize(EnumValue enumValue) {
enumMethod = enumValue.enumMethod();
enumClass = enumValue.enumClass();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) {
return Boolean.TRUE;
}
if (enumClass == null || enumMethod == null) {
return Boolean.TRUE;
}
Class<?> valueClass = value.getClass();
try {
Method method = enumClass.getMethod(enumMethod, valueClass);
if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
throw new RuntimeException(StringUtil.formatIfArgs("%s method return is not boolean type in the %s class", enumMethod, enumClass));
}
Boolean result = (Boolean)method.invoke(null, value);
return result == null ? false : result;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException | SecurityException e) {
throw new RuntimeException(StringUtil.formatIfArgs("This %s(%s) method does not exist in the %s", enumMethod, valueClass, enumClass), e);
}
}
}
}
备注
自定义注解需要实现 ConstraintValidator 校验类,这里我们定义一个叫 Validator 的类来实现它,同时实现它下面的两个方法 initialize、isValid,一个是初始化参数的方法,另一个就是校验逻辑的方法,本例子中我们将校验类定义在该注解内,用 @Constraint(validatedBy = EnumValue.Validator.class) 注解指定校验类,内部逻辑实现比较简单就是使用了静态类反射调用验证方法的方式。
对于被校验的方法我们要求,它必须是返回值类型为 Boolean 或 boolean ,并且必须是一个静态的方法,返回返回值为 null 时我们认为是校验不通过的,按 false 逻辑走。
校验的目标对象类
package cn.notemi.demo.model.po;
import cn.notemi.demo.annotations.EnumValue;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
/**
* @desc 用户PO
*/
@ApiModel("用户PO")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Table
@Entity
public class User extends BasePO<String> {
private static final long serialVersionUID = -7491215402569546437L;
@ApiModelProperty(value = "用户主键")
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY,generator = "SELECT REPLACE(UUID(),'-','')")
@Length(min=1, max=64)
private String id;
@ApiModelProperty(value = "昵称")
@NotBlank
@Length(min=1, max=64)
private String nickname;
@ApiModelProperty(value = "性别")
@NotBlank
@EnumValue(enumClass=UserGenderEnum.class, enumMethod="isValidName")
private String gender;
@ApiModelProperty(value = "头像")
@Length(max=256)
private String avatar;
@ApiModelProperty(value = "状态")
@NotBlank
@EnumValue(enumClass=UserTypeEnum.class, enumMethod="isValidName")
private String type;
@ApiModelProperty(value = "账号状态")
@EnumValue(enumClass=UserStatusEnum.class, enumMethod="isValidName")
private String status;
/**
* 用户性别枚举
*/
public enum UserGenderEnum {
/**男*/
MALE,
/**女*/
FEMALE,
/**未知*/
UNKNOWN;
public static boolean isValidName(String name) {
for (UserGenderEnum userGenderEnum : UserGenderEnum.values()) {
if (userGenderEnum.name().equals(name)) {
return true;
}
}
return false;
}
}
/**
* 用户类型枚举
*/
public enum UserTypeEnum {
/**普通*/
NORMAL,
/**管理员*/
ADMIN;
public static boolean isValidName(String name) {
for (UserTypeEnum userTypeEnum : UserTypeEnum.values()) {
if (userTypeEnum.name().equals(name)) {
return true;
}
}
return false;
}
}
/**
* 用户状态枚举
*/
public enum UserStatusEnum {
/**启用*/
ENABLED,
/**禁用*/
DISABLED;
public static boolean isValidName(String name) {
for (UserStatusEnum userStatusEnum : UserStatusEnum.values()) {
if (userStatusEnum.name().equals(name)) {
return true;
}
}
return false;
}
}
}
Controller
package cn.notemi.demo.controller;
import cn.notemi.demo.annotations.LoginAuth;
import cn.notemi.demo.annotations.ResponseResult;
import cn.notemi.demo.model.bo.LoginUser;
import cn.notemi.demo.model.po.User;
import cn.notemi.demo.model.qo.LoginQO;
import cn.notemi.demo.model.vo.LoginVO;
import cn.notemi.demo.repository.UserRepository;
import cn.notemi.demo.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* Title:UserController
**/
@RestController
@RequestMapping("/api/user")
@ResponseResult
public class UserController {
@Autowired
private UserRepository userRepository;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User addUser(@Valid @RequestBody User user) {
user.setId(UUID.randomUUID().toString());
user.setCreateTime(new Date());
return userRepository.save(user);
}
}