mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1497 lines
36 KiB
HTML
1497 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>27 注解处理器.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/00 开篇词 为什么我们要学习Java虚拟机?.md.html">00 开篇词 为什么我们要学习Java虚拟机?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/01 Java代码是怎么运行的?.md.html">01 Java代码是怎么运行的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/02 Java的基本类型.md.html">02 Java的基本类型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/03 Java虚拟机是如何加载Java类的.md.html">03 Java虚拟机是如何加载Java类的.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/04 JVM是如何执行方法调用的?(上).md.html">04 JVM是如何执行方法调用的?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/05 JVM是如何执行方法调用的?(下).md.html">05 JVM是如何执行方法调用的?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/06 JVM是如何处理异常的?.md.html">06 JVM是如何处理异常的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/07 JVM是如何实现反射的?.md.html">07 JVM是如何实现反射的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/08 JVM是怎么实现invokedynamic的?(上).md.html">08 JVM是怎么实现invokedynamic的?(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/09 JVM是怎么实现invokedynamic的?(下).md.html">09 JVM是怎么实现invokedynamic的?(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/10 Java对象的内存布局.md.html">10 Java对象的内存布局.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/11 垃圾回收(上).md.html">11 垃圾回收(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/12 垃圾回收(下).md.html">12 垃圾回收(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/13 Java内存模型.md.html">13 Java内存模型.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/14 Java虚拟机是怎么实现synchronized的?.md.html">14 Java虚拟机是怎么实现synchronized的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/15 Java语法糖与Java编译器.md.html">15 Java语法糖与Java编译器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/16 即时编译(上).md.html">16 即时编译(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/17 即时编译(下).md.html">17 即时编译(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/18 即时编译器的中间表达形式.md.html">18 即时编译器的中间表达形式.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/19 Java字节码(基础篇).md.html">19 Java字节码(基础篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/20 方法内联(上).md.html">20 方法内联(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/21 方法内联(下).md.html">21 方法内联(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html">22 HotSpot虚拟机的intrinsic.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html">23 逃逸分析.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/24 字段访问相关优化.md.html">24 字段访问相关优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/25 循环优化.md.html">25 循环优化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/26 向量化.md.html">26 向量化.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/深入拆解Java虚拟机/27 注解处理器.md.html">27 注解处理器.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/28 基准测试框架JMH(上).md.html">28 基准测试框架JMH(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/29 基准测试框架JMH(下).md.html">29 基准测试框架JMH(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/30 Java虚拟机的监控及诊断工具(命令行篇).md.html">30 Java虚拟机的监控及诊断工具(命令行篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/31 Java虚拟机的监控及诊断工具(GUI篇).md.html">31 Java虚拟机的监控及诊断工具(GUI篇).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">32 JNI的运行机制.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/33 Java Agent与字节码注入.md.html">33 Java Agent与字节码注入.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/34 Graal:用Java编译Java.md.html">34 Graal:用Java编译Java.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/35 Truffle:语言实现框架.md.html">35 Truffle:语言实现框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/36 SubstrateVM:AOT编译框架.md.html">36 SubstrateVM:AOT编译框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/尾声丨道阻且长,努力加餐.html.md.html">尾声丨道阻且长,努力加餐.html.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/工具篇 常用工具介绍.md.html">工具篇 常用工具介绍.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>27 注解处理器</h1>
|
||
|
||
<p>注解(annotation)是 Java 5 引入的,用来为类、方法、字段、参数等 Java 结构提供额外信息的机制。我先举个例子,比如,Java 核心类库中的<code>@Override</code>注解是被用来声明某个实例方法重写了父类的同名同参数类型的方法。</p>
|
||
|
||
<pre><code>package java.lang;
|
||
|
||
|
||
|
||
@Target(ElementType.METHOD)
|
||
|
||
@Retention(RetentionPolicy.SOURCE)
|
||
|
||
public @interface Override {
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p><code>@Override</code>注解本身被另外两个元注解(即作用在注解上的注解)所标注。其中,<code>@Target</code>用来限定目标注解所能标注的 Java 结构,这里<code>@Override</code>便只能被用来标注方法。</p>
|
||
|
||
<p><code>@Retention</code>则用来限定当前注解生命周期。注解共有三种不同的生命周期:<code>SOURCE</code>,<code>CLASS</code>或<code>RUNTIME</code>,分别表示注解只出现在源代码中,只出现在源代码和字节码中,以及出现在源代码、字节码和运行过程中。</p>
|
||
|
||
<p>这里<code>@Override</code>便只能出现在源代码中。一旦标注了<code>@Override</code>的方法所在的源代码被编译为字节码,该注解便会被擦除。</p>
|
||
|
||
<p>我们不难猜到,<code>@Override</code>仅对 Java 编译器有用。事实上,它会为 Java 编译器引入了一条新的编译规则,即如果所标注的方法不是 Java 语言中的重写方法,那么编译器会报错。而当编译完成时,它的使命也就结束了。</p>
|
||
|
||
<p>我们知道,Java 的注解机制允许开发人员自定义注解。这些自定义注解同样可以为 Java 编译器添加编译规则。不过,这种功能需要由开发人员提供,并且以插件的形式接入 Java 编译器中,这些插件我们称之为注解处理器(annotation processor)。</p>
|
||
|
||
<p>除了引入新的编译规则之外,注解处理器还可以用于修改已有的 Java 源文件(不推荐),或者生成新的 Java 源文件。下面,我将用几个案例来详细阐述注解处理器的这些功能,以及它背后的原理。</p>
|
||
|
||
<h2>注解处理器的原理</h2>
|
||
|
||
<p>在介绍注解处理器之前,我们先来了解一下 Java 编译器的工作流程。</p>
|
||
|
||
<p><img src="assets/64e93f67c3b422afd90966bfe9aaf5b8.png" alt="img" /></p>
|
||
|
||
<p>如上图所示 出处 [1],Java 源代码的编译过程可分为三个步骤:</p>
|
||
|
||
<ol>
|
||
|
||
<li>将源文件解析为抽象语法树;</li>
|
||
|
||
<li>调用已注册的注解处理器;</li>
|
||
|
||
<li>生成字节码。</li>
|
||
|
||
</ol>
|
||
|
||
<p>如果在第 2 步调用注解处理器过程中生成了新的源文件,那么编译器将重复第 1、2 步,解析并且处理新生成的源文件。每次重复我们称之为一轮(Round)。</p>
|
||
|
||
<p>也就是说,第一轮解析、处理的是输入至编译器中的已有源文件。如果注解处理器生成了新的源文件,则开始第二轮、第三轮,解析并且处理这些新生成的源文件。当注解处理器不再生成新的源文件,编译进入最后一轮,并最终进入生成字节码的第 3 步。</p>
|
||
|
||
<pre><code>package foo;
|
||
|
||
|
||
|
||
import java.lang.annotation.*;
|
||
|
||
|
||
|
||
@Target({ ElementType.TYPE, ElementType.FIELD })
|
||
|
||
@Retention(RetentionPolicy.SOURCE)
|
||
|
||
public @interface CheckGetter {
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在上面这段代码中,我定义了一个注解<code>@CheckGetter</code>。它既可以用来标注类,也可以用来标注字段。此外,它和<code>@Override</code>相同,其生命周期被限定在源代码中。</p>
|
||
|
||
<p>下面我们来实现一个处理<code>@CheckGetter</code>注解的处理器。它将遍历被标注的类中的实例字段,并检查有没有相应的<code>getter</code>方法。</p>
|
||
|
||
<pre><code>public interface Processor {
|
||
|
||
|
||
|
||
void init(ProcessingEnvironment processingEnv);
|
||
|
||
|
||
|
||
Set<String> getSupportedAnnotationTypes();
|
||
|
||
|
||
|
||
SourceVersion getSupportedSourceVersion();
|
||
|
||
|
||
|
||
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
|
||
|
||
|
||
|
||
...
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>所有的注解处理器类都需要实现接口<code>Processor</code>。该接口主要有四个重要方法。其中,<code>init</code>方法用来存放注解处理器的初始化代码。之所以不用构造器,是因为在 Java 编译器中,注解处理器的实例是通过反射 API 生成的。也正是因为使用反射 API,每个注解处理器类都需要定义一个无参数构造器。</p>
|
||
|
||
<p>通常来说,当编写注解处理器时,我们不声明任何构造器,并依赖于 Java 编译器,为之插入一个无参数构造器。而具体的初始化代码,则放入<code>init</code>方法之中。</p>
|
||
|
||
<p>在剩下的三个方法中,<code>getSupportedAnnotationTypes</code>方法将返回注解处理器所支持的注解类型,这些注解类型只需用字符串形式表示即可。</p>
|
||
|
||
<p><code>getSupportedSourceVersion</code>方法将返回该处理器所支持的 Java 版本,通常,这个版本需要与你的 Java 编译器版本保持一致;而<code>process</code>方法则是最为关键的注解处理方法。</p>
|
||
|
||
<p>JDK 提供了一个实现<code>Processor</code>接口的抽象类<code>AbstractProcessor</code>。该抽象类实现了<code>init</code>、<code>getSupportedAnnotationTypes</code>和<code>getSupportedSourceVersion</code>方法。</p>
|
||
|
||
<p>它的子类可以通过<code>@SupportedAnnotationTypes</code>和<code>@SupportedSourceVersion</code>注解来声明所支持的注解类型以及 Java 版本。</p>
|
||
|
||
<p>下面这段代码便是<code>@CheckGetter</code>注解处理器的实现。由于我使用了 Java 10 的编译器,因此将支持版本设置为<code>SourceVersion.RELEASE_10</code>。</p>
|
||
|
||
<pre><code>package bar;
|
||
|
||
|
||
|
||
import java.util.Set;
|
||
|
||
|
||
|
||
import javax.annotation.processing.*;
|
||
|
||
import javax.lang.model.SourceVersion;
|
||
|
||
import javax.lang.model.element.*;
|
||
|
||
import javax.lang.model.util.ElementFilter;
|
||
|
||
import javax.tools.Diagnostic.Kind;
|
||
|
||
|
||
|
||
import foo.CheckGetter;
|
||
|
||
|
||
|
||
@SupportedAnnotationTypes("foo.CheckGetter")
|
||
|
||
@SupportedSourceVersion(SourceVersion.RELEASE_10)
|
||
|
||
public class CheckGetterProcessor extends AbstractProcessor {
|
||
|
||
|
||
|
||
@Override
|
||
|
||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||
|
||
// TODO: annotated ElementKind.FIELD
|
||
|
||
for (TypeElement annotatedClass : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(CheckGetter.class))) {
|
||
|
||
for (VariableElement field : ElementFilter.fieldsIn(annotatedClass.getEnclosedElements())) {
|
||
|
||
if (!containsGetter(annotatedClass, field.getSimpleName().toString())) {
|
||
|
||
processingEnv.getMessager().printMessage(Kind.ERROR,
|
||
|
||
String.format("getter not found for '%s.%s'.", annotatedClass.getSimpleName(), field.getSimpleName()));
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
|
||
|
||
private static boolean containsGetter(TypeElement typeElement, String name) {
|
||
|
||
String getter = "get" + name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
|
||
|
||
for (ExecutableElement executableElement : ElementFilter.methodsIn(typeElement.getEnclosedElements())) {
|
||
|
||
if (!executableElement.getModifiers().contains(Modifier.STATIC)
|
||
|
||
&& executableElement.getSimpleName().toString().equals(getter)
|
||
|
||
&& executableElement.getParameters().isEmpty()) {
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>该注解处理器仅重写了<code>process</code>方法。这个方法将接收两个参数,分别代表该注解处理器所能处理的注解类型,以及囊括当前轮生成的抽象语法树的<code>RoundEnvironment</code>。</p>
|
||
|
||
<p>由于该处理器针对的注解仅有<code>@CheckGetter</code>一个,而且我们并不会读取注解中的值,因此第一个参数并不重要。在代码中,我直接使用了</p>
|
||
|
||
<pre><code>`roundEnv.getElementsAnnotatedWith(CheckGetter.class)`
|
||
|
||
</code></pre>
|
||
|
||
<p>来获取所有被<code>@CheckGetter</code>注解的类(以及字段)。</p>
|
||
|
||
<p><code>process</code>方法涉及各种不同类型的<code>Element</code>,分别指代 Java 程序中的各个结构。如<code>TypeElement</code>指代类或者接口,<code>VariableElement</code>指代字段、局部变量、enum 常量等,<code>ExecutableElement</code>指代方法或者构造器。</p>
|
||
|
||
<pre><code>package foo; // PackageElement
|
||
|
||
|
||
|
||
class Foo { // TypeElement
|
||
|
||
int a; // VariableElement
|
||
|
||
static int b; // VariableElement
|
||
|
||
Foo () {} // ExecutableElement
|
||
|
||
void setA ( // ExecutableElement
|
||
|
||
int newA // VariableElement
|
||
|
||
) {}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这些结构之间也有从属关系,如上面这段代码所示 (出处 [2])。我们可以通过<code>TypeElement.getEnclosedElements</code>方法,获得上面这段代码中<code>Foo</code>类的字段、构造器以及方法。</p>
|
||
|
||
<p>我们也可以通过<code>ExecutableElement.getParameters</code>方法,获得<code>setA</code>方法的参数。具体这些<code>Element</code>类都有哪些 API,你可以参考它们的 Javadoc[3]。</p>
|
||
|
||
<p>在将该注解处理器编译成 class 文件后,我们便可以将其注册为 Java 编译器的插件,并用来处理其他源代码。注册的方法主要有两种。第一种是直接使用 javac 命令的<code>-processor</code>参数,如下所示:</p>
|
||
|
||
<pre><code>$ javac -cp /CLASSPATH/TO/CheckGetterProcessor -processor bar.CheckGetterProcessor Foo.java
|
||
|
||
error: Class 'Foo' is annotated as @CheckGetter, but field 'a' is without getter
|
||
|
||
1 error
|
||
|
||
</code></pre>
|
||
|
||
<p>第二种则是将注解处理器编译生成的 class 文件压缩入 jar 包中,并在 jar 包的配置文件中记录该注解处理器的包名及类名,即<code>bar.CheckGetterProcessor</code>。</p>
|
||
|
||
<pre><code>(具体路径及配置文件名为`META-INF/services/javax.annotation.processing.Processor`)
|
||
|
||
</code></pre>
|
||
|
||
<p>当启动 Java 编译器时,它会寻找 classpath 路径上的 jar 包是否包含上述配置文件,并自动注册其中记录的注解处理器。</p>
|
||
|
||
<pre><code>$ javac -cp /PATH/TO/CheckGetterProcessor.jar Foo.java
|
||
|
||
error: Class 'Foo' is annotated as @CheckGetter, but field 'a' is without getter
|
||
|
||
1 error
|
||
|
||
</code></pre>
|
||
|
||
<p>此外,我们还可以在 IDE 中配置注解处理器。这里我就不过多演示了,感兴趣的同学可以自行搜索。</p>
|
||
|
||
<h2>利用注解处理器生成源代码</h2>
|
||
|
||
<p>前面提到,注解处理器可以用来修改已有源代码或者生成源代码。</p>
|
||
|
||
<p>确切地说,注解处理器并不能真正地修改已有源代码。这里指的是修改由 Java 源代码生成的抽象语法树,在其中修改已有树节点或者插入新的树节点,从而使生成的字节码发生变化。</p>
|
||
|
||
<p>对抽象语法树的修改涉及了 Java 编译器的内部 API,这部分很可能随着版本变更而失效。因此,我并不推荐这种修改方式。</p>
|
||
|
||
<p>如果你感兴趣的话,可以参考 [Project Lombok][4]。这个项目自定义了一系列注解,并根据注解的内容来修改已有的源代码。例如它提供了<code>@Getter</code>和<code>@Setter</code>注解,能够为程序自动添加<code>getter</code>以及<code>setter</code>方法。有关对使用内部 API 的讨论,你可以参考 [这篇博客][5],以及 [Lombok 的回应][6]。</p>
|
||
|
||
<p>用注解处理器来生成源代码则比较常用。我们以前介绍过的压力测试 jcstress,以及接下来即将介绍的 JMH 工具,都是依赖这种方式来生成测试代码的。</p>
|
||
|
||
<pre><code>package foo;
|
||
|
||
|
||
|
||
import java.lang.annotation.*;
|
||
|
||
|
||
|
||
@Target(ElementType.METHOD)
|
||
|
||
@Retention(RetentionPolicy.SOURCE)
|
||
|
||
public @interface Adapt {
|
||
|
||
Class<?> value();
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在上面这段代码中,我定义了一个注解<code>@Adapt</code>。这个注解将接收一个<code>Class</code>类型的参数<code>value</code>(如果注解类仅包含一个名为<code>value</code>的参数时,那么在使用注解时,我们可以省略<code>value=</code>),具体用法如这段代码所示。</p>
|
||
|
||
<pre><code>// Bar.java
|
||
|
||
package test;
|
||
|
||
import java.util.function.IntBinaryOperator;
|
||
|
||
import foo.Adapt;
|
||
|
||
|
||
|
||
public class Bar {
|
||
|
||
@Adapt(IntBinaryOperator.class)
|
||
|
||
public static int add(int a, int b) {
|
||
|
||
return a + b;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>接下来,我们来实现一个处理<code>@Adapt</code>注解的处理器。该处理器将生成一个新的源文件,实现参数<code>value</code>所指定的接口,并且调用至被该注解所标注的方法之中。</p>
|
||
|
||
<pre><code>package bar;
|
||
|
||
|
||
|
||
import java.io.*;
|
||
|
||
import java.util.Set;
|
||
|
||
|
||
|
||
import javax.annotation.processing.*;
|
||
|
||
import javax.lang.model.SourceVersion;
|
||
|
||
import javax.lang.model.element.*;
|
||
|
||
import javax.lang.model.type.TypeMirror;
|
||
|
||
import javax.lang.model.util.ElementFilter;
|
||
|
||
import javax.tools.JavaFileObject;
|
||
|
||
|
||
|
||
import javax.tools.Diagnostic.Kind;
|
||
|
||
|
||
|
||
@SupportedAnnotationTypes("foo.Adapt")
|
||
|
||
@SupportedSourceVersion(SourceVersion.RELEASE_10)
|
||
|
||
public class AdaptProcessor extends AbstractProcessor {
|
||
|
||
|
||
|
||
@Override
|
||
|
||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||
|
||
for (TypeElement annotation : annotations) {
|
||
|
||
if (!"foo.Adapt".equals(annotation.getQualifiedName().toString())) {
|
||
|
||
continue;
|
||
|
||
}
|
||
|
||
|
||
|
||
ExecutableElement targetAsKey = getExecutable(annotation, "value");
|
||
|
||
|
||
|
||
for (ExecutableElement annotatedMethod : ElementFilter.methodsIn(roundEnv.getElementsAnnotatedWith(annotation))) {
|
||
|
||
if (!annotatedMethod.getModifiers().contains(Modifier.PUBLIC)) {
|
||
|
||
processingEnv.getMessager().printMessage(Kind.ERROR, "@Adapt on non-public method");
|
||
|
||
continue;
|
||
|
||
}
|
||
|
||
if (!annotatedMethod.getModifiers().contains(Modifier.STATIC)) {
|
||
|
||
// TODO support non-static methods
|
||
|
||
continue;
|
||
|
||
}
|
||
|
||
|
||
|
||
TypeElement targetInterface = getAnnotationValueAsTypeElement(annotatedMethod, annotation, targetAsKey);
|
||
|
||
if (targetInterface.getKind() != ElementKind.INTERFACE) {
|
||
|
||
processingEnv.getMessager().printMessage(Kind.ERROR, "@Adapt with non-interface input");
|
||
|
||
continue;
|
||
|
||
}
|
||
|
||
|
||
|
||
TypeElement enclosingType = getTopLevelEnclosingType(annotatedMethod);
|
||
|
||
createAdapter(enclosingType, annotatedMethod, targetInterface);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
return true;
|
||
|
||
}
|
||
|
||
|
||
|
||
private void createAdapter(TypeElement enclosingClass, ExecutableElement annotatedMethod,
|
||
|
||
TypeElement targetInterface) {
|
||
|
||
PackageElement packageElement = (PackageElement) enclosingClass.getEnclosingElement();
|
||
|
||
String packageName = packageElement.getQualifiedName().toString();
|
||
|
||
String className = enclosingClass.getSimpleName().toString();
|
||
|
||
String methodName = annotatedMethod.getSimpleName().toString();
|
||
|
||
String adapterName = className + "_" + methodName + "Adapter";
|
||
|
||
|
||
|
||
ExecutableElement overriddenMethod = getFirstNonDefaultExecutable(targetInterface);
|
||
|
||
|
||
|
||
try {
|
||
|
||
Filer filer = processingEnv.getFiler();
|
||
|
||
JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + adapterName, new Element[0]);
|
||
|
||
|
||
|
||
try (PrintWriter out = new PrintWriter(sourceFile.openWriter())) {
|
||
|
||
out.println("package " + packageName + ";");
|
||
|
||
out.println("import " + targetInterface.getQualifiedName() + ";");
|
||
|
||
out.println();
|
||
|
||
out.println("public class " + adapterName + " implements " + targetInterface.getSimpleName() + " {");
|
||
|
||
out.println(" @Override");
|
||
|
||
out.println(" public " + overriddenMethod.getReturnType() + " " + overriddenMethod.getSimpleName()
|
||
|
||
+ formatParameter(overriddenMethod, true) + " {");
|
||
|
||
out.println(" return " + className + "." + methodName + formatParameter(overriddenMethod, false) + ";");
|
||
|
||
out.println(" }");
|
||
|
||
out.println("}");
|
||
|
||
}
|
||
|
||
} catch (IOException e) {
|
||
|
||
throw new RuntimeException(e);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
private ExecutableElement getExecutable(TypeElement annotation, String methodName) {
|
||
|
||
for (ExecutableElement method : ElementFilter.methodsIn(annotation.getEnclosedElements())) {
|
||
|
||
if (methodName.equals(method.getSimpleName().toString())) {
|
||
|
||
return method;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
processingEnv.getMessager().printMessage(Kind.ERROR, "Incompatible @Adapt.");
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
|
||
|
||
private ExecutableElement getFirstNonDefaultExecutable(TypeElement annotation) {
|
||
|
||
for (ExecutableElement method : ElementFilter.methodsIn(annotation.getEnclosedElements())) {
|
||
|
||
if (!method.isDefault()) {
|
||
|
||
return method;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
processingEnv.getMessager().printMessage(Kind.ERROR,
|
||
|
||
"Target interface should declare at least one non-default method.");
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
|
||
|
||
private TypeElement getAnnotationValueAsTypeElement(ExecutableElement annotatedMethod, TypeElement annotation,
|
||
|
||
ExecutableElement annotationFunction) {
|
||
|
||
TypeMirror annotationType = annotation.asType();
|
||
|
||
|
||
|
||
for (AnnotationMirror annotationMirror : annotatedMethod.getAnnotationMirrors()) {
|
||
|
||
if (processingEnv.getTypeUtils().isSameType(annotationMirror.getAnnotationType(), annotationType)) {
|
||
|
||
AnnotationValue value = annotationMirror.getElementValues().get(annotationFunction);
|
||
|
||
if (value == null) {
|
||
|
||
processingEnv.getMessager().printMessage(Kind.ERROR, "Unknown @Adapt target");
|
||
|
||
continue;
|
||
|
||
}
|
||
|
||
TypeMirror targetInterfaceTypeMirror = (TypeMirror) value.getValue();
|
||
|
||
return (TypeElement) processingEnv.getTypeUtils().asElement(targetInterfaceTypeMirror);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
processingEnv.getMessager().printMessage(Kind.ERROR, "@Adapt should contain target()");
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
|
||
|
||
private TypeElement getTopLevelEnclosingType(ExecutableElement annotatedMethod) {
|
||
|
||
TypeElement enclosingType = null;
|
||
|
||
Element enclosing = annotatedMethod.getEnclosingElement();
|
||
|
||
|
||
|
||
while (enclosing != null) {
|
||
|
||
if (enclosing.getKind() == ElementKind.CLASS) {
|
||
|
||
enclosingType = (TypeElement) enclosing;
|
||
|
||
} else if (enclosing.getKind() == ElementKind.PACKAGE) {
|
||
|
||
break;
|
||
|
||
}
|
||
|
||
enclosing = enclosing.getEnclosingElement();
|
||
|
||
}
|
||
|
||
return enclosingType;
|
||
|
||
}
|
||
|
||
|
||
|
||
private String formatParameter(ExecutableElement method, boolean includeType) {
|
||
|
||
StringBuilder builder = new StringBuilder();
|
||
|
||
builder.append('(');
|
||
|
||
String separator = "";
|
||
|
||
|
||
|
||
for (VariableElement parameter : method.getParameters()) {
|
||
|
||
builder.append(separator);
|
||
|
||
if (includeType) {
|
||
|
||
builder.append(parameter.asType());
|
||
|
||
builder.append(' ');
|
||
|
||
}
|
||
|
||
builder.append(parameter.getSimpleName());
|
||
|
||
separator = ", ";
|
||
|
||
}
|
||
|
||
builder.append(')');
|
||
|
||
return builder.toString();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在这个注解处理器实现中,我们将读取注解中的值,因此我将使用<code>process</code>方法的第一个参数,并通过它获得被标注方法对应的<code>@Adapt</code>注解中的<code>value</code>值。</p>
|
||
|
||
<p>之所以采用这种麻烦的方式,是因为<code>value</code>值属于<code>Class</code>类型。在编译过程中,被编译代码中的<code>Class</code>常量未必被加载进 Java 编译器所在的虚拟机中。因此,我们需要通过<code>process</code>方法的第一个参数,获得<code>value</code>所指向的接口的抽象语法树,并据此生成源代码。</p>
|
||
|
||
<p>生成源代码的方式实际上非常容易理解。我们可以通过<code>Filer.createSourceFile</code>方法获得一个类似于文件的概念,并通过<code>PrintWriter</code>将具体的内容一一写入即可。</p>
|
||
|
||
<p>当将该注解处理器作为插件接入 Java 编译器时,编译前面的<code>test/Bar.java</code>将生成下述代码,并且触发新一轮的编译。</p>
|
||
|
||
<pre><code>package test;
|
||
|
||
import java.util.function.IntBinaryOperator;
|
||
|
||
|
||
|
||
public class Bar_addAdapter implements IntBinaryOperator {
|
||
|
||
@Override
|
||
|
||
public int applyAsInt(int arg0, int arg1) {
|
||
|
||
return Bar.add(arg0, arg1);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<blockquote>
|
||
|
||
<p>注意,该注解处理器没有处理所编译的代码包名为空的情况。</p>
|
||
|
||
</blockquote>
|
||
|
||
<h2>总结与实践</h2>
|
||
|
||
<p>今天我介绍了 Java 编译器的注解处理器。</p>
|
||
|
||
<p>注解处理器主要有三个用途。一是定义编译规则,并检查被编译的源文件。二是修改已有源代码。三是生成新的源代码。其中,第二种涉及了 Java 编译器的内部 API,因此并不推荐。第三种较为常见,是 OpenJDK 工具 jcstress,以及 JMH 生成测试代码的方式。</p>
|
||
|
||
<p>Java 源代码的编译过程可分为三个步骤,分别为解析源文件生成抽象语法树,调用已注册的注解处理器,和生成字节码。如果在第 2 步中,注解处理器生成了新的源代码,那么 Java 编译器将重复第 1、2 步,直至不再生成新的源代码。</p>
|
||
|
||
<hr />
|
||
|
||
<p>今天的实践环节,请实现本文的案例<code>CheckGetterProcessor</code>中的 TODO 项,处理由<code>@CheckGetter</code>注解的字段。</p>
|
||
|
||
<p>[1] http://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html
|
||
|
||
[2] http://hannesdorfmann.com/annotation-processing/annotationprocessing101
|
||
|
||
[3] https://docs.oracle.com/javase/10/docs/api/javax/lang/model/element/package-summary.html
|
||
|
||
[4] https://projectlombok.org/
|
||
|
||
[5] http://notatube.blogspot.com/2010/11/project-lombok-trick-explained.html
|
||
|
||
[6] http://jnb.ociweb.com/jnb/jnbJan2010.html#controversy</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/26 向量化.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/深入拆解Java虚拟机/28 基准测试框架JMH(上).md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997a1509b33cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|