19. Groovy 面向对象编程-注解学习

2022-12-08 17:55:55 浏览数 (1)

1. 介绍

本篇为Groovy学习笔记第十九篇。分享关于注解的相关知识。Annotations(注解)。现在的各种开发语言可以说都有注解。

注解除了可以在实际开发中使用,而各种开发插件也大量使用了注解功能。

例如,我们给某个属性或者变量添加注解后。就可以自动获取相关参数信息等,这都是通过注解的方式让编译器自动生成了相关的匿名类和方法的。

本质上来说,注解就是可以节省部分样板代码,告知编译器我要实现这个功能,你自动帮我实现。

2. 基本注解

PS:有说标准应该是叫注释,也有说是叫注解。百度说注解和注释有不同的解释。我不想纠结这些东西。 我理解的注释:通过// 只提供一些描述性解释。根本不参与代码效果的 叫做注释。 通过@关键字,会影响程序或者编译器的解释。叫做注解。

2.1 注解定义

在Groovy中注解是一种专用于注解代码元素的特殊接口。注解是一种类型,其超级接口是java.lang.annotation。注解的声明方式与接口非常相似,使用@interface关键字。

代码语言:javascript复制
//创建一个名称为 SomeAnnotation的注解对象。@interface SomeAnnotation {   } 
//其实该注解本质上就是一个接口对象,我们可以继承接口试试效果。class Zin implements SomeAnnotation{    //继承注解的接口,必须重构该方法。这个是默认抽象的方法。    @Override    Class<SomeAnnotation> annotationType() {        return null    }}
def xx = new Zin()println(xx instanceof SomeAnnotation) //输出  :true

注解可以以方法的形式定义成员,而不包含主体和可选的默认值。可以定义的成员类型为:

  • 基本类型,例如int,float,long,等等。
  • java.lang.String:String字符串对象。
  • java.lang.Class :Class类对象。
  • java.lang.Enum :枚举对象。
  • java.lang.annotation.annotation: java的注解对象。
  • 以上对象组成的Array数组对象。

下面结合更多的示例进行了解吧。示例如下:

代码语言:javascript复制
@interface SomeAnnotation {    String value()                          }
@interface SomeAnnotation {    String value() default 'zinyan.com'      //默认值为  zinyan.com}@interface SomeAnnotation {    int step()    default 1                         }@interface SomeAnnotation {    Class appliesTo()                        }@interface SomeAnnotation {}
@interface SomeAnnotations {    SomeAnnotation[] value()       //定义枚举对象              }
enum DayOfWeek { mon, tue, wed, thu, fri, sat, sun }
@interface Scheduled {    DayOfWeek dayOfWeek()      //定义枚举对象             }

与Java语言不同,在Groovy中,可以使用注解来更改语言的语义。AST转换尤其如此,它将基于注解生成代码。

2.2 注解使用

在上面介绍了如何定义注解。就是在类的前面通过@ 关键字进行创建。这里定义如何使用注解。

代码语言:javascript复制
@SomeAnnotation                 void someMethod() {  //这个是使用了注解的方法}
@SomeAnnotation                 class SomeClass {} //使用了注解的类
@SomeAnnotation String var       //使用了注解的参数

为了限制可以应用注解的范围,有必要使用java.lang.annotation在注解定义上声明它。目标注解。例如,下面是如何声明注解可以应用于类或方法的:

代码语言:javascript复制
import java.lang.annotation.ElementTypeimport java.lang.annotation.Target
@Target([ElementType.METHOD, ElementType.TYPE])      //@Target 用于范围注解注解@interface SomeAnnotation {}       //接口注解 仅在ElementType.METHOD,和ElementType.TYPE 上允许SomeAnnotation

Groovy不支持java 8中引入的java.lang.annotation.ElementType.TYPE_PARAMETERjava.lang.annotation.ElementTyp.TEYPE_PALAMETER元素类型。

代码语言:javascript复制
import java.lang.annotation.ElementTypeimport java.lang.annotation.Target
@Target([ElementType.METHOD, ElementType.TYPE])     @interface SomeAnnotation {}      
//添加上注解之后,这个接口中的传参就必须是ElementType.METHOD, ElementType.TYPE 否则就会错误了@SomeAnnotationdef getZinyanDemo(ElementType type){    "这是一个注解方法的使用"}
println(getZinyanDemo(ElementType.TYPE)) //输出: 这是一个注解方法的使用

例如我传其他的入参:

代码语言:javascript复制
println(getZinyanDemo(123))

就会提示错误:

代码语言:javascript复制
Caught: groovy.lang.MissingMethodException: No signature of method: Zinyan.getZinyanDemo() is applicable for argument types: (Integer) values: [123]Possible solutions: getZinyanDemo(java.lang.annotation.ElementType)groovy.lang.MissingMethodException: No signature of method: Zinyan.getZinyanDemo() is applicable for argument types: (Integer) values: [123]Possible solutions: getZinyanDemo(java.lang.annotation.ElementType)    at Zinyan.run(Zinyan.groovy:12)

2.3 注解成员参数

使用注解时,需要至少设置所有没有默认值的成员。例如:

代码语言:javascript复制
//创建了一个Page的注解对象,并且定义了一个statusCode的 int 参数。@interface Page {    int statusCode()}
//我们使用Page注解对象的时候,就必须对该参数进行初始化操作@Page(statusCode=404)void notFound() {    // ...}

但是,如果成员值是value值,则可以在在注解赋值时可以省略value=操作。示例如下:

代码语言:javascript复制
//创建一个注解对象,定义了两个参数,其中int参数的默认值为200@interface Page {    String value()    int statusCode() default 200}
//我们在使用注解的时候,有默认值的参数我们就可以不用初始化。@Page(value='zinyan')                    void home() {    // ...}//如果我们只有一个参数需要初始化,可以把value=的字段都省略。@Page('zinyan')                         void userList() {    // ...}
//我们也可以初始化的时候 都进行参数值配置@Page(value='error',statusCode=404)     void notFound() {    // ...}

PS: 到这里,我们可能还是比较迷茫。这个注解到底有什么用处? 除了限制参数。就没有其他的作用了吗?不要急。

2.4 保留策略

注解的可见性取决于其保留策略。注解的保留策略是使用java.lang.annotation.Retention设置的。保留注解示例如下:

代码语言:javascript复制
import java.lang.annotation.Retentionimport java.lang.annotation.RetentionPolicy
@Retention(RetentionPolicy.SOURCE)         //定义了注解的保留策略为:RetentionPolicy.SOURCE   @interface SomeAnnotation {}    

定义完毕后@SomeAnnotation注解将会保留 SOURCE

java.lang.annotation.RetentionPolicy中提供了可能的保留目标和描述的列表。RetentionPolicy枚举对象选择通常取决于您希望注解在编译时还是运行时可见。

  • RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
  • RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,注解的默认策略;
  • RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。

ps:名称叫做保留策略,其实就是注解对象的生命周期。

2.5 闭合注解参数

Groovy中注解的一个有趣的特性是可以使用闭包作为注解值。因此,注解可以与多种表达式一起使用,并且仍然具有IDE支持。例如,想象一个框架,在该框架中,您希望基于环境约束(如JDK版本或操作系统)执行一些方法。可以编写以下代码:

代码语言:javascript复制
class Tasks {    Set result = []    void alwaysExecuted() {        result << 1    }    @OnlyIf({ jdk>=6 })    void supportedOnlyInJDK6() {        result << 'JDK 6'    }    @OnlyIf({ jdk>=7 && windows })    void requiresJDK7AndWindows() {        result << 'JDK 7 Windows'    }}

要使@OnlyIf注解接受闭包作为参数,只需将该值声明为类:

代码语言:javascript复制
@Retention(RetentionPolicy.RUNTIME)@interface OnlyIf {    Class value()                    }

上面只是创建示例,下面结合完整的示例看看效果:

代码语言:javascript复制
import java.lang.annotation.Retentionimport java.lang.annotation.RetentionPolicyimport java.lang.reflect.Modifier
class Runner {    static <T> T run(Class<T> taskClass) {        def tasks = taskClass.newInstance() //获取类对象        def params = [jdk: 8, windows: false]   //模拟传值        def each = tasks.class.declaredMethods.each { m ->  // 通过反射的方式获取类属性            if (Modifier.isPublic(m.modifiers) && m.parameterTypes.length == 0) { //判断类属性信息是否有                def onlyIf = m.getAnnotation(OnlyIf)  //获取类的 OnlyIf注解对象                if (onlyIf) {  //得到该注解对象                    Closure cl = onlyIf.value().newInstance(tasks, tasks)                    cl.delegate = params //进行赋值操作                    if (cl()) {                        m.invoke(tasks)                    }                } else {                    m.invoke(tasks)  //否则直接执行,也是就是为什么输出结果中会有1                }            }        }        each        tasks    }}

class Tasks {    Set result = []
    void alwaysExecuted() {        result << 1    }
    @OnlyIf({ jdk >= 6 && jdk <7 })    void supportedOnlyInJDK6() {        result << 'JDK 6'    }
    @OnlyIf({ jdk >= 7 && jdk <8})    void requiresJDK7AndWindows() {        result << 'JDK 7 Windows'    }
    @OnlyIf({ jdk >= 8 })    void requiresJDK8AndWindows() {        result << 'JDK 8 Windows'    }}
@Retention(RetentionPolicy.RUNTIME)@interface OnlyIf {    Class value()}
def tasks = Runner.run(Tasks)println(tasks.result)  //输出:[1, JDK 8 Windows]

在上面的示例中,我们在创建Tasks类时并没有给它初始化赋值。只是在Runner运行的时候 配置了传参,然后通过识别注解对象@OnlyIf 然后调用了不同的方法进行赋值操作。

在实际生产和使用中,Runner类属于封装为插件等工具中。这样我们就可以根据不同的环境,打包编译不同的代码和方法。

3. 元注解-Meta Annotations

下面介绍有关元注解。这个元和元宇宙可不是一个概念哦。

3.1 声明元注解

元注解,也称为注解别名,是在编译时被其他注解替换的注解(一个元注解是一个或多个注解的别名)。元注解可以用于减少涉及多个注解的代码的大小。

让我们从一个简单的例子开始。假设你有@Service@Transactional注解,并且你想用这两个注解来注解一个类,示例如下:

代码语言:javascript复制
@Service@Transactionalclass MyTransactionalService {}

元注解可以通过将两个注解减少为具有相同语义的单个注解来帮助。例如,我们可改写为如下:

代码语言:javascript复制
@TransactionalService                           class MyTransactionalService {}

在上面这个示例中@TransactionalService 就是一个元注解了。那我们能够直接这么使用吗?当然不行了。元注解也是需要我们进行创建声明的。

元注解声明为常规注解,使用@AnnotationCollector进行标识。在上面的例子中@TransactionalService注解可以写为:

示例如下:

代码语言:javascript复制
import groovy.transform.AnnotationCollector
@Service                                        @Transactional                                  @AnnotationCollector    //声明下面的注解为 元注解                        @interface TransactionalService {  //创建一个TransactionalService的注解对象。}

3.2 元注解的行为

Groovy支持预编译和源格式元注解。这意味着我们的元注解可能是预编译的,或者可以将其放在与当前编译的源码树相同的源码树中。

要注意:元注解是Groovy独有的特性。不能用元注解注解Java类。同样,也不能用Java编写元注解:元注解的定义和用法都必须是在Groovy代码中使用。但我们可以在元注解中使用Java注解和Groovy注解。

当Groovy编译器遇到用元注解注解的类时,它会用收集的注解替换它。因此,在我们前面的示例中,它将用@Transactional@Service替换@TransactiionalService。这也就是为什么说元注解为注解别名了。它在编译过程中会被编译器自动进行拆分为它聚合的多个注解。

从元注解到收集的注解的转换在语义分析编译阶段执行。

除了用收集的注解替换别名之外,元注解还能够处理它们,包括参数。完整示例如下:

代码语言:javascript复制
import groovy.transform.AnnotationCollector
import java.lang.annotation.Retentionimport java.lang.annotation.RetentionPolicy

//定义两个注解对象@Retention(RetentionPolicy.RUNTIME)@interface ZinyanUrl {    String url()}
@Retention(RetentionPolicy.RUNTIME)@interface ZinyanName{    String name()}
//定义一个元注解对象@ZinyanUrl@ZinyanName@AnnotationCollector   //声明下面的注解为 元注解@interface ZinyanCollector {  //创建一个ZinyanCollector的注解对象。}
@ZinyanCollector(name = 'Z同学', url = 'https://zinyan.com')class Test {
}
def annotations = Test.annotations *. annotationType()println(annotations)  //输出 [interface ZinyanUrl, interface ZinyanName]

3.3 元注解参数

元注解可以收集具有参数的注解。为了说明这一点,我们将假设两个注解,每个注解接受一个参数。用上面的示例中的注解:

代码语言:javascript复制
import groovy.transform.AnnotationCollector
import java.lang.annotation.Retentionimport java.lang.annotation.RetentionPolicy//定义两个注解对象@Retention(RetentionPolicy.RUNTIME)@interface ZinyanUrl {    String url()}@Retention(RetentionPolicy.RUNTIME)@interface ZinyanName{    String name()}
//创建一个元注解@ZinyanUrl(url='zinyan.com')@ZinyanName(name='zinyan')@AnnotationCollector@interface Zinyan {}

默认情况下,替换注解时,它们将获得别名中定义的注解参数值。更有趣的是,元注解支持覆盖特定值:

代码语言:javascript复制
@ZinyanCollector(name = 'Z同学', url = 'https://zinyan.com')class Test {}
def values = Test.getAnnotation(ZinyanName).name()print(values) //输出:Z同学
def url = Test.getAnnotation(ZinyanUrl).url()println(url)  //输出:https://zinyan.com

如果两个注解定义了相同的参数名称,则默认处理器会将注解值复制到所有接受此参数的注解:

代码语言:javascript复制
import groovy.transform.AnnotationCollector
import java.lang.annotation.Retentionimport java.lang.annotation.RetentionPolicy

//定义两个注解对象@Retention(RetentionPolicy.RUNTIME)@interface ZinyanUrl {    String value()}
@Retention(RetentionPolicy.RUNTIME)@interface ZinyanName{    String value()}
//定义一个元注解对象@ZinyanUrl@ZinyanName@AnnotationCollector   //声明下面的注解为 元注解@interface ZinyanCollector {  //创建一个ZinyanCollector的注解对象。}
@ZinyanCollector('zinyan')class Test {}
def urlValue = Test.getAnnotation(ZinyanUrl).value()println(urlValue)  //输出 :zinyan
def nameValue = Test.getAnnotation(ZinyanName).value()println(nameValue)  //输出 :zinyan

我们在@ZinyanCollector配置的值就会自动被赋值到@ZinyanUrl@ZinyanName中的value参数中了。

PS:上面的写法是因为我们的方法名称叫做value,完整写法为:@ZinyanCollector(value='zinyan')。但是因为value可以省略。就写成了@ZinyanCollector('zinyan')

如果收集的注解中定义了具有不兼容类型的相同成员,则会导致编译时错误。

例如上面的两个注解中都是String类型,都叫做value 那么没有问题,但是如果一个是int类型,一个是String类型。在编译的时候就会出现错误了。

3.4 处理元注解中的重复注解

@AnnotationCollector注解支持一个模式参数,该参数可用于更改默认处理器在存在重复注解时处理注解替换的方式。

例如,创建了一个包含@ToString注解的元注解,然后将元注解放在一个已经有显式@ToStriing注解的类上。这应该是一个错误吗?是否应同时应用这两个注解?一个优先于另一个吗?没有正确的答案。在某些情况下,这些答案中的任何一个都可能是正确的。因此,Groovy不是试图抢先一种正确的方法来处理重复注解问题,而是让我们编写自己的自定义元注解处理器(下面将介绍),并让我们在AST转换中编写任何我们喜欢的检查逻辑,这是聚合的常见目标。话虽如此,通过简单地设置模mode,可以在任何额外的编码中自动为我们处理许多常见的场景。mode参数的行为由所选的AnnotationCollectorMode枚举值决定。相关的值可以参考下列表格:

AnnotationCollectorMode.DUPLICATE 是默认值。也就是我们不配置时默认的注解冲突解决模型。

模型

描述

DUPLICATE

将始终插入注解集合中的注解。运行所有转换后,如果存在多个注解(不包括保留SOURCE的注释),则会出错。

PREFER_COLLECTOR

将添加收集器中的注解,并删除任何具有相同名称的现有注解。

PREFER_COLLECTOR_MERGED

将添加收集器中的注解,并删除任何具有相同名称的现有注解。但在现有注解中找到的任何新参数都将合并到添加的注解中。

PREFER_EXPLICIT

如果发现任何具有相同名称的现有注解,则收集器中的注解将被忽略。

PREFER_EXPLICIT_MERGED

如果发现任何具有相同名称的现有注解,则收集器中的注解将被忽略,但收集器注解上的任何新参数都将添加到现有注解中。

使用示例如下所示:

代码语言:javascript复制
//定义一个元注解对象@ZinyanUrl@ZinyanName@AnnotationCollector(mode=AnnotationCollectorMode.PREFER_COLLECTOR)   //声明下面的注解为 元注解@interface ZinyanCollector {  //创建一个ZinyanCollector的注解对象。}

3.5 自定义注解处理器

自定义注解处理器将允许我们选择如何将元注释扩展为收集的注解。在这种情况下,元注解的行为完全取决于我们的定义。

我们自定义注解处理器必须实现:

  • 创建元注解处理器,该类必须继承org.codehaus.groovy.transform.AnnotationCollectorTransform
  • 声明要在元注解声明中使用的处理器。

下面让我们结合示例进行学习。首先创建一个元注解对象:

代码语言:javascript复制
import groovy.transform.AnnotationCollectorimport groovy.transform.CompileStaticimport groovy.transform.TypeCheckingMode
//定义一个元注解对象@CompileStatic(TypeCheckingMode.SKIP)@AnnotationCollector()   //声明下面的注解为 元注解@interface ZinyanCollector {  //创建一个ZinyanCollector的注解对象。}

创建这个元注解,该元注解定义了支持扩展为TypeCheckingMode.SKIP。问题是默认的元注释处理器不支持枚举为注解值。

也就是说上面的注解不会生效。我们如果定义为:

代码语言:javascript复制
import groovy.transform.AnnotationCollector
@AnnotationCollector(processor = "demo.zinyan.groovy.processor.CompileProcessor")@interface ZinyanCollector {  //创建一个ZinyanCollector的注解对象。}

我们不再使用@CompileStatic进行注释。原因是我们依赖于处理器参数,它引用了一个将生成注释的类。

我们需要主动创建这个CompileProcessor处理器。示例如下:

代码语言:javascript复制
package demo.zinyan.groovy.processor
import groovy.transform.CompileStatic
import groovy.transform.TypeCheckingModeimport org.codehaus.groovy.ast.AnnotatedNodeimport org.codehaus.groovy.ast.AnnotationNodeimport org.codehaus.groovy.ast.ClassHelperimport org.codehaus.groovy.ast.ClassNodeimport org.codehaus.groovy.ast.expr.ClassExpressionimport org.codehaus.groovy.ast.expr.PropertyExpressionimport org.codehaus.groovy.control.SourceUnitimport org.codehaus.groovy.transform.AnnotationCollectorTransform
@CompileStaticclass CompileProcessor extends AnnotationCollectorTransform {    private static final ClassNode CS_NODE = ClassHelper.make(CompileStatic)    private static final ClassNode TC_NODE = ClassHelper.make(TypeCheckingMode)
    List<AnnotationNode> visit(AnnotationNode collector,                               AnnotationNode aliasAnnotationUsage,                               AnnotatedNode aliasAnnotated,                               SourceUnit source) {        def node = new AnnotationNode(CS_NODE)        def enumRef = new PropertyExpression(                new ClassExpression(TC_NODE), "SKIP")        node.addMember("value", enumRef)        Collections.singletonList(node)    }}

在本例中,visit方法是唯一必须重写的方法。这意味着返回一个注释节点列表,这些注释节点将添加到用元注释注释的节点。在本例中,我们返回了一个对应于@CompileStatic(TypeCheckingMode.SKIP)的值。

visit 方法是通过AnnotationCollectorTransform继承而来的哦。

4. 小结

Groovy中关于注解的相关知识就到这里结束了。上面都是介绍了注解的一些定义和基本使用的规则。我们只有了解这些基本规则了,才能更深入的学习和使用注解。

上面的内容参考Groovy官方文档:http://docs.groovy-lang.org/docs/groovy-4.0.6/html/documentation/#_annotations 。

实例代码都进行过本地Groovy环境的运行。如果你觉得我介绍的还可以希望能够给我点个赞鼓励一下。谢谢。

下一章,将会介绍面向对象编程中的特性模块。

PS:所有的学习,只是让我们了解Groovy,并打下阅读代码和编写代码的基础。如果想将学习的知识转为可以使用的知识。我们还需要阅读其他人写的Groovy代码,自己根据需求编写Groovy代码。在不断的阅读和使用中提高自己的水平。

0 人点赞