背景:最近在工作中发现我们 SRE 的某个 java 项目中,存在大量 annotation 的应用,虽然 java 的注解与 python 的装饰器语法非常类似,但在原理上肯定千差万别。
为了不甘一直处在一知半解的状态,所以这个周末准备全面学习一下对应语法与原理,并与 python 中的实践做一个对比,以便有一个更加深入的理解~
Decorator in Python(装饰器)
语法
常用的语法大致有两种:不带参数
& 带参数
1. 不带参数
刚好拿一个最近在写的 telegram 机器人中,接口权限管控的例子:
1 | def admin(f): |
使用装饰器后,实现可插拔地控制 promote 接口只有「管理员」可以调用,达到代码解耦的目的:
1 |
|
2. 带参数
python 中有一个包叫做 retry
,就是一个很不错的例子:
https://github.com/invl/retry/blob/master/retry/api.py
1 | def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger): |
源代码使用了内置的 @decorator
方法简化了代码,稍微有一点不太好理解,其实等同于:
1 | def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger): |
当被装饰的接口(make_trouble
)在执行过程中,如果抛出了预期内的 exception((ValueError, TypeError)
),则按提前制定好的策略进行重试:
1 |
|
原理
看上去有一点复杂,但只要牢记以下 两者语法的等价关系,即可理解 Python 装饰器的核心思想了😄:
不带参数
1 |
|
带参数
1 |
|
Annotation in Java(注解)
语法
注解的定义
注解的定义 与 接口的定义 非常相似(其实注解就是 interface
的一种):
1 | // 定义 |
注解的使用
使用方式与 python 非常类似,参考下面的例子:
1 | // 使用 |
但不同于 python 的是,在 java8 发布后,注解还可以在类/方法/变量的类型上配合使用(Type Annotations),例如:
1 | // 1. 类的实例化 |
内置的注解
java 还实现了一部分内置的注解
例如 @FunctionalInterface
: 个人理解就是将一个方法的 reference 作为一个变量🤪
注解还可以直接用于其他注解的定义中😯,例如:
@Retention
⚠️划重点,注意 Retention 是保留的意思- SOURCE: 不对编译器可见(只保留在源码中)
- CLASS: 在编译时发挥作用,但被 JVM 忽略(只在 class 文件保留)
- RUNTIME: 在 JVM 运行时被保留并使用
@Target
定义了使用对象的限制,例如:- ANNOTATION_TYPE: 只能在另一个注解上使用
- 等等..
@Repeatable
: 是否可以重复在一个类上使用。@Inherited
: 是否允许子类继承该注解
例如 @FunctionalInterface
的定义:
1 |
|
可重复的注解
虽然个人觉得没有太多必要,但 java 还是提供了这个选项。看了一眼实现还是挺有意思的,简单描述一下:
1 | // 第一步:定义单个 Schedule 注解 |
原理
说实话写到这里,虽然大致知道了注解的用法,似乎对其原理还是毫无头绪。参考了一些文章后的理解:
1. 注解的本质
上文提到注解其实就是一个接口,而它的本质:继承了 Annotation 接口的接口:
对 class 文件反编译后:
1 | // Compiled from "Hello.java" |
2. 注解的获取
利用了 java 的反射机制,获取一个注解类实例,并拿到对应的 value 属性。
1 | Class cls = Main.class; |
3. how does it works!!!
但还是不太明白,从定义 annotation 的接口,到获取对应的实例中间,到底发生了什么呢?
查阅了一些文章后,尝试开启 saveGeneratedFiles 为 "true"
后,目录里出现了 proxy.class
,而其中 $Proxy1.class
就是我们苦苦寻求的真相。
1 | ➜ annotation tree |
当我们上文在调用 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 呢?你的心中有没有一个答案😊