注解
注解是那些插入到源代码中使用其它工具可以对其进行处理的标签。这些工具可以在源码层次上进行操作,或者可以处理编译器在其中放置了注解的类文件。
注解不会改变程序的编译方式。Java编泽器对于包含注解和不包含注解的代码会生成相同的虚拟机指令。
注解的一些可能的用法:
- 附属文件的自动生成,例如部署描述符或者bean信息类。
- 测试、日志、事务语义等代码的自动生成。
1、语法
1.1、注解接口
注解是由注解接口来定义的:
modifiers @interface AnnotationName{
elementDeclaration1
elementDeclaration2
...
}
//每个元素声明都具有下面这种形式
type elementName();
//或
type elementName() default value;
例如,下面的注解具有两个元素:assignedTo和serverity:
public @interface BugReport {
String assignedTo() default "[none]";
int severity();
}
所有的注解接口都隐式地扩展自java.lang.annotation.Annotation
接口。这个接口是一个常规接口,不是一个注解接口:
public interface Annotation {
//如果other是一个实现了与该注解对象相同的注解接口的对象,并且如果该对象和other的所有元素都彼此相等。那么返回True
boolean equals(Object obj);
//返回一个与equals方法兼容、由注解接口名以及元素值衍生而来的散列码
int hashCode();
//返回一个包含注解接口名以及元素值的字符串表示,例如,@BugReport(assignedTo=[none],serverity=0)
String toString();
//返回Class对象,它用于描述该注解对象的注解接口。注意:调用注解对象上的getClass方法可以返回真正额类,而不是接口
Class<? extends Annotation> annotationType();
}
注意:我们无法手动扩展Annotation接口,所有的注解都直接扩展自Annotation。
注解元素的类型为下列之一:
- 基本类型(int、short、long、byte、char、double、float或者boolean)
- String
- Class(具有一个可选的类型参数,例如Class<? extends MyClass>)
- enum类型
- 注解类型
- 由前面类型组成的数组(由数组组成的数组不是合法的元素类型)
public @interface BugReport {
enum Status {
UNCONFIRMED,
CONFIRMED,
FIXED,
NOTABUG
};
boolean showStopper() default false;
String assignedTo() default "[none]";
Class<?> testCase() default Void.class;
Status status() default Status.UNCONFIRMED;
Reference ref() default @Reference();
String[] reportedBy();
}
1.2、注解格式
每个注解都具有下面这种格式:
@AnnottionName(elementName1=value1, elementName2=value2,...)
例如:
@BugReport(assignedTo="Harry", severity=10)
//元素的顺序无关紧要
@BugReport(severity=10, assignedTo="Harry")
如果某个元素的值未指定,那么就使用声明的默认值。
例如:
@BugReport(severity=10)
//元素assignedTo的值就是字符串"[none]"
警告:默认值并不是和注解存储在一起的;相反地,它们是动态计算而来的。例如,如果你将元素assignedTo的默认值更改为"[]",然后重新编译BugReport接口,那么注解BugReport(severity=10)将使用这个新的默认值,甚至在那些在默认值修改之前就已经编译过的类文件中也是如此。
有两个特殊的快捷方式可以用来化简注解:
- 标记注解:如果没有指定元素,要么是因为注解中没有任何元素,要么是因为所有元素都使用默认值,那么就不需要使用圆括号。
@BugReport
//等价于
@BugReport(assignedTo="[none]", severity=0)
- 单值注解:如果一个元素具有特殊的名字value,并且没有指定其它元素,那么就可以忽略调这个元素名以及等号。
public @interface ActionListenerFor {
String value();
}
//可以写成如下方式
@ActionListenerFor("yellow")
//等价于
@ActionListenerFor(value="yellow")
一个项可以有多个注解:
@Test
@BugReport(showStopper=true,reportedBy="Joe")
public void checkRandomInsertions();
如果注解的作者将其声明为可重复的,那么就可以多次重复使用同一个注解:
@BugReport(showStopper=true, reportedBy="joe")
@BugReport(reportedBy={"harry", "carl"})
public void checkRandomInsertions()
警告:一个注解元素永远不能设置为null,甚至不允许其默认值为null。这样在实际应用中会相当不方便。必须使用其他的默认值,例如""或者Void.class。
如果元素值是一个数组,那么要将它的值用括号括起来:
@BugReport(..., reportedBy={"Harry", "Carl"})
如果该元素具有单值,那么可以忽略括号:
@BugReport(..., reportedBy="Joe")
既然一个注解元素可以是另一个注解,那么就可以创建出任意复杂的注解:
@BugReport(ref=@Reference(id="23423"), ...)
注释:在注解中引入循环依赖是一种错误。例如,因为BugReport具有一个注解类型为Reference的元素,所以Reference就不能再拥有一个类型为BugReport的元素。
1.3、注解声明
声明注解可以出现在下列声明处:
- 类(包括enum)
- 接口(包括注解接口)
- 方法
- 构造器
- 实例域(包含enum常量)
- 局部变量
- 参数变量
- 类型参数
对于类和接口,需要将注解防止在class和interface关键字的前面:
@Entity public class User {...}
对于变量,需要将他们放置在类型的前面:
@SuppressWarnings("unchecked) List<User> users = ...;
public User getUser(@Param("id")String userId);
包是在文件package-info.java中注解的,该文件只包含以注解先导的包语句。
/**
Package-level Javadoc
*/
@GPL(version="3")
package com.horstmann.corejava;
import org.gnu.GPL;
注释:对局部变量的注解只能在源码级别上进行处理。类文件并不描述局部变量。因此,所有的局部变量注解在编译完一个类的时候就会被遗弃掉。同样地,对包的注解
不能在源码级别之外存在。
1.4、注解类型用法
生命注解提供了正在被声明的项的相关信息。
public User getUser(@NonNull String userId)
//断言userId参数不为空
假设,我们有一个类型为List<String>
的参数,并且想要表示其中的所有的字符串都不为null。这就是类型用法注解的大显身手之处,可以将该注解防止到类型参数之前:List<@NonNull String>
。
类型用法注解可以出现在下面的位置:
- 与泛化类型参数一起使用:
List<@NonNull String>, Comparator<@NonNull String> reverseOrder()
- 数组中的任何位置:
@NonNull String[][] words
(words[i][j]不为null)String@NonNull [][] words
(words不为null)String[] @NonNull [] words
(words[i]不为null)
- 与超类和实现接口一起使用:
class Warning extends @Localized Message
- 与构造器调用一起使用:
new @Localized String(...)
- 与强制转型和instanceof检查一起使用:
(@Localized String) text
,if (text instanceof @Localized String)
(这些注解只供外部工具使用,它们对强制转型和类型检查不会产生任何影响) - 与异常规约一起使用:
public String read() throws @Localized IOException
- 与通配符和类型边界一起使用:
List<@Localized ? extends Message>
,List<? extends @Localized Message>
- 与方法和构造器引用一起使用:
@Localized Message::getText
有多种类型位置是不能被注解的:
@NonNull String.class //error:cannot annotate class literal
import java.lang.@NonNull String; //error:Cannot annotate import
可以将注解放置到诸如private和static这样的其他修饰符的前面或后面。习惯(但不是必需)的做法,是将类型用法注解放置到其他修饰符的后面和将声明注解放置到其他修饰符的前面。例如,
private @NonNull String text;
@Id private String userId;
1.4、注解this
假设想要将参数注解为在方法中不会被修改:
public class Point {
public boolean equals(@ReadOnly Object other) {...}
}
那么,处理这个注解的工具类在看到下面的调用时
p.equals(q)
机会推理出q没有被修改过。
但是p呢?当方法被调用时,this变量是绑定到p的。但是this从来都没有被声明过,因此无法注解它。
实际上,可以用一种很少使用的语法变体来声明,这样就可以添加注解:
public class Point {
public bolean equals(@ReadOnly Point this, @ReadOnly Object other) {...}
}
第一个参数被成为接收器参数,它必须被命名为this,而它的类型就是要构建的类。
传递给内部类构造器的是另一个不同的隐藏参数,即对其外部类对象的引用。也可以让这个参数显示化:
public class Sequence {
private int from;
private int to;
Class Iterator implements java.util.Iterator<Integer> {
private int current;
public Iterator(@ReadOnly Sequence Sequence.this) {
this.current = Sequence.this.from;
}
...
}
...
}
2、标准注解
注解接口 | 应用场景 | 目的 |
---|---|---|
Deprecated | 全部 | 将项标记为过时的 |
SuppressWarnings | 除了包和注解之外的所有情况 | 阻止某个给定类型的警告信息 |
SafeVarargs | 方法和构造器 | 断言varargs参数可安全使用 |
Override | 方法 | 检查该方法是否覆盖了某一个超类方法 |
FunctionalInterface | 接口 | 将接口标记为只有一个抽象方法的函数式接口 |
PostConstruct/PreDestroy | 方法 | 被标记的方法应该在构造之后或移除之前立即被调用 |
Resource | 类、接口、方法、域 | 在类或接口上:标记为在其他敌方要用到的资源。在方法或域上:为"注入"而标记 |
Resources | 类、接口 | 一个资源数组 |
Generated | 全部 | |
Target | 注解 | 指明可以一个用这个注解的那些项 |
Retention | 注解 | 指明这个注解可以保留多久 |
Documented | 注解 | 指明这个注解应该包含在注解项的文档中 |
Inherited | 注解 | 指明当这个注解应用与一个类的时候,能够自动被它的子类继承 |
Repeatable | 注解 | 指明这个注解可以在同一个项上应用多次 |
2.1、用于编译的注解
2.1.1、@Deprecated
@Deprecated注解可以被添加到任何不再鼓励使用的项上。所以,当你使用一个已过时的项时,编译器将会发出警告。这个注解与Javadoc标签@deprecated具有同等功效。但是,该注解会一直持久化到运行时。
2.1.2、@SuppressWarnings
@SuppressWarnings注解会告知编译器阻止特定类型的警告信息,例如:
@SuppressWarnings("unchecked")
@SuppressWarnings常用的参数:
unchecked
:该参数用于抑制未经检查的警告,例如在使用泛型时类型安全性检查的警告。使用场景包括:- 在使用原生态类型时,如果确定代码逻辑没有问题,可以使用
@SuppressWarnings("unchecked")
来忽略类型安全的警告。 - 解决与遗留代码或第三方库集成时出现的泛型相关的警告。
- 在使用原生态类型时,如果确定代码逻辑没有问题,可以使用
deprecation
:该参数用于抑制使用已过时的API的警告。使用场景包括:- 在需要使用已过时的API的情况下,可以使用
@SuppressWarnings("deprecation")
来消除对该API产生的警告。 - 临时处理某个过时API的使用,但应该考虑尽快迁移到替代的非过时API。
- 在需要使用已过时的API的情况下,可以使用
rawtypes
:该参数用于抑制使用原始类型(Raw types)相关的警告。使用场景包括:- 在使用原始类型与泛型混合的代码中,可以使用
@SuppressWarnings("rawtypes")
来抑制原始类型的警告。
- 在使用原始类型与泛型混合的代码中,可以使用
unused
:该参数用于抑制未使用的代码或变量的警告。使用场景包括:- 在开发过程中,暂时有意不使用某个代码块或变量,可以使用
@SuppressWarnings("unused")
来消除未使用的警告。
- 在开发过程中,暂时有意不使用某个代码块或变量,可以使用
fallthrough
:该参数用于抑制在switch语句中缺失break语句而产生的警告。使用场景包括:- 在某些情况下,故意省略case后的break语句,可以使用
@SuppressWarnings("fallthrough")
来抑制相关警告。
- 在某些情况下,故意省略case后的break语句,可以使用
serial
:该参数用于抑制没有定义SerialVersionUID的类实现Serializable接口时的警告。使用场景包括:- 当不需要显示定义SerialVersionUID时,可以使用
@SuppressWarnings("serial")
来忽略警告。
- 当不需要显示定义SerialVersionUID时,可以使用
all
:该参数用于抑制所有类型的警告。使用场景包括:- 当需要忽略所有警告时,可以使用
@SuppressWarnings("all")
来一次性抑制所有警告。
- 当需要忽略所有警告时,可以使用
2.1.3、@Override
@Override这种注解只能应用到方法上。编译器会检查具有这种注解的方法是否真正覆盖了一个来自超类的方法。
例如,声明:
public MyClass {
@Override
public boolean equals(MyClass other);
}
那么编辑器会报告一个错误。因为,这个equals方法没有覆盖Object类的equals方法。
2.1.4、@Generated
@Generated注解的目的是供代码生成工具来使用。任何生成的源代码都可以被注解,从而与程序员提供的代码区分开。
例如,代码编辑器可以隐藏生成的代码,或者代码生成器可以移除生成代码的旧版本。每个注解都必须包含一个表示代码生成器的唯一标识符,而日期字符串(ISO8601)和注释字符串是可选的:
@Generated("com.horstmann.beanproperty", "2008-01-04T12:0856.35-0700");
2.2、用于管理资源的注解
2.2.1、@PostConstruct/@PreDestroy
@PostConstruct/@PreDestroy注解用于控制对象生命周期的环境中,例如Web容器和应用服务器。标记了这些注解的方法应该在对象被构建之后,或者在对象被移除之前,紧接着调用。
2.2.2、@Resource
@Resource注解用于资源注入。
例如,考虑一下访问数据库的Web应用。当然,数据库访问信息不应该被硬编码到Web应用中。而是应该让Web容器提供某种用户接口,以便设置连接参数和数据库资源的JNDI名字。在这个Web应用中,可以像下面这样引用数据源:
@Resource(name="jdbc/mydb")
private DataSource source;
当包含这个域的对象被构造时,容器会注入一个对该数据源的引用。
2.3、元注解
2.3.1、@Target
@Target元注解可以应用于一个注解,以限制该注解可以应用到哪些项上。
例如:
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface BugReport
Target的参数为枚举类型ElementType
,可以指定任意数量的元素类型,需要用括号括起来:
ANNOTATION_TYPE
:注解类型声明PACKAGE
:包TYPE
:类(包括enum)及接口(包括注解类型)METHOD
:方法CONSTRUCTOR
:构造器FIELD
:成员域(包括enum常量)PARAMETER
:方法或构造器参数LOCAL_VARIABLE
:局部变量TYPE_PARAMETER
:类型参数TYPE_USER
:类型用法
一条没有@Target限制的注解可以应用于任何项上。编译器将检查你是否将一条注解只应用到了某个允许的项上。例如,如果将@BugReport应用于一个成员域上,则会导致一个编译器错误。
2.3.2、@Retention
@Retention元注解用于指定一条注解应该保留多长时间,参数是枚举类型,默认为RetentionPolicy.CLASS
:
SOURCE
:不包括在类文件中的注解CLASS
:包括在类文件中的注解,按时虚拟机不需要将它们再入RUNTIME
:包括在类文件中的注解,并由虚拟机载入。通过反射API可获得它们。
2.3.3、@Documented
@Documented元注解为像Javadoc这样的归档工具提供了一些提示。
应该像处理其他修饰符(例如protected和static)一样来处理归档注解,以实现其归档目的。其他注解的使用并不会纳入归档的范畴。
2.3.4、@Inherited
@Inherited元注解只能应用于对类的注解。如果一个类具有继承注解,那么它的所有子类都自动具有同样的注解。这使得创建一个与Serializable这样的标记接口具有相同运行方式的注解变得很容易。
实际上,@Serializable注解应该比没有任何方法的Serializable标记接口更适用。一个类之所以可以被序列化,是因为存在着对它的成员域进行读写的运行期支持,而不是因为任何面向对象的设计原则。注解比接口继承更擅长描述这一事实。当然,可序列化接口是在JDK1.1中产生的,远比注解出现得早。
假设定义了一个继承注解@Persistent来指明一个类的对象可以存储到数据库中,那么该持久类的子类就会自动被注解为是持久性的:
@Inherited @interface Persistent{}
@Persistent calss Employee{...}
//Manager也是持久的
class Manager extents Employee{...}
2.3.5、@Repeatable
@Repeatable元注解指明这个注解可以在同一个项上应用多次。
对于Java SE8来说,将同种类型的注解多次应用于某一项是合法的。为了向后兼容,可重复注解的实现者需要提供一个容器注解,它可以将这些重复注解存储到一个数组中。
@Repeatable(TestCases.class)
public @interface TestCase {
String params();
String expected();
}
@interface TestCases {
TestCase[] value();
}
无论何时,只要提供了两个或更多个@TestCse注解,那么它们就会自动地被包装到一个@TestCases注解中。
3、处理注解
3.1、运行期处理
如果注解的@Retention为RetentionPolicy.RUNTIME
,这意味着注释会被编译器记录在类文件中,可以被VM在运行时保留,因此可以反射式读取。
3.1.1、示例一
比如:我们定义一个@Monitor注解,该注解用于标记在方法上,以统计该方法的执行时间:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitor {
}
被测试的类:
public class TestClass {
@Monitor
public void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a run");
}
@Monitor
public void b() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b run");
}
public void c() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("c run");
}
}
注解的处理类,通过反射拿到Method,判断是否有@monitor注解,然后进行时间统计:
public class PerformanceMonitor {
public static void monitor(Object object, Method method) throws InvocationTargetException, IllegalAccessException {
long start = System.currentTimeMillis();
Object result = method.invoke(object, null);
long end = System.currentTimeMillis();
System.out.println(method.getName() + "执行时间:" + (end - start) + "ms");
}
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
TestClass testClass = new TestClass();
Method[] methods = testClass.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Monitor.class)) {
PerformanceMonitor.monitor(testClass, method);
}
}
}
}
a run
a执行时间:1001ms
b run
b执行时间:2001ms
3.1.2、示例二
在用户界面编程中,很多时候都需要在事件源上添加鉴定器。很多监听器是下面这种形式的:
myButton.addActionListener(() -> doSomething());
这种方式多少有一些繁琐,所以我们定义一个注解,标注在自定义方法上,使用如下方式绑定监听器:
@ActionListenerFor(source="myButton")
public void doSomething() {
...
}
注解接口如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
String source();
}
注解处理类如下:
public class ActionListenerInstaller {
//处理传入类的注解
public static void processAnntations(Object obj) {
try {
Class<?> cl = obj.getClass();
for (Method m : cl.getDeclaredMethods()) {
if (m.isAnnotationPresent(ActionListenerFor.class)) {
//获取注解
ActionListenerFor a = m.getAnnotation(ActionListenerFor.class);
//获取source值对应的字段(Button)
Field f = cl.getDeclaredField(a.source());
f.setAccessible(true);
addListener(f.get(obj), obj, m);
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
public static void addListener(Object source, final Object param, final Method m) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//为ActionListener接口生成代理
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return m.invoke(param);
}
};
Object listener = Proxy.newProxyInstance(null, new Class[]{java.awt.event.ActionListener.class}, handler);
//listener绑定到Button
Method method = source.getClass().getMethod("addActionListener", ActionListener.class);
method.invoke(source, listener);
}
}
窗口:
public class ButtonFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private JPanel panel;
private JButton yellowButton;
private JButton blueButton;
private JButton redButton;
public ButtonFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
panel = new JPanel();
add(panel);
yellowButton = new JButton("Yellow");
blueButton = new JButton("Blue");
redButton =new JButton("Red");
panel.add(yellowButton);
panel.add(blueButton);
panel.add(redButton);
//绑定注解的方法与Button
ActionListenerInstaller.processAnntations(this);
}
@ActionListenerFor(source = "yellowButton")
public void yellowBackgroud() {
panel.setBackground(Color.YELLOW);
}
@ActionListenerFor(source = "blueButton")
public void blueBackgroud() {
panel.setBackground(Color.BLUE);
}
@ActionListenerFor(source = "redButton")
public void redBackgroud() {
panel.setBackground(Color.RED);
}
}
测试:
public class MainAPP {
public static void main(String[] args) {
ButtonFrame frame = new ButtonFrame();
frame.setVisible(true);
}
}
3.2、源码级处理
如果注解的@Retention为RetentionPolicy.SOURCE
,这意味着该注解仅在编译时保留。
一般用于自动处理源代码以产生更多的源代码、配置文件、脚本或IDE插件等其他任何我们想要生成的东西。
3.2.1、注解处理器
注解处理器通常通过扩展AbstracProcess
类而实现Processo接口。
public abstract class AbstractProcessor implements Processor {
protected ProcessingEnvironment processingEnv;
private boolean initialized = false;
protected AbstractProcessor() {}
public Set<String> getSupportedOptions();
public SourceVersion getSupportedSourceVersion();
public synchronized void init(ProcessingEnvironment processingEnv);
public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText);
protected synchronized boolean isInitialized();
private static Set<String> arrayToSet(String[] array) {
assert array != null;
Set<String> set = new HashSet<String>(array.length);
for (String s : array)
set.add(s);
return Collections.unmodifiableSet(set);
}
}
- public Set<String> getSupportedOptions():默认的实现是从注解
SupportedOptions
获取值,该值是一个字符数组,例如:
@SupportedOptions({"name","age"})
public class SzzTestProcessor extends AbstractProcessor {
}
不过貌似该接口并没有什么用处。
有资料表示 该可选参数可以从processingEnv获取到参数。
String resultPath = processingEnv.getOptions().get(参数);
实际上这个获取的参数是编译期通过入参 -Akey=name 设置的,跟getSupportedOptions没有什么关系。
- public Set getSupportedAnnotationTypes():获取当前的注解处理类能够处理哪些注解类型,默认实现是从SupportedAnnotationTypes注解里面获取; 注解值是个字符串数组 String [] ; 匹配上的注解,会通过当前的注解处理类的 process方法传入。
例如:使用*通赔hi走支持所有的注解
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
}
又或者可以直接重写这个接口
@Override
public ImmutableSet<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoService.class.getName());
}
最终他们生效的地方就是用来做过滤,因为处理的时候会获取到所有的注解,然后根据这个配置来获取自己能够处理的注解。
- public SourceVersion getSupportedSourceVersion():获取该注解处理器最大能够支持多大的版本,默认是从注解 SupportedSourceVersion中读取,或者自己重写方法,如果都没有的话 默认值是
RELEASE_6
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
}
或者重写(推荐 , 获取最新的版本)
@Override
public SourceVersion getSupportedSourceVersion() {
//设置为能够支持最新版本
return SourceVersion.latestSupported();
}
- public synchronized void init(ProcessingEnvironment processingEnv):init是初始化方法,这个方法传入了ProcessingEnvironment 对象。一般我们不需要去重写它,直接使用抽象类就行了。 当然你也可以根据自己的需求来重新
@Override
public synchronized void init(ProcessingEnvironment pe) {
super.init(pe);
System.out.println("SzzTestProcessor.init.....");
// 可以获取到编译器参数(下面两个是一样的)
System.out.println(processingEnv.getOptions());
System.out.println(pe.getOptions());
}
- public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):process方法提供了两个参数,第一个是我们请求处理注解类型的集合(也就是我们通过重写getSupportedAnnotationTypes方法所指定的注解类型),第二个是有关当前和上一次循环的信息的环境。
返回值表示这些注解是否由此 Processor 声明 如果返回 true,则这些注解不会被后续 Processor 处理; 如果返回 false,则这些注解可以被后续的 Processor 处理。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("SzzTestProcessor.process.....;");
return false;
}
我们可以通过RoundEnvironment接口获取注解元素,注意annotations只是注解类型,并不知道哪些实例被注解标记了,RoundEnvironment是可以知道哪些被注解标记了的。
方法 | 描述 |
---|---|
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) | 返回被指定注解类型注解的元素集合 |
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) | 返回被指定注解类型注解的元素集合 |
processingOver() | 如果循环处理完成返回true,否则返回false |
ProcessingEnvironment:
public interface ProcessingEnvironment {
//返回传递给注释处理工具的特定于 processor的选项
Map<String,String> getOptions();
//返回用来报告错误、警报和其他通知的Messager
Messager getMessager();
//用来创建新源、类或辅助文件的Filer
Filer getFiler();
//返回用来在元素上进行操作的某些实用工具方案的实现
//Elements是一个工具类,可以处理相关Element(包括ExecutableElement、PackageEement、TypeElement、TypeParameerElement、VariableElement)
Elements getElementUtils();
//返沪i用来在类上今习惯操作的某些使用工具方法的实现
Types getTypeUtils();
//返回任何生成的源和类文件应该复合的源版本
SourceVersion getSourceVersion();
//返回当前的语言环境;如果没有有效的语言环境,则返回null
Locale getLocale();
}
示例:
@SupportedAnnotationType("com.example.ToString")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ToStringAnnotationProcessor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnviroment currentRound) {
...
}
}
3.2.2、处理流程
注解处理流程由多轮完成。每一轮都从编译器在源文件中搜索注解并选择适合这些注解的 注解处理器(AbstractProcessor) 开始。每个注解处理器依次在相应的源上被调用。
如果在此过程中生成了任何文件,则将以生成的文件作为输入开始另一轮。这个过程一直持续到处理阶段没有新文件生成为止。
注解处理器的处理步骤:
- 在java编译器中构建
- 编译器开始执行未执行过的注解处理器
- 循环处理注解元素(Element),找到被该注解所修饰的类,方法,或者属性
- 生成对应的类,并写入文件
- 判断是否所有的注解处理器都已执行完毕,如果没有,继续下一个注解处理器的执行(回到步骤1)
3.2.3、注册注解处理器
并不是说我们实现了AbstractProcessor类就会生效,由于注解处理器(AbstractProcessor) 是在编译期执行的,而且它是作为一个Jar包的形式来生效,所以我们需要将注解处理器作为一个单独的Module来打包。 然后在需要使用到注解处理器的Module引用。
这个注解处理器 所在Module打包的时候需要注意:
因为AbstractProcessor本质上是通过ServiceLoader来加载的(SPI), 所以想要被成功注册上。则有两种方式
一、配置SPI:
在resource/META-INF.services
文件夹下创建一个名为javax.annotation.processing.Processor
的文件;里面的内容就是你的注解处理器的全限定类名。
服务配置文件不正确, 或构造处理程序对象javax.annotation.processing.Processor: Provider org.example.SzzTestProcessor not found时抛出异常错误
如果是用Maven编译的话,请加上如下配置:
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
注解处理器打包成功,就可以提供给别的Module使用了。
二、使用@AutoService 自动配置SPI的配置文件:
@AutoService 是Google开源的一个小插件,它可以自动的帮我们生成META-INF/services 的文件,也就不需要你去手动的创建配置文件了。
当然,上面的 <compilerArgument>-proc:none</compilerArgument>
参数也不需要了。
所以也就不会有编译期上述的问题xxx not found 问题了。因为编译的时候META-INF/services 还没有配置你的注解处理器,也就不会抛出加载异常了。
例如下面,使用@AutoService(Processor.class),他会自动帮我们生成对应的配置文件
@AutoService(Processor.class)
public class SzzBuildProcessor extends AbstractProcessor {
}
另外,实际上 @AutoService 自动生成配置文件也是通过AbstractProcessor来实现的。
注意:注解和注解处理器是单独的module:注解处理器只需要在编译的时候使用,注解的Module只需要引入注解处理器的Jar包就行了。因此我们需要将注解处理器分离为单独的module。并且打包的时候请先打包注解处理器的Module.自定义Processor类最终是通过打包成jar,在编译过程中调用的。
3.2.4、语言模型API
应该使用语言模型API来分析源码级的注解。与用来呈现类和方法的虚拟机表示形式的反射API不同,语言模型API让我们可以根据Java语言的规则去分析Java程序。
编译器会产生一棵树,其节点是实现了javax.lang.model.element.Element
接口及其TypeElement
、
VariableElement
、ExecutableElement
等子接口的类的实例。这些节点可以类比于编译时的Class
、
Field/Parament
和Method/Constructor
反射类。
- ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
- PackageElement:表示一个包程序元素。
- TypeElement:表示一个类或接口程序元素。
- TypeParameterElement:表示一般类、接口、方法或构造方法元素的形式类型参数。
- VariableElement:表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
RoundEnvironment通过调用下面的方法交给你一个由特定注解标注过的所有元素构成的集:
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)
在源码级别上等价于AnnotatedElement接口的是AnnotatedConstruct。使用下面的方法就可以获得属于给定注解类的单条注解或重复的注解:
A getAnnotation(Class<A> annotationType)
A[] etAnnotationsByType(Class<A> annotationType)
getEnclosedElements方法会返回封装此元素(非严格意义上)的最里层元素。
- 如果是PackageElement,则返回null
- 如果是TypeElement,则返回PackageElement
- 如果是VariableElement,则返回ExecutableElement
- 如果是TypeParameterElement,则返回TypeParameterElement
在Element上调用getSimpleName或在TypeElement上调用getQualifiedName会产生一个Name对象,它可以用toString方法转换为一个字符串。
3.2.5、示例
1. 需求描述:
假设我们的用户模块中有一些简单的 POJO 类,其中包含几个字段:
public class Company {
private String name;
private String email ;
}
public class Personal {
private String name;
private String age;
}
我们想创建对应的构建器帮助类来更流畅地实例化POJO类:
Company company = new CompanyBuilder()
.setName("ali").build();
Personal personal = new PersonalBuilder()
.setName("szz").build();
2. 实现方案:
如果每个POJO都要手动的去创建对应的Build构建器,未免太繁杂了,我们可以通过注解的形式,去自动的帮我们的POJO类生成对应的Build构建器,但是当然不是每个都生成,按需生成;
定义一个 @BuildProperty 注解,在需要生成对应的setXX方法的方法上标记注解,当自定义注解处理器扫描@BuildProperty注解,按照需求自动生成Build构建器。例如:
public class Company {
private String name;
private String email;
public String getName() {
return name;
}
@BuildProperty
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
@BuildProperty
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Company{" +
"name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
}
3. 定义注解:
创建一个注解处理器Module:annotation_processor_handler
,在其中创建注解接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuildProperty {
}
4. 注解处理器:
//只处理这个注解
@SupportedAnnotationTypes("pers.zhang.BuildProperty")
public class MyBuildProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//虽然是循环遍历,但是我们只支持BuildProperty一个注解
for (TypeElement annotation : annotations) {
//获取所有被该注解标记过的实例
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
//按照需求 检查注解使用的是否正确 以set开头,并且参数只有一个
//按照是否复合需求分类收集
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
Collectors.partitioningBy(element ->
((ExecutableType)element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")
)
);
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
//打印注解使用错误的case
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty 注解必须放到方法上并且是set开头的单参数方法", element));
if (setters.isEmpty()) {
continue;
}
Map<String, List<Element>> groupMap = new HashMap<>();
//按照全限定类名分组,一个组内是标记了BuildProperty的方法集合,一个类创建一个Builder
setters.forEach(setter -> {
//全限定类名
String className = ((TypeElement) setter.getEnclosingElement()).getQualifiedName().toString();
List<Element> elements = groupMap.get(className);
if (elements != null) {
elements.add(setter);
} else {
List<Element> newElements = new ArrayList<>();
newElements.add(setter);
groupMap.put(className, newElements);
}
});
groupMap.forEach((groupSetterKey, groupSetterValue) -> {
//获取类名SimpleName和set方法的入参
Map<String, String> setterMap = groupSetterValue.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType()).getParameterTypes().get(0).toString()
));
try {
//组装XXXBuild类。并创建对应的类文件
writeBuilderFile(groupSetterKey, setterMap);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
//返回false表示单签处理器处理了之后,其他处理器也可以接着处理。处理true表示,其它处理器不再处理。
return true;
}
private void writeBuilderFile(String className, Map<String, String> setterMap) throws IOException {
//包名
String packageName = null;
int lastDot= className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0,lastDot);
}
//简单类名
String simpleClassName = className.substring(lastDot + 1);
//Builder类名
String builderClassName = className + "Builder";
//Builder的简单类名
String builderSimpleClassName = builderClassName.substring(lastDot + 1);
//创建Builder类文件对象
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
//package packageName;\n
//\n
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
//public class builderSimpleClassName {\n
out.print("public class ");
out.print(builderSimpleClassName);
out.print(" {");
out.println();
// private simpleClassName = new simpleClassName();\n
//\n
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
// public simpleClassName build() {\n
// return object;\n
// }\n
//\n
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
//逐个方法构建
setterMap.entrySet().forEach(setter -> {
String methodName = setter.getKey();
String argumentType = setter.getValue();
// public builderSimpleClassName methodName(argumentType value) {\n
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
// object.methodName(value);\n
out.print(" object.");
out.print(methodName);
out.println("(value);");
// return this;\n
// }\n
//\n
out.println(" return this;");
out.println(" }");
out.println();
});
//}\n
out.println("}");
}
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
System.out.println("---------------------------");
System.out.println(processingEnv.getOptions());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
5. 注册注解处理器:
在resources
下创建META-INF/services
文件夹,再创建javax.annotation.processing.Processor
文件。
文件中的内容是需要注册的注解处理器全限定类名:
6. 配置编译参数:
因为这里选择的是手动配置了 META-INF.services; 所以我们需要配置一下编译期间忽略Processor; 主要参数就是
<compilerArgument>-proc:none</compilerArgument>
如下所示:
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
7. 执行编译打包:
mvn install一下, 其他Module就可以引用了。
8. 使用:
创建一个新的Module:annotation_processor_test
,引用上面的模块:
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>annotation_processor_handler</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
创建实体:
public class Company {
private String name;
private String email;
public String getName() {
return name;
}
@BuildProperty
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
@BuildProperty
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Company{" +
"name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
}
public class Personal {
private String name;
private Integer age;
public String getName() {
return name;
}
@BuildProperty
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
@BuildProperty
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Personal{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
annotation_processor_test
编译之后,就会在target文件夹生成BuildXXX类。 并且只有我们用注解BuildProperty标记了的方法才会生成对应的方法。 而且如果注解BuildProperty使用的方式不对,我们也会打印出来了异常。
生成的Builder类如下:
测试:
public class MainTest {
public static void main(String[] args) {
Company company = new CompanyBuilder()
.setEmail("222@qq.com")
.setName("tom")
.build();
Personal personal = new PersonalBuilder()
.setAge(10)
.setName("jerry")
.build();
System.out.println(company);
System.out.println(personal);
}
}
Company{name='tom', email='222@qq.com'}
Personal{name='jerry', age=10}
3.3、字节码处理
如果注解的@Retention为RetentionPolicy.CLASS
,这意味着该注解会保留在class文件中,单运行时不会保存。所以可以在字节码级别上进行处理。
class文件是归档过的,这种格式相当复杂,并且在特殊类库的支持的情况下,处理类文件具有很大的挑战性。一般使用ASM库进行处理。
3.3.1、示例
使用ASM向已注解方法中添加日志信息。如果一个方法被这样注解过:
@LogEntry(logger=loggerName)
那么,在方法的开头部分,将添加下面这条语句的字节码:
Logger.getLogger(loggerName).entering(className, methodName);
例如,如果对Item类的hashCode方法做了如下注解:
@LogEntry(logger="global")
public int hashCode()
那么,在任何时候调用该方法,都会报告一条与下面打印出来的消息相似的消息:
十一月 10, 2023 4:17:02 下午 pers.zhang.asm.Item hashCode
较详细: ENTRY
为了实现这项需求,需要遵循下面几点:
- 加载类文件中的字节码
- 定位所有的方法
- 对于每个方法,检查它是不是有一个LogEntry注解
- 如果有,在方法开头部分添加下面所列指令的字节码:
ldc loggerName
invokestatic
java/utillogging/Logger.getLogger:(Ljava/lang/Sting;)Ljava/util/logging/Logger;
ldc className
ldc methodName
invokevirtual
java/util/logging/Logger.entering:(Ljava/lang/String;Ljava/lang/String;)V
1. 定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface LogEntry {
String logger();
}
2. 引入ASM:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<asm.version>9.0</asm.version>
</properties>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-analysis</artifactId>
<version>${asm.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Java Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<fork>true</fork>
<compilerArgs>
<arg>-g</arg>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
3. 测试用的类:
public class Item {
private String description;
private int partNumber;
public Item(String description, int partNumber) {
this.description = description;
this.partNumber = partNumber;
}
public String getDescription() {
return description;
}
public String toString() {
return "[description=" + description + ",partNumber=" + partNumber + "]";
}
@LogEntry(logger = "pers.zhang.asm")
public boolean equals(Object otherObject) {
if (this == otherObject)
return true;
if (otherObject == null)
return false;
if (getClass() != otherObject.getClass())
return false;
Item other = (Item)otherObject;
return Objects.equals(description, other.description) && partNumber == other.partNumber;
}
@LogEntry(logger = "pers.zhang.asm")
public int hashCode() {
return Objects.hash(description, partNumber);
}
}
4. 处理字节码工具类:
public class EntryLogger extends ClassVisitor {
private String className;
public EntryLogger(ClassWriter writer, String className) {
super(Opcodes.ASM9, writer);
this.className = className;
}
public MethodVisitor visitMethod(int access, final String methodName, String desc,
String signatrue, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, methodName, desc, signatrue, exceptions);
return new AdviceAdapter(Opcodes.ASM9, mv, access, methodName, desc) {
private String loggerName;
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return new AnnotationVisitor(Opcodes.ASM9) {
@Override
public void visit(String name, Object value) {
if (desc.equals("Lpers/zhang/asm/LogEntry;") && name.equals("logger")) {
loggerName = value.toString();
}
}
};
}
@Override
protected void onMethodEnter() {
if (loggerName != null) {
visitLdcInsn(loggerName);
visitMethodInsn(INVOKESTATIC, "java/util/logging/Logger", "getLogger",
"(Ljava/lang/String;)Ljava/util/logging/Logger;", false);
visitLdcInsn(className);
visitLdcInsn(methodName);
visitMethodInsn(INVOKEVIRTUAL, "java/util/logging/Logger", "entering",
"(Ljava/lang/String;Ljava/lang/String;)V", false);
loggerName = null;
}
}
};
}
public static void main(String[] args) throws IOException {
String str = "target/classes/pers/zhang/asm/Item.class";
ClassReader reader = new ClassReader(new FileInputStream(str));
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
EntryLogger entryLogger = new EntryLogger(writer, "pers.zhang.asm.Item");
reader.accept(entryLogger, ClassReader.EXPAND_FRAMES);
Files.write(Paths.get(str), writer.toByteArray());
}
}
运行main方法,可以看到class文件已经重新生成:
5. 测试:
public class SetTest {
public static void main(String[] args) {
Logger.getLogger("pers.zhang.asm").setLevel(Level.FINEST);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.FINEST);
Logger.getLogger("pers.zhang.asm").addHandler(handler);
HashSet<Item> parts = new HashSet();
parts.add(new Item("Toaster", 1279));
parts.add(new Item("Microwave", 4104));
parts.add(new Item("Toaster", 1279));
System.out.println(parts);
}
}
十一月 10, 2023 4:17:02 下午 pers.zhang.asm.Item hashCode
较详细: ENTRY
十一月 10, 2023 4:17:02 下午 pers.zhang.asm.Item hashCode
较详细: ENTRY
十一月 10, 2023 4:17:02 下午 pers.zhang.asm.Item hashCode
较详细: ENTRY
十一月 10, 2023 4:17:02 下午 pers.zhang.asm.Item equals
较详细: ENTRY
[[description=Microwave,partNumber=4104], [description=Toaster,partNumber=1279]]