简单易懂讲注解

简单易懂讲注解

注解是什么

简单的说,注解就是一种将元数据信息从 xml 剥离开来,然后保存在 java 源代码中,这将使得代码更加清晰易懂,无需维护两个地方: java 源代码以及 xml 配置文件。

典型的场景就是 spring 框架,我们都知道,spring 框架将一个 bean 保存在容器里有两种方式,一种是采用配置文件的方式生成 bean 并且保存在容器中,使用的时候通过 bean 工厂拿对应的 bean 实例即可。这种方式很繁琐,不仅需要维护 java 源代码,还需要在 xml 配置里再维护一遍。另一种方式是采用注解的方式,在类名上使用 @Component或者@Service(当然还有其他方式,但不是本篇文章的重点)。然后在使用的时候采用 @Autowired 形式注入即可。这样就无需繁琐的 xml 配置。(例子)

当然,采用传统 xml 维护元素据还是使用注解,各有优劣,需要根据实际场景进行评估。

如何使用注解

接下来我们先从一个简单的注解定义开始,然后介绍一些注解的关键属性

定义注解

如下例子,Test 注解看起来很像接口的定义,注解和其他接口和类一样,都会被编译成 class 文件。像这种不含任何元素的被称为标记注解,如 java 8 新加入的用于声明一个接口时函数式接口的注解:@FunctionalInterface。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    
}

当然,注解也是可以定义一些属性的。如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    int value();
    String name() default "-- default --";
}

其中 value() 以及 name() 就是该注解的属性,其中 value() 没有默认值,那么在使用该注解的时候,必须指定 value 属性,name 有个默认值,使用的时候可以不需要指定默认值。

如下例子,就是该注解的使用方式,在 1 处,由于没有指定 value 属性,所以编译失败。

public class AnnotationDemo {

    @Test(1)
    private int value;
    
    @Test() // 1 编译失败
    private int withoutValue;

    @Test(2)
    private String withoutName;

    @Test(value = 3, name = "name")
    private String name;
}

这个注解现在来说是没有一丝丝意义的,因为我们还没有为其编写注释处理器,注释处理器在后面会介绍。

@Test 注解中使用到的 @Target 注解、@Retention 注解以及他们的参数枚举,会在下文的元注解中介绍。

常见注解

常见的注解这里主要介绍 jdk 的注解

  • @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
  • @Deprecated:表示当前类 or 方法 or 字段被弃用了,不应该再使用了,使用会产生告警
  • @SuppressWarnings:关闭不当的编译器警告信息。
  • @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口

元注解

上文的 @Test 注解中,我们使用到了 @Target 注解、@Retention 注解,这两个注解为元注解,

目前一共有 5 个元注解:

注解 解释
@Target 表示注解可以用于哪些地方。可能的 ElementType 参数包括:CONSTRUCTOR:构造器的声明 FIELD:字段声明(包括 enum 实例) LOCAL_VARIABLE:局部变量声明 METHOD:方法声明 PACKAGE:包声明 PARAMETER:参数声明 TYPE:类、接口(包括注解类型)或者 enum 声明
@Retention 表示注解信息保存的时长。可选的 RetentionPolicy 参数包括: SOURCE:注解将被编译器丢弃 CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。 RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documented 将此注解保存在 Javadoc 中
@Inherited 允许子类继承父类的注解
@Repeatable 允许一个注解可以被使用一次或者多次(Java 8)

使用最多的其实就是 @Target 以及 @Retention。

@Targe:注解中指定的每一个 ElementType 就是一个约束,它告诉编译器,这个自定义的注解只能用于指定的类型。你可以指定 enum ElementType 中的一个值,或者以逗号分割的形式指定多个值。如果想要将注解应用于所有的 ElementType,那么可以省去 @Target 注解,但是这并不常见。

@Retention:表明注解存在的时长,使用最多的是 RUNTIME,使用 RUNTIME 的时候,注解在运行期也保留着,这时就可以通过反射机制读取注解信息,如果使用 SOURCE,CLASS,那么就无法通过反射获取。

注解处理器

单独定义一个注释是没什么意义的,我们要给一个注释赋予意义,那么就得 coding,给这个注释编写一个注解处理器。这里我仅演示最简单的注解处理器。

这个列子很简单,定义了一个注解 @Test,该注解可以在方法上使用,可以被带入到运行时。AnnotationDemo 类实现了 Interface 接口,demo1()、demo2()、demo3()使用了注解,其中 demo3() 使用默认值,demo4() 没有引入注解。这里实现接口的原因是为了使用动态代理来调用方法,处理注解的逻辑写在动态代理里。动态代理类 InvokeClass,可以看到 invoke 方法里拿到 obj 对应的方法(这里不直接用入参的 method 字段是因为该字段代表接口方法,接口方法没有加注解,获取到的 Test annotation 会为空),这里拿到了方法上的注解信息后可以编写自己想要的处理逻辑,我这边就简单把 @Test 注解的 value() 值打印出来。

动态代理文章可以看:简单易懂将反射

(这里字符串判空写的有点丑了,是因为我没引入对应工具类)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value() default "--- default ---";
}
interface Interface {

    void demo1();

    void demo2();

    void demo3();

    void demo4();
}


public class AnnotationDemo implements Interface {

    @Test("demo1")
    @Override
    public void demo1() {

    }

    @Test("demo2")
    @Override
    public void demo2() {

    }

    @Test
    @Override
    public void demo3() {

    }

    @Override
    public void demo4() {

    }
}
class InvokeClass implements InvocationHandler {

    Object obj;

    public InvokeClass(Object obj) {
        this.obj = obj;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method objectMethod = obj.getClass().getMethod(method.getName());
        Test annotation = objectMethod.getAnnotation(Test.class);
        if (Objects.nonNull(annotation) && !"".equals(annotation.value())) {
            System.out.println(annotation.value());
        }
        return method.invoke(obj, args);
    }
}


public class Main {

    public static void main(String[] args) {

        Interface anInterface = (Interface) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Interface.class}, new InvokeClass(new AnnotationDemo()));
        anInterface.demo1();
        anInterface.demo2();
        anInterface.demo3();
        anInterface.demo4();
    }

}

输出:

demo1
demo2
--- default ---

Spring 如何自定义注解

在 spring 中使用自定义注解一般是配合 aop 使用的。

如下,还是注解 @Test ,有个 AnnotationDemo 类,在方法上使用了注解,并且将自身注入 spring 容器 (@Service),并且通过实现 BeanFactoryAware 接口,在初始化的时候调用 setBeanFactory 方法,这里通过传入的 bean 工厂获取到 bean 并且调用方法。

定义一个切面 AspectDemo,切点 pointcut 为我们自定义的注解类,增强 advice 是打印了 @Test 注解的 value() 信息。这样当调用了使用了 @Test 的注解的方法的时候,就是会打印对应的 value() 信息。启动项目,由于在 setBeanFactory 方法中调用了 AnnotationDemo 类的几个方法,因此打印出了对应的注解的 value 信息。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value() default "--- default ---";
}
@Service
public class AnnotationDemo implements BeanFactoryAware {

    @Test("demo1")
    public void demo1() {

    }

    @Test("demo2")
    public void demo2() {

    }

    @Test
    public void demo3() {

    }

    public void demo4() {

    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        AnnotationDemo demo = beanFactory.getBean(AnnotationDemo.class);
        demo.demo1();
        demo.demo2();
        demo.demo3();
        demo.demo4();
    }
}
@Component
@Aspect
public class AspectDemo {

    @Pointcut("@annotation(com.example.spring_project.Test)")
    private void pointcut() {}

    @Before("pointcut() && @annotation(test)")
    public void advice(Test test) {
        System.out.println(test.value());
    }

}

输出:

demo1
demo2
--- default ---

文章为本人学习过程中的一些个人见解,漏洞是必不可少的,希望各位大佬多多指教,帮忙修复修复漏洞!!!

进入本人语雀文档体验更好哦
https://www.yuque.com/docs/share/d1d3f7bb-0918-4844-a870-fc50eb0da707?# 《注解》

参考资料:java 编程思想