最近看了b站up主codesheep的注解文章,自己对注解的理解很浅薄,就自己找资料研究了注解。
(codesheep关于注解使用的文章地址)https://www.bilibili.com/read/cv4802402
我先是体验了一下springboot自带的hibernate-validator的校验功能。
springboot依赖中自带了hibernate-validator,所以不用导入依赖
hibernate-validator的校验功能的总结如下
- 在要检验的实体类的字段上加入hibernate-validator提供的注解,如@NotNull、@Max、@Min
- 在controller的RequestMapping修饰的方式参数前加上@Valid注解
- 如果嫌弃返回的参数绑定失败信息不好看,就自定义一个全局异常拦截器
以上步骤请参考我的这篇博客 https://blog.csdn.net/zm9898/article/details/104742097
体验了使用hibernate-validator注解后,我就想自己写一个注解,并且实现它的功能,我的需求是这样的:定义一个注解,当它加入到某个string类型的字段上后,这个字段在以后验证时就必须包含注解中定义的字符串。如下图,注解中填写了“襄阳”,那么address字段值在验证时就必须要包含“襄阳”。
在正式开始写代码前,先说说我学习注解后的一点心得体会:注解就仅仅是在程序代码中加了个一个标记而已,有了这个标记,我们就可以在代码中使用java的反射技术来赋予这个注解特定的功能。几个重要的java Api类如:Method、Field、Paramter都实现了AnnotatedElement接口,这保证了我们可以使用反射技术,从这些类中获得到包含“注解”的东西,然后对这些东西进行操作。
AnnotatedElement接口继承关系图
AnnotatedElement里面的方法图
其中这些方法的含义是
T getAnnotation(Class annotationType):得到指定类型的注解引用。没有返回null。
Annotation[] getAnnotations():得到自己身上所有的注解,包含从父类继承下来的。
Annotation[] getDeclaredAnnotations():得到自己身上的注解。
boolean isAnnotationPresent(Class<? extends Annotation> annotationType):判断自己身上有没有指定的注解。
下面开始实现最开始的注解需求
首先看看我的工程目录,作到心中有数,项目是使用springboot,工程中有最终实现完成的注解、有使用注解的实体类student、有实现访问的controller、有自定义的异常、有全局异常拦截器、有实现注解校验功能的工具类
以下是项目pom依赖,lombok是一种可以简化get、set方法的东西,并非项目中必需的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
我定义的注解AddressAnnotation ,含义已经在上文和代码注释中说明。
/**
* @Author 张满
* @Description 自定义地址注解,加了本注解的字段就必须包含mustContainStr字符串
* @Date 2020/3/8 21:12
* @Param
* @return
**/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AddressAnnotation {
//必须包含的字符串
String mustContainStr();
}
使用AddressAnnotation注解的实体类student(重点看address属性!)
@Data
public class Student {
@Min(value = 20,message = "最小值为20")
@Max(value = 100,message = "最大值为100")
@NotNull(message = "年龄不能为空")
private Integer age;
@NotBlank(message = "名字不能为空")
private String name;
@Length(max = 3,min = 3,message = "号码必须要3位")
private String number;
//加上地址注解,则必须包含相应的值
@AddressAnnotation(mustContainStr = "襄阳")
@NotBlank(message = "地址不能为空")
private String address;
}
当前端请求controller中的方法时,如下:先使用@Valid通过hibernate-vlidator的校验,然后再使用MyAnnotationValidateUtil校验工具类校验我们自己的注解
@RestController
public class StudentController {
@GetMapping("/addStudent")
public Object addStudent(@Valid Student student) throws IllegalAccessException {
MyAnnotationValidateUtil.validateAddressAnnotation(student);
return "success";
}
}
MyAnnotationValidateUtil代码如下,它也是我们注解开发的核心,由它来完成注解的功能。
/**
* @Author 张满
* @Description 自定义注解验证工具类
* @Date 2020/3/8 23:21
* @vsersion 1.0.0
**/
public class MyAnnotationValidateUtil {
/**
* @Author 张满
* @Description 验证AddressAnnotation注解
* @Date 2020/3/8 22:41
* @Param [student]
* @return java.lang.String
**/
public static void validateAddressAnnotation(Object object) throws IllegalAccessException{
Class<?> clazz = object.getClass();
//获得对象的所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//设置让私有字段也可以访问
field.setAccessible(true);
//判断字段是否实现了AddressAnnotation注解
boolean flag = field.isAnnotationPresent(AddressAnnotation.class);
//如果实现了AddressAnnotation注解
if(flag){
//判断这个字段是不是String类型的,如果这个字段是
if(field.getType().getName().equals("java.lang.String")){
//如果是String类型的,就检查字段值是否包含注解中的mustContainStr值(字段中是否包含襄阳?)
AddressAnnotation addressAnnotation = field.getAnnotation(AddressAnnotation.class);
//注解中要求包含的值
String mustContainStr = addressAnnotation.mustContainStr();
//传来的字段值
String fieldStr = (String) field.get(object);
//判断是否字段值是否包含注解要求的值
if(fieldStr.contains(mustContainStr)){
continue;
}else{
//不包含抛出异常
throw new NotContainsStrException(field.getName()+"中必须包含"+mustContainStr);
}
}
}
}
}
}
以上代码,我们先获得传入对象的所有字段,遍历出所有添加AddressAnnotation注解的字段,然后判断这个字段是不是String类型的,如果加注解的字段不是String类型的,则这个注解对这个字段不起任何作用。反之,如果这个字段是String类型的,我们就判断传来的字段值是否包含注解中要求的值,如果包含继续判断,如果不包含,抛出自定义异常类NotContainsStrException
自定义异常类NotContainsStrException的代码
/**
* @Author 张满
* @Description 不包含相应的字符串异常
* @Date 2020/3/9 0:05
* @vsersion 1.0.0
**/
public class NotContainsStrException extends RuntimeException{
public NotContainsStrException(String message) {
super(message);
}
}
当抛出自定义异常类时,我们的全局异常拦截器就会拦截,以下是全局异常拦截器类(主要看else if中的代码)
/**
* @Author 张满
* @Description 全局异常拦截器
* @Date 2020/3/8 20:19
* @vsersion 1.0.0
**/
@ControllerAdvice
public class GlobalExceptionInterceptor {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object exceptionHander(HttpServletRequest request,Exception e){
List returnlist = new ArrayList();
//返回hibernate-valitor的注解验证异常结果
if(e instanceof BindException){
Iterator<FieldError> iterator = ((BindException) e).getBindingResult().getFieldErrors().iterator();
while (iterator.hasNext()){
FieldError fieldError = iterator.next();
String errorMsg = fieldError.getDefaultMessage();
returnlist.add(errorMsg);
}
return returnlist;
}
else if(e instanceof NotContainsStrException){
//如果出现 NotContainsStrException 异常,返回相应信息
//返回自定义地址注解的验证异常结果
return e.getMessage();
}else {
//出现无法预料的异常,就在控制台打印异常信息,并返回给请求者信息(生产环境下应关闭)
e.printStackTrace();
return e.getMessage();
}
}
}
使用postman进行测试
先发送address中不包含“襄阳”的值,测试成功。
再发送address中包含“襄阳”的值,测试成功
如果改变注解中要求的值(mustContainStr),那么对postman传来值的要求也改变了。
结果:自定义注解并成功实现其功能!