背景:最近在工作中发现我们 SRE 的某个 java 项目中,存在大量 annotation 的应用,虽然 java 的注解与 python 的装饰器语法非常类似,但在原理上肯定千差万别。
为了不甘一直处在一知半解的状态,所以这个周末准备全面学习一下对应语法与原理,并与 python 中的实践做一个对比,以便有一个更加 深入 的理解~
常用的语法大致有两种: 不带参数
& 带参数
刚好拿一个最近在写的 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
python 中有一个包叫做 retry
,就是一个很不错的例子:
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 装饰器的核心思想了:smile::
@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)()
注解的定义 与 接口的定义 非常相似(
其实注解就是 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 作为一个变量
注解还可以直接用于其他注解的定义中:hushed:,例如:
@Retention
:warning:划重点
,注意 Retention 是保留的意思 @Target
定义了使用对象的限制,例如: @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() { ... }
说实话写到这里,虽然大致知道了注解的用法,似乎对其原理还是毫无头绪。参考了一些文章后的理解:
上文提到注解其实就是一个接口,而它的本质:继承了 Annotation 接口的接口:
对 class 文件反编译后:
// Compiled from "Hello.java" public interface annotation.Hello extends java.lang.annotation.Annotation { public abstract java.lang.String value(); }
利用了 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
但还是不太明白,从定义 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 呢?你的心中有没有一个答案:blush: