..

Annotation(Decorator) 在 Java & Python 中的应用

背景:最近在工作中发现我们 SRE 的某个 java 项目中,存在大量 annotation 的应用,虽然 java 的注解与 python 的装饰器语法非常类似,但在原理上肯定千差万别。

为了不甘一直处在一知半解的状态,所以这个周末准备全面学习一下对应语法与原理,并与 python 中的实践做一个对比,以便有一个更加深入的理解~

Decorator in Python(装饰器)

语法

常用的语法大致有两种:不带参数 & 带参数

1. 不带参数

刚好拿一个最近在写的 telegram 机器人中,接口权限管控的例子:

def admin(f):
    def wrapper(bot, update):
        # ...
        
        # 用户必须是管理员才可以操作
        if chat_member.status not in (ChatMember.CREATOR, ChatMember.ADMINISTRATOR):
            return

        f(bot, update)

    return wrapper

使用装饰器后,实现可插拔地控制 promote 接口只有「管理员」可以调用,达到代码解耦的目的:

@admin
def promote(bot: Bot, update: Update):
    pass

2. 带参数

python 中有一个包叫做 retry,就是一个很不错的例子: https://github.com/invl/retry/blob/master/retry/api.py

def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger):

    @decorator
    def retry_decorator(f, *fargs, **fkwargs):
        args = fargs if fargs else list()
        kwargs = fkwargs if fkwargs else dict()
        return f(*fargs, **fkwargs) # 实际被装饰函数的调用执行

    return retry_decorator

源代码使用了内置的 @decorator 方法简化了代码,稍微有一点不太好理解,其实等同于:

def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger):

    def retry_decorator(f):
        def wrapper(*fargs, **fkwargs):
            args = fargs if fargs else list()
            return f(*fargs, **fkwargs) # 实际被装饰函数的调用执行
        return wrapper
        
    return retry_decorator

当被装饰的接口(make_trouble)在执行过程中,如果抛出了预期内的 exception((ValueError, TypeError)),则按提前制定好的策略进行重试:

@retry((ValueError, TypeError), tries=7, delay=1, backoff=2)
def make_trouble():
    '''Retry on ValueError or TypeError, sleep 1, 2, 4, 8, ... seconds between attempts.'''

原理

看上去有一点复杂,但只要牢记以下 两者语法的等价关系,即可理解 Python 装饰器的核心思想了😄:

不带参数

@admin
def promote(bot: Bot, update: Update):
    pass
    
# 等价于
admin(promote)(bot, update)

带参数

@retry((ValueError, TypeError), tries=7, delay=1, backoff=2)
def make_trouble():
    '''Retry on ValueError or TypeError, sleep 1, 2, 4, 8, ... seconds between attempts.'''
    pass    

# 等价于
retry((ValueError, TypeError), tries=7, delay=1, backoff=2, 'example')(make_trouble)()

Annotation in Java(注解)

语法

注解的定义

注解的定义 与 接口的定义 非常相似(其实注解就是 interface 的一种):

// 定义
public @interface ClassPreamble {
    String author();
    String date();
    int currentRevision() default 1;
    String[] reviewers();
}

注解的使用

使用方式与 python 非常类似,参考下面的例子:

// 使用
@ClassPreamble(
        author = "John Doe",
        date = "3/17/2002",
        currentRevision = 6,
        // Note array notation
        reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation {}

但不同于 python 的是,在 java8 发布后,注解还可以在类/方法/变量的类型上配合使用(Type Annotations),例如:

// 1. 类的实例化
new @Interned MyObject();

// 2. 类型转换(@NonNull 指使编译器如果发现 null 的潜在可能,则抛出一个警告,以避免在运行态的时候抛出 NPE)
myString = (@NonNull String) str;

// 3. implements clause(不知道如何翻译) 
class UnmodifiableList<T> implements
    @Readonly List<@Readonly T> { ... }
        
// 4. 异常抛出的定义
void monitorTemperature() throws
        @Critical TemperatureException { ... }

内置的注解

java 还实现了一部分内置的注解

例如 @FunctionalInterface: 个人理解就是将一个方法的 reference 作为一个变量🤪

注解还可以直接用于其他注解的定义中😯,例如:

  • @Retention ⚠️划重点,注意 Retention 是保留的意思
    • SOURCE: 不对编译器可见(只保留在源码中)
    • CLASS: 在编译时发挥作用,但被 JVM 忽略(只在 class 文件保留)
    • RUNTIME: 在 JVM 运行时被保留并使用
  • @Target 定义了使用对象的限制,例如:
    • ANNOTATION_TYPE: 只能在另一个注解上使用
    • 等等..
  • @Repeatable: 是否可以重复在一个类上使用。
  • @Inherited: 是否允许子类继承该注解

例如 @FunctionalInterface 的定义:

@Documented
@Retention(value=RUNTIME)
@Target(value=TYPE)
public @interface FunctionalInterface

可重复的注解

虽然个人觉得没有太多必要,但 java 还是提供了这个选项。看了一眼实现还是挺有意思的,简单描述一下:

// 第一步:定义单个 Schedule 注解
@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

// 第二步:定义包含可以包含多个 Schedule 的注解
public @interface Schedules {
    Schedule[] value();
}

// 第三步:具体的使用
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... } 

原理

说实话写到这里,虽然大致知道了注解的用法,似乎对其原理还是毫无头绪。参考了一些文章后的理解:

1. 注解的本质

上文提到注解其实就是一个接口,而它的本质:继承了 Annotation 接口的接口:

对 class 文件反编译后:

// Compiled from "Hello.java"
public interface annotation.Hello extends java.lang.annotation.Annotation {
  public abstract java.lang.String value();
}

2. 注解的获取

利用了 java 的反射机制,获取一个注解类实例,并拿到对应的 value 属性。

Class cls = Main.class;
Method method = cls.getMethod("main", String[].class);

// 使用反射获取一个注解类实例
Hello hello = method.getAnnotation(Hello.class);
System.out.println(hello.value());

// output: hello

3. how does it works!!!

但还是不太明白,从定义 annotation 的接口,到获取对应的实例中间,到底发生了什么呢?

查阅了一些文章后,尝试开启 saveGeneratedFiles 为 "true" 后,目录里出现了 proxy.class,而其中 $Proxy1.class 就是我们苦苦寻求的真相。

➜  annotation tree
.
├── Hello.class
├── Hello.java
├── Main.class
├── Main.java
└── com
    └── sun
        └── proxy
            ├── $Proxy0.class
            └── $Proxy1.class

当我们上文在调用 getAnnotation 获取注解实例的时候,返回的其实是一个 jdk 通过动态代理机制生成的一个代理类 $Proxy1,它实现了我们的注解接口,并将所有方法重写:

所以调用 value 方法的时候,本质上是调用 AnnotationInvocationHandler#invoke,通过方法的名称(value)作为 key,去注解的 map 中取出对应的 value:

终于真相大白了,默默在心里说了一句:原来是这样~

p.s. 偶然翻到一个简化版的实现,感兴趣可以看看:https://gist.github.com/nathansgreen/11084652

总结

python 装饰器与 java 的注解,虽然使用的语法相似,但同时貌似除了语法就没有其他类似的部分了。。。

从文章的篇幅不难看出,java 的 annotation 和 python 相比「复杂」的许多。但到底是功能强大的好,还是 Simple is better than complex 呢?你的心中有没有一个答案😊

参考