写点什么

Java 深度历险(六)——Java 注解

  • 2011-03-22
  • 本文字数:5093 字

    阅读完需:约 17 分钟

在开发 Java 程序,尤其是 Java EE 应用的时候,总是免不了与各种配置文件打交道。以 Java EE 中典型的 S(pring)S(truts)H(ibernate) 架构来说, Spring Struts Hibernate 这三个框架都有自己的 XML 格式的配置文件。这些配置文件需要与 Java 源代码保存同步,否则的话就可能出现错误。而且这些错误有可能到了运行时刻才被发现。把同一份信息保存在两个地方,总是个坏的主意。理想的情况是在一个地方维护这些信息就好了。其它部分所需的信息则通过自动的方式来生成。JDK 5 中引入了源代码中的注解(annotation)这一机制。注解使得 Java 源代码中不但可以包含功能性的实现代码,还可以添加元数据。注解的功能类似于代码中的注释,所不同的是注解不是提供代码功能的说明,而是实现程序功能的重要组成部分。Java 注解已经在很多框架中得到了广泛的使用,用来简化程序中的配置。

使用注解

在一般的 Java 开发中,最常接触到的可能就是 @Override @SupressWarnings 这两个注解了。使用 @Override 的时候只需要一个简单的声明即可。这种称为标记注解(marker annotation ),它的出现就代表了某种配置语义。而其它的注解是可以有自己的配置参数的。配置参数以名值对的方式出现。使用 @SupressWarnings 的时候需要类似 @SupressWarnings({“uncheck”, “unused”}) 这样的语法。在括号里面的是该注解可供配置的值。由于这个注解只有一个配置参数,该参数的名称默认为 value,并且可以省略。而花括号则表示是数组类型。在 JPA 中的 @Table 注解使用类似 @Table(name = “Customer”, schema = “APP”) 这样的语法。从这里可以看到名值对的用法。在使用注解时候的配置参数的值必须是编译时刻的常量。

从某种角度来说,可以把注解看成是一个 XML 元素,该元素可以有不同的预定义的属性。而属性的值是可以在声明该元素的时候自行指定的。在代码中使用注解,就相当于把一部分元数据从 XML 文件移到了代码本身之中,在一个地方管理和维护。

开发注解

在一般的开发中,只需要通过阅读相关的 API 文档来了解每个注解的配置参数的含义,并在代码中正确使用即可。在有些情况下,可能会需要开发自己的注解。这在库的开发中比较常见。注解的定义有点类似接口。下面的代码给出了一个简单的描述代码分工安排的注解。通过该注解可以在源代码中记录每个类或接口的分工和进度情况。

复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Assignment {
String assignee();
int effort();
double finished() default 0;
}

@interface 用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型。可以通过 default 来声明参数的默认值。在这里可以看到 @Retention @Target 这样的元注解,用来声明注解本身的行为。@Retention 用来声明注解的保留策略,有 CLASS RUNTIME SOURCE 这三种,分别表示注解保存在类文件、JVM 运行时刻和源代码中。只有当声明为 RUNTIME 的时候,才能够在运行时刻通过反射 API 来获取到注解的信息。@Target 用来声明注解可以被添加在哪些类型的元素上,如类型、方法和域等。

处理注解

在程序中添加的注解,可以在编译时刻或是运行时刻来进行处理。在编译时刻处理的时候,是分成多趟来进行的。如果在某趟处理中产生了新的 Java 源文件,那么就需要另外一趟处理来处理新生成的源文件。如此往复,直到没有新文件被生成为止。在完成处理之后,再对 Java 代码进行编译。JDK 5 中提供了 apt 工具用来对注解进行处理。apt 是一个命令行工具,与之配套的还有一套用来描述程序语义结构的 Mirror API 。Mirror API(com.sun.mirror.*)描述的是程序在编译时刻的静态结构。通过 Mirror API 可以获取到被注解的 Java 类型元素的信息,从而提供相应的处理逻辑。具体的处理工作交给 apt 工具来完成。编写注解处理器的核心是 AnnotationProcessorFactory AnnotationProcessor 两个接口。后者表示的是注解处理器,而前者则是为某些注解类型创建注解处理器的工厂。

以上面的注解 Assignment 为例,当每个开发人员都在源代码中更新进度的话,就可以通过一个注解处理器来生成一个项目整体进度的报告。 首先是注解处理器工厂的实现。

复制代码
public class AssignmentApf implements AnnotationProcessorFactory {
public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> atds,? AnnotationProcessorEnvironment env) {
if (atds.isEmpty()) {
return AnnotationProcessors.NO_OP;
}
return new AssignmentAp(env); // 返回注解处理器
}
public Collection<String> supportedAnnotationTypes() {
return Collections.unmodifiableList(Arrays.asList("annotation.Assignment"));
}
public Collection<String> supportedOptions() {
return Collections.emptySet();
}
}

AnnotationProcessorFactory 接口有三个方法:getProcessorFor 是根据注解的类型来返回特定的注解处理器;supportedAnnotationTypes 是返回该工厂生成的注解处理器所能支持的注解类型;supportedOptions 用来表示所支持的附加选项。在运行 apt 命令行工具的时候,可以通过 -A 来传递额外的参数给注解处理器,如 -Averbose=true。当工厂通过 supportedOptions 方法声明了所能识别的附加选项之后,注解处理器就可以在运行时刻通过 AnnotationProcessorEnvironment 的 getOptions 方法获取到选项的实际值。注解处理器本身的基本实现如下所示。

复制代码
public class AssignmentAp implements AnnotationProcessor {
private AnnotationProcessorEnvironment env;
private AnnotationTypeDeclaration assignmentDeclaration;
public AssignmentAp(AnnotationProcessorEnvironment env) {
this.env = env;
assignmentDeclaration = (AnnotationTypeDeclaration) env.getTypeDeclaration("annotation.Assignment");
}
public void process() {
Collection<Declaration> declarations = env.getDeclarationsAnnotatedWith(assignmentDeclaration);
for (Declaration declaration : declarations) {
processAssignmentAnnotations(declaration);
}
}
private void processAssignmentAnnotations(Declaration declaration) {
Collection<AnnotationMirror> annotations = declaration.getAnnotationMirrors();
for (AnnotationMirror mirror : annotations) {
if (mirror.getAnnotationType().getDeclaration().equals(assignmentDeclaration)) {
Map<AnnotationTypeElementDeclaration, AnnotationValue> values = mirror.getElementValues();
String assignee = (String) getAnnotationValue(values, "assignee"); // 获取注解的值
}
}
}
}

注解处理器的处理逻辑都在 process 方法中完成。通过一个声明( Declaration )的 getAnnotationMirrors 方法就可以获取到该声明上所添加的注解的实际值。得到这些值之后,处理起来就不难了。

在创建好注解处理器之后,就可以通过 apt 命令行工具来对源代码中的注解进行处理。 命令的运行格式是 apt -classpath bin -factory annotation.apt.AssignmentApf src/annotation/work/*.java,即通过 -factory 来指定注解处理器工厂类的名称。实际上,apt 工具在完成处理之后,会自动调用 javac 来编译处理完成后的源代码。

JDK 5 中的 apt 工具的不足之处在于它是 Oracle 提供的私有实现。在 JDK 6 中,通过 JSR 269 把自定义注解处理器这一功能进行了规范化,有了新的 javax.annotation.processing 这个新的 API。对 Mirror API 也进行了更新,形成了新的 javax.lang.model 包。注解处理器的使用也进行了简化,不需要再单独运行 apt 这样的命令行工具,Java 编译器本身就可以完成对注解的处理。对于同样的功能,如果用 JSR 269 的做法,只需要一个类就可以了。

复制代码
@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes("annotation.Assignment")
public class AssignmentProcess extends AbstractProcessor {
private TypeElement assignmentElement;
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Elements elementUtils = processingEnv.getElementUtils();
assignmentElement = elementUtils.getTypeElement("annotation.Assignment");
}
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(assignmentElement);
for (Element element : elements) {
processAssignment(element);
}
}
private void processAssignment(Element element) {
List<? extends AnnotationMirror> annotations = element.getAnnotationMirrors();
for (AnnotationMirror mirror : annotations) {
if (mirror.getAnnotationType().asElement().equals(assignmentElement)) {
Map<? extends ExecutableElement, ? extends AnnotationValue> values = mirror.getElementValues();
String assignee = (String) getAnnotationValue(values, "assignee"); // 获取注解的值
}
}
}
}

仔细比较上面两段代码,可以发现它们的基本结构是类似的。不同之处在于 JDK 6 中通过元注解 @SupportedAnnotationTypes 来声明所支持的注解类型。另外描述程序静态结构的 javax.lang.model 包使用了不同的类型名称。使用的时候也更加简单,只需要通过 javac -processor annotation.pap.AssignmentProcess Demo1.java 这样的方式即可。

上面介绍的这两种做法都是在编译时刻进行处理的。而有些时候则需要在运行时刻来完成对注解的处理。这个时候就需要用到 Java 的反射 API。反射 API 提供了在运行时刻读取注解信息的支持。不过前提是注解的保留策略声明的是运行时。Java 反射 API 的 AnnotatedElement 接口提供了获取类、方法和域上的注解的实用方法。比如获取到一个 Class 类对象之后,通过 getAnnotation 方法就可以获取到该类上添加的指定注解类型的注解。

实例分析

下面通过一个具体的实例来分析说明在实践中如何来使用和处理注解。假定有一个公司的雇员信息系统,从访问控制的角度出发,对雇员的工资的更新只能由具有特定角色的用户才能完成。考虑到访问控制需求的普遍性,可以定义一个注解来让开发人员方便的在代码中声明访问控制权限。

复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredRoles {
String[] value();
}

下一步则是如何对注解进行处理,这里使用的 Java 的反射 API 并结合动态代理。下面是动态代理中的 InvocationHandler 接口的实现。

复制代码
public class AccessInvocationHandler<T> implements InvocationHandler {
final T accessObj;
public AccessInvocationHandler(T accessObj) {
this.accessObj = accessObj;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RequiredRoles annotation = method.getAnnotation(RequiredRoles.class); // 通过反射 API 获取注解
if (annotation != null) {
String[] roles = annotation.value();
String role = AccessControl.getCurrentRole();
if (!Arrays.asList(roles).contains(role)) {
throw new AccessControlException("The user is not allowed to invoke this method.");
}
}
return method.invoke(accessObj, args);
}
}

在具体使用的时候,首先要通过 Proxy.newProxyInstance 方法创建一个 EmployeeGateway 的接口的代理类,使用该代理类来完成实际的操作。

参考资料


感谢张凯峰对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

关注 IT 趋势,承载前沿、深入、有温度的内容。感兴趣的读者可以搜索 ID:laocuixiabian,或者扫描下方二维码加关注。

2011-03-22 11:0094864

评论 1 条评论

发布
用户头像
看不懂
2019-09-02 15:40
回复
没有更多了
发现更多内容

音乐NFT系统开发的技术难点

北京木奇移动技术有限公司

区块链技术 软件外包公司 音乐NFT

【GreatSQL优化器-11】finalize_table_conditions

GreatSQL

AI智能体在自动化测试中的应用

测试人

反向 Debug 了解一下?揭秘 Java DEBUG 的基本原理

京东科技开发者

版面分析技术研究方向:真实世界中更丰富的版面布局

合合技术团队

人工智能 AI 数据集 Transformer

故障测试与性能测试交叉实践

FunTester

音乐 NFT 系统的智能合约开发

北京木奇移动技术有限公司

智能合约 软件外包公司 音乐NFT

智能网联汽车的数据脱敏

芯盾时代

车联网 物联网 数据安全 智能汽车

音视频编解码开发的技术难点

北京木奇移动技术有限公司

音视频开发 音视频引擎 软件外包公司

破局铜加工生产管理困境:MES系统引领智能化转型

万界星空科技

制造业 mes 万界星空科技 铜管加工行业mes 铜加工行业

哈啰:构建智能出行RAG,ES还是向量数据库?

Zilliz

Milvus 向量数据库 rag 哈啰 zilliz cloud

PIRF 421:Measurements – Embracing the Imperial System

Echo!!!

English

面向法律场景的大模型RAG检索增强解决方案

阿里云大数据AI技术

人工智能 阿里云 LLM rag PAI

2025-01-15:执行操作可获得的最大总奖励 Ⅰ。用go语言,给定一个整数数组 rewardValues,其中包含 n 个代表奖励值的数字。 你开始时的总奖励 x 为 0,并且所有下标都是未标记状

福大大架构师每日一题

福大大架构师每日一题

如何在 Windows 上安装 Python 环境的详细指南

克莱因瓶

加入我们|申请成为亚马逊云科技 Community Builder,共建云端社区!

亚马逊云科技 (Amazon Web Services)

《CPython Internals》阅读笔记:p151-p151

codists

CPython Internals

HBase深度历险

京东科技开发者

基于云主机搭建Termgraph绘图工具,将数据转化为可视化图形

华为云开发者联盟

Python 云主机 鲲鹏 ECS 华为开发者空间

音视频编解码的开发框架

北京木奇移动技术有限公司

音视频开发 音视频引擎 软件外包公司

Easysearch Rollup 使用指南

极限实验室

Rollup Performance easysearch

从0到1:基于SSM的陪诊小程序开发笔记(一)

CC同学

音视频编解码的性能优化

北京木奇移动技术有限公司

软件外包公司 音视频编码 音视频解码

深入了解淘宝天猫API接口:商品详情与关键词搜索商品列表的实用指南

代码忍者

淘宝API接口

记录一次RPC服务有损上线的分析过程

京东科技开发者

基于Springboot: 宠物小程序开发笔记(上)

CC同学

普通人如何赶上AI大模型浪潮

老张

人工智能 AI 自由职业 第二曲线 大模型

MIAOYUN荣获“新质榜样·2024信创力量最佳技术解决方案奖”

MIAOYUN

云计算 云原生 解决方案 信创 超融合

【FAQ】HarmonyOS SDK 闭源开放能力 —Map Kit(4)

HarmonyOS SDK

harmoyos

没想到学会这个 canvas 库,竟然做这么多项目

秦少卫

Fabric.js 开源图片编辑器 开源vue图片编辑器 商品定制工具 服装设计工具

向量数据库如何助力Text2SQL处理高基数类别数据

Zilliz

text2sql Zilli Cloud Waii 高基数类别数据

Java深度历险(六)——Java注解_Java_成富_InfoQ精选文章