java自定义注解原理+实战

141 阅读7分钟

大家好,我是公众号:【java小杰要加油】,最近在项目中,发现了很多地方都用到了自定义注解, 根据自定义的注解,再去做一些个性化的操作,非常方便,今天来分享给大家

  • 话不多说,直接开车

注解大致介绍

首先,让我们来声明一个注解


// 注解可以作用在哪里
@Target({ElementType.TYPE})
//  该注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
// 指示默认情况下,带有类型的注释将由javadoc *和类似工具来记录
@Documented
// 可以继承父类注解
@Inherited   
// bean
@Component
public @interface DIYClassAnnotation {
    DIYEnum diyEnum();
    // 年龄默认24 岁
    int age() default 24;

}

可以注意到,我们声明的这个注解,他自己又带着很多元注解,我们依此来解释下,对应可取的值也如下

  • @Target : 指此注解可以标注在哪些地方,是字段?还是类?还是方法?
    • TYPE :类,接口(包括注释类型)或枚举声明
    • FIELD:字段声明(包括枚举常量)
    • METHOD:方法声明
    • PARAMETER:形式参数声明
    • CONSTRUCTOR:构造函数声明
    • LOCAL_VARIABLE:局部变量声明
    • ANNOTATION_TYPE:注释类型声明
    • PACKAGE:包声明
    • TYPE_PARAMETER:类型参数声明
    • TYPE_USE:使用类型
  • @Retention :指该注解的生命周期,存活在哪个阶段
    • SOURCE:批注将被编译器丢弃
    • CLASS:注释将由编译器记录在类文件中,但不必在运行时由VM保留。这是默认的行为。
    • RUNTIME:注释将由编译器记录在类文件中,并在运行时由VM保存*,因此可以通过反射方式读取它们
  • @Documented :指是默认情况下,带有类型的注释将由javadoc *和类似工具来记录
  • @Inherited :可以继承父类注解
  • 里面的值只能用基本类型boolean、int、double、float、long、byte、short、charString、Enum、Class以及一些其他注解

我们一般写注解的时候。就是用图中上面那几个加粗颜色的属性和值

实战演练

  • 其实使用这个自定义注解,千言万语就一句话
  1. 声明一个自定义的注解
  2. 通过反射等方式取出这个注解,再根据这个注解中自己设定的值去做一些定制化的操作
  • 本文将演示三种类型的自定义注解怎么用,平常开发也就这三种了(我接触的)

一、自定义类注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Component
public @interface DIYClassAnnotation {
    // 自定义枚举类型
    DIYEnum diyEnum();
    // 年龄默认24 岁
    int age() default 24;
}

看一下这个枚举类型


public enum DIYEnum {
    xiaoJie("小杰","打代码"),
    TEACHER("老师","教书"),
    CHEF("厨师","做饭");

    private String name;
    private String worker;

    DIYEnum(String name,String worker) {
        this.name = name;
        this.worker = worker;
    }

    public String getWorker() {
        return worker;
    }

    public void setWorker(String worker) {
        this.worker = worker;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


}

二、自定义字段注解


// 注解到什么地方  属性 上
@Target({ElementType.FIELD})
//  该注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
// 指示默认情况下,带有类型的注释将由javadoc *和类似工具来记录
@Documented
// 可以继承父类注解
@Inherited
// bean
@Component
public @interface DIYFieldAnnotation {
    // 性别
    String sex();

}

三、自定义方法注解


// 注解到什么地方  方法 上
@Target({ElementType.METHOD})
//  该注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
// 指示默认情况下,带有类型的注释将由javadoc *和类似工具来记录
@Documented
// 可以继承父类注解
@Inherited
// bean
@Component
public @interface DIYMethodAnnotation {
    // 是否校验
    int verification();

    // 接口名称
    String interfaceName();
}

  • 其实我们注意到,感觉大家都大差不差啊,是的没错,也就是target作用域不一样罢了

我们自定义注解定义完了,下面要开始真正使用啦

  • 定义个抽象父类Person
// 抽象父类
public abstract class Person {
  public  abstract void hobby();
}

  • 定义学生Student子类
@DIYClassAnnotation(diyEnum = DIYEnum.xiaoJie,age=23 )
public class Student extends Person {

    @DIYFieldAnnotation(sex = "男")
    private String sex;

    @Override
    public void hobby() {
        System.out.println(DIYEnum.xiaoJie.getWorker());
    }
}
  • 定义老师Teacher子类
@DIYClassAnnotation(diyEnum = DIYEnum.TEACHER,age=46 )
public class Teacher extends Person {

    @DIYFieldAnnotation(sex = "女")
    private String sex;

    @Override
    public void hobby() {
        System.out.println(DIYEnum.TEACHER.getWorker());
    }
}
  • 定义厨师Chef子类
@DIYClassAnnotation(diyEnum = DIYEnum.CHEF,age=50 )
public class Chef extends Person {

    @DIYFieldAnnotation(sex = "男")
    private String sex;


    @Override
    public void hobby() {
        System.out.println(DIYEnum.CHEF.getWorker());
    }
}

再来一个注解工具类



public class DIYAnnotationUtils {
    public static Person getPerson(Person ...persons){

        for (Person person:persons) {
            // 判断这个类是否有这个注解
            if (person.getClass().isAnnotationPresent(DIYClassAnnotation.class)){
                // 得到这个自定义的注解
                DIYClassAnnotation workerAnnotation = person.getClass().getAnnotation(DIYClassAnnotation.class);
                // 判断这个自定义注解注解的值是否是我们想要的
                if (DIYEnum.xiaoJie.getName().equals(workerAnnotation.diyEnum().getName())){
                    // 反射得到这个对象的属性
                    Field[] fields = person.getClass().getDeclaredFields();
                    for (Field field:fields) {
                        // 如果这个字段有这个注解
                        if (field.isAnnotationPresent(DIYFieldAnnotation.class)){
                            // 打印出这个属性上有这个注解的值
                            DIYFieldAnnotation annotation = field.getAnnotation(DIYFieldAnnotation.class);
                            System.out.println(annotation.sex());
                        }
                    }
                    return person;
                }
            }
        }
        return null;
    }
}

最主要的就是这个工具类(用到反射),其中根据传进来的对象判断符合不符合我们的要求 (注解时的名字是不是小杰),如果符合的话,把注解在属性上的注解拿出来

  • 我们通过测视类来调用一下

public class Test {
    public static void main(String[] args) {
        Student student =new Student();
        Chef chef = new Chef() ;
        Teacher teacher = new Teacher();
        Person person = DIYAnnotationUtils.getPerson(student, chef, teacher);
        if (person != null){
            person.hobby();
        }
    }
}

输出结果是

男
打代码
  • 下面我们来演示下怎么在方法上使用这注解,最常见的组合就是自定义注解+AOP

下面来看下controller


@RestController
public class Controller {
     // 此方法需要校验
     @DIYMethodAnnotation(verification = 1,interfaceName = "学生爱好接口")
     @RequestMapping("/verification")
     public   String   verificationMethod(String id){
         new Student().hobby();
         return "校验";
    }

    // 此方法不需要校验
    @DIYMethodAnnotation(verification = 0,interfaceName = "老师爱好接口")
    @RequestMapping("/noVerification")
    public   String   noVerificationMethod(String id){
        new Teacher().hobby();

        return "不校验";
    }

    // 此方法没有注解
    @RequestMapping("/noAnnotation")
    public   String   noAnnotationMethod(String id){
        new Chef().hobby();

        return "无注解";
    }

}

再看下切面类 本文注重讲解注解,这个切面类还有很多完善的地方不过不在本文范围内


@Component
@Aspect
public class LogAspect {
    // 注解的位置
    @Pointcut("@annotation(com.example.demo.annotation.DIYMethodAnnotation)")
    public void diyPointCut(){};

    @Around("diyPointCut()")
    public Object diyAround(ProceedingJoinPoint joinPoint){
        //获得被增强的方法相关信息
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        // 获得这个方法
        Method method = signature.getMethod();
        // 获得这个方法上面的注解
        DIYMethodAnnotation diyMethodAnnotation = method.getAnnotation(DIYMethodAnnotation.class);
        // 根据注解自定义的一些属性去做自定义的操作
        if (diyMethodAnnotation.verification() == 1){
            System.out.println("当前校验的是:"+diyMethodAnnotation.interfaceName());
            System.out.println("方法名称是:"+method.getName());
            System.out.println("传递参数是:"+JSON.toJSONString(joinPoint.getArgs()));
        }

        System.out.println("aop 拦截器里 verification:"+diyMethodAnnotation.verification() );


        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
    }
}

我们将项目跑起来,分别访问两个打了注解的接口

  • 需要校验的接口
http://localhost:8081/verification?id=1

当前校验的是:学生爱好接口
方法名称是:verificationMethod
传递参数是:["1"]
aop 拦截器里 verification:1
打代码

  • 不需要校验的接口
http://localhost:8081/noVerification?id=1

aop 拦截器里 verification:0
做饭
  • 没有注解的接口
http://localhost:8081/noAnnotation?id=1

做饭

由输出结果可以得出一个结论,

  • 没有注解的接口,走不到AOP,因为我们AOP配置的是只有注解的接口才进行AOP校验,
  • 如果接口上有注解的话,又有两种情况(这是我们自己设置的)
    • verification 0 的时候 这个注解也不进行特别的操作
      • 输出 ”当前校验的是,方法名称是,传递参数是
    • verification 1 的时候 这个注解进行特别的操作

综上所述,我们在日常开发中,如果对某个类/字段/方法有什么特殊的要求的话可以使用自定义注解,再通过反射获取到此注解,再根据这个注解中自定义的值在进行我们自定义的操作

好文推荐

最后

再贴一张最近火爆全网同时也对我触动很大的话

  • 自助者,天助之

欢迎大家关注,我是【java小杰要加油】,有什么想说的想看的欢迎评论区里留言,我们下期见。