本文将从以下几点为你介绍java注解以及如何自定义
- 引言
- 注解定义
- 注解意义
- 注解分类
- 自定义
- 结束语
引言
Java注解在日常开发中经常遇到,但通常我们只是用它,难道你不会好奇注解是怎么实现的吗?为什么@Data的注解可以生成getter和setter呢?为什么@BindView可以做到不需要findViewById呢?为什么retrofit2只要写个接口就可以做网络请求呢?本文将为你一一解答其中的奥妙。另外注解依赖于反射,我相信绝大多数的Java开发者都写过反射,也都知道反射是咋回事,所以如果你还不理解反射,请先花几分钟熟悉后再阅读本文。
注解定义
Annotation也叫元数据,是代码层面的说明。它在JDK1.5以后被引入,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。我们可以简单的理解注解只是一种语法标注,它是一种约束、标记。
注解意义
Java引入注解是想把某些元数据做到与代码强耦合。因为在JDK1.5以前,描述元数据都是使用xml的,但是xml总是表现出松耦合,导致文件过多时难以维护。比如Spring早期的注入xml,如今2.0大多转为注解注入。
虽然注解做到了强耦合,但是一些常量参数使用xml会显结构更加清晰,所以在日常使用时,总是会把xml和Annotation结合起来使用以达到最优使用。
注解分类
通常我们会按照注解的运行机制将其分类,但是在按照注解的运行机制分类之前,我们先按基本分类来看一遍注解。
基本分类
- 内置注解(基本注解)
内置注解只有三个,位于java.lang包下,他们分别是- @Override - 检查该方法是否是重载方法,如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated - 标记过时方法,如果使用该方法,会报编译警告。
- @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
- 元注解
元注解只有四个,位于java.lang.annotation包中- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问
- @Documented - 标记这些注解是否包含在用户文档中
- @Target - 标记这个注解应该是哪种 Java 成员
- ElementType.CONSTRUCTOR 构造方法声明
- ElementType.FIELD 字段声明
- ElementType.LOCAL_VARIABLE 局部变量声明
- ElementType.METHOD 方法声明
- ElementType.PACKAGE 包声明
- ElementType.PARAMETER 参数声明
- ElementType.TYPE 类、接口方法
- @Inherited - 标记注解可继承
- 自定义注解
可以通过元注解来自定义
运行机制分类
主要是按照元注解中的@Retention参数将其分为三类
- 源码注解(@Retention(RetentionPolicy.SOURCE))
注解只在源码中存在,编译时丢弃,编译成.class文件就不存在了 - 编译时注解(@Retention(RetentionPolicy.CLASS))
编译时会记录到.class中,运行时忽略 - 运行时注解(@Retention(RetentionPolicy.RUNTIME))
运行时存在起作用,影响运行逻辑,可以通过反射读取
我们可以简单的把Java程序从源文件创建到程序运行的过程看作为两大步骤
- 源文件由编译器编译成字节码
- 字节码由java虚拟机解释运行
那么被标记为RetentionPolicy.SOURCE的注解只能保留在源码级别,即最多只能在源码中对其操作,被标记为RetentionPolicy.CLASS的被保留到字节码,所以最多只到字节码级别操作,那么对应的RetentionPolicy.RUNTIME可以在运行时操作。
自定义
前面的都是司空见惯的知识点,可能大家都知道或有有了解过,但是一说到自定义,估计够呛,那么下面就按运行机制的分类,每一类都自定义一个注解看下注解到底是怎么回事。
RetentionPolicy.RUNTIME
运行时注解通常需要先通过类的实例反射拿到类的属性、方法等,然后再遍历属性、方法获取位于其上方的注解,然后就可以做相应的操作了。
比如现在有一个Person的接口以及其实现类Student。
public interface Person {
@PrintContent("来自注解 PrintContent 唱歌")
void sing(String value);
@PrintContent("来自注解 PrintContent 跑步")
void run(String value);
@PrintContent("来自注解 PrintContent 吃饭")
void eat(String value);
@PrintContent("来自注解 PrintContent 工作")
void work(String value);
}
实现类
public class Student implements Person {
@Override
public void sing(String value) {
System.out.println(value == null ? "这是音乐课,我们在唱歌" : value);
}
@Override
public void run(String value) {
System.out.println(value == null ? "这是体育课,我们在跑步" : value);
}
@Override
public void eat(String value) {
System.out.println(value == null ? "中午我们在食堂吃饭" : value);
}
@Override
public void work(String value) {
System.out.println(value == null ? "我们的工作是学习" : value);
}
}
执行逻辑
@Autowired
private Person person;
public void student() {
person.eat(null);
person.run(null);
person.sing(null);
person.work(null);
}
我们想
- 自定义属性注解@Autowired表示自动注入Student对象给person。
- 提供方法注解@PrintContent表示当value为null时打印的默认值,并在调用的方法前后插入自己想做的操作。
因为这是一个运行时的注解,所以我们需要反射先拿到这个注解,然后再对其进行操作。
Autowired的实现,可以看到非常简单,仅仅是先拿到类的所有属性,然后对其遍历,发现属性使用了Autowired注解并且是Person类型,那么就new一个Student为其赋值,这样就做到了自动注入的效果。
private static void inject(Object obj) {
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field field : declaredFields) {
if (field.getType() == Person.class) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true);
try {
Person student = new Student();
field.set(obj, student);
} catch (IllegalAccessException e) {
e.printStackTrace();
}}}
}}
如果是想当value为null时打印的注解的默认值,并在调用的方法前后插入自己想做的操作。这种对接口方法进行拦截并操作的称为动态代理,java提供Proxy.newProxyInstance支持。
private static void inject(Object obj) {
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field field : declaredFields) {
if (field.getType() == Person.class) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true);
try {
Person student = new Student();
Class> cls = student.getClass();
Person person = (Person) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), new DynamicSubject(student));
field.set(obj, person);
} catch (IllegalAccessException e) {
e.printStackTrace();
}}}
}}
其中需要自定义DynamicSubject,即对方法的真实拦截操作。这样就会把@PrintContent注解的值作为参数打印处理。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isAnnotationPresent(PrintContent.class)) {
PrintContent printContent = method.getAnnotation(PrintContent.class);
System.out.println(String.format("----- 调用 %s 之前 -----", method.getName()));
method.invoke(object, printContent.value());
System.out.println(String.format("----- 调用 %s 之后 -----\n", method.getName()));
return proxy;
}
return null;
}
最后附上两个自定义的注解
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrintContent {
String value();
}
以上可以帮助你对retorfit2的理解,因为动态代理就是它的核心之一。如果你想学习更多,可以参考仿retorfit2设计实现的Activity参数注入
RetentionPolicy.SOURCE
大家或许对@BindView生成代码有所怀疑,对@Data如何生成代码有所好奇,那么我们就自定义个@Data来看看怎么注解是怎么生成代码的。本文依赖于idea工具,并非google提供的@AutoService实现。
bean是开发中经常被使用的,getter、setter方法是被我们所厌弃写的,idea帮我们做了一键生成的插件工具,但是如果这一步都都不想操作呢?我只想用一个注解生成,比如下面的User类。
@Data
public class User {
private Integer age;
private Boolean sex;
private String address;
private String name;
}
通过@Data就可以生成这样的类,是不是很神奇?
public class User{
private Integer age;
private Boolean sex;
private String address;
private String name;
public User() {
}
public Integer getAge() {
return this.age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean hasSex() {
return this.sex;
}
public void isSex(Boolean sex) {
this.sex = sex;
}
public String getAddress() {
return this.address;
}
public void setAddress(String address) {
this.address = address;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
要实现这样的神奇操作,大概的思路是先自定义某个RetentionPolicy.SOURCE级别的注解,然后实现一个注解处理器并设置SupportedAnnotationTypes为当前的注解,最后在META-INF注册该注解处理器。所以首先我们定义Data注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Data {
}
然后需要自定义注解处理器来处理Data注解
@SupportedAnnotationTypes("com.data.Data")
public class DataProcessor extends AbstractProcessor {
}
然后在resources/META-INF文件夹下新建services文件夹,如果没有META-INF也新建。然后在services文件夹里新建javax.annotation.processing.Processor文件,注意名字是固定的,打开文件后写上前面定义的注解处理器全称,比如com.data.DataProcessor,这样就表示该注解处理器被注册了。
待程序要执行的时候,编译器会先读取这里的文件然后扫描整个工程,如果工程中有使用已注册的注解处理器中的SupportedAnnotationTypes里的注解,那么就会执行对应的注解处理中的process方法。下面重点处理注解处理器AbstractProcessor,把大部分的解释都写在注解里。
@SupportedAnnotationTypes("com.data.Data")
public class DataProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "-----开始自动生成源代码");
try {
// 返回被注释的节点
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(Data.class);
for (Element e : elements) {
// 如果注释在类上
if (e.getKind() == ElementKind.CLASS && e instanceof TypeElement) {
TypeElement element = (TypeElement) e;
// 类的全限定名
String classAllName = element.getQualifiedName().toString() + "New";
// 返回类内的所有节点
List extends Element> enclosedElements = element.getEnclosedElements();
// 保存字段的集合
Map fieldMap = new HashMap<>();
for (Element ele : enclosedElements) {
if (ele.getKind() == ElementKind.FIELD) {
//字段的类型
TypeMirror typeMirror = ele.asType();
//字段的名称
Name simpleName = ele.getSimpleName();
fieldMap.put(simpleName, typeMirror);
}
}
// 生成一个Java源文件
String targetClassName = classAllName;
if (classAllName.contains(".")) {
targetClassName = classAllName.substring(classAllName.lastIndexOf(".") + 1);
}
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(targetClassName);
// 写入代码
createSourceFile(classAllName, fieldMap, sourceFile.openWriter());
} else {
return false;
}
}
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "-----完成自动生成源代码");
return true;
}
/**
* 属性首字母大写
*/
private String humpString(String name) {
String result = name;
if (name.length() == 1) {
result = name.toUpperCase();
}
if (name.length() > 1) {
result = name.substring(0, 1).toUpperCase() + name.substring(1);
}
return result;
}
private void createSourceFile(String className, Map fieldMap, Writer writer) throws IOException {
// 检查属性是否以"get", "set", "is", "has“这样的关键字开头,如果有这样的 属性就报错
String[] errorPrefixes = {"get", "set", "is", "has"};
for (Map.Entry map : fieldMap.entrySet()) {
String name = map.getKey().toString();
for (String prefix : errorPrefixes) {
if (name.startsWith(prefix)) {
throw new RuntimeException("Properties do not begin with 'get'、'set'、'is'、'has' in " + name);
}
}
}
String packageName;
String targetClassName = className;
if (className.contains(".")) {
packageName = className.substring(0, className.lastIndexOf("."));
targetClassName = className.substring(className.lastIndexOf(".") + 1);
} else {
packageName = "";
}
// 生成源代码
JavaWriter jw = new JavaWriter(writer);
jw.emitPackage(packageName);
jw.beginType(targetClassName, "class", EnumSet.of(Modifier.PUBLIC));
jw.emitEmptyLine();
for (Map.Entry map : fieldMap.entrySet()) {
String name = map.getKey().toString();
String type = map.getValue().toString();
//字段
jw.emitField(type, name, EnumSet.of(Modifier.PRIVATE));
jw.emitEmptyLine();
}
for (Map.Entry map : fieldMap.entrySet()) {
String name = map.getKey().toString();
String type = map.getValue().toString();
String prefixGet = "get";
String prefixSet = "set";
if (type.equals("java.lang.Boolean")) {
prefixGet = "has";
prefixSet = "is";
}
//getter
jw.beginMethod(type, prefixGet + humpString(name), EnumSet.of(Modifier.PUBLIC))
.emitStatement("return " + name)
.endMethod();
jw.emitEmptyLine();
//setter
jw.beginMethod("void", prefixSet + humpString(name), EnumSet.of(Modifier.PUBLIC), type, name)
.emitStatement("this." + name + " = " + name)
.endMethod();
jw.emitEmptyLine();
}
jw.endType().close();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
至此整个Data的注解生成器就写完了,由于我们的idea没有对应的插件帮助我们做一些操作,所以生成的类编译生成字节码时会报已存在异常,所以如果你想更进一步,可以考虑写个插件,本文仅限注解,未提供这样的插件。
以上完成后,只要在data class头添加@Data注解,在编译程序之前,就可以看到target/classes中存在了我们生成的代码。至此你应该了解@BindView或者@Data的原理,或许你也可以尝试自定义一个ButterKnife框架。
RetentionPolicy.CLASS
字节码级别的实际上对应用程序员来说没多大作用,因为此类注解一般是在字节码文件上进行操作,我们一般理解整个过程是在.java编译为.class后在将要被加载到虚拟机之前。那么很显然RetentionPolicy.CLASS类别的注解是直接修改字节码文件的。所以一般用此注解需要底层开发人员的配合,或者当你需要造轮子了可以考虑用一下,不过需要ASM的配合来使用的,如果仅仅是开发应用基本用不到。
不过这里还是介绍下它的使用,先造场景:现在有个People类,其中有个size属性等于9,观察到上方的类注解是@Prinln(12),现在想在字节码层面把size的9替换为12。
@Prinln(12)
public class People {
int size = 9;
double phone = 12.0;
Boolean sex;
String name;
}
由于我们并不知道字节码文件是怎么写的,所以需要先通过Show Bytecode的插件来查看类的字节码是啥样子的
// class version 52.0 (52)
// access flags 0x21
public class com/People {
// compiled from: People.java
@Lcom/ann/Prinln;(value=12) // invisible
// access flags 0x0
I size
// access flags 0x2
private D phone
// access flags 0x2
private Ljava/lang/Boolean; sex
// access flags 0x2
private Ljava/lang/String; name
// access flags 0x1
public ()V
L0
LINENUMBER 12 L0
ALOAD 0
INVOKESPECIAL java/lang/Object. ()V
L1
LINENUMBER 14 L1
ALOAD 0
BIPUSH 9
PUTFIELD com/People.size : I
L2
LINENUMBER 16 L2
ALOAD 0
LDC 12.0
PUTFIELD com/People.phone : D
RETURN
L3
LOCALVARIABLE this Lcom/People; L0 L3 0
MAXSTACK = 3
MAXLOCALS = 1
}
虽然知道了是这样的,但还是看不懂啊,咋整呢?没关系,我们看ASMified。ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。而且ASMified我们是可以看得懂的,虽然没学过,但是很好理解。比如上面的People的ASMified就是这样的。
public class PeopleDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/People", null, "java/lang/Object", null);
cw.visitSource("People.java", null);
{
av0 = cw.visitAnnotation("Lcom/ann/Prinln;", false);
av0.visit("value", new Integer(12));
av0.visitEnd();
}
{
fv = cw.visitField(0, "size", "I", null, null);
fv.visitEnd();
}
{
fv = cw.visitField(ACC_PRIVATE, "phone", "D", null, null);
fv.visitEnd();
}
{
fv = cw.visitField(ACC_PRIVATE, "sex", "Ljava/lang/Boolean;", null, null);
fv.visitEnd();
}
{
fv = cw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(12, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(14, l1);
mv.visitVarInsn(ALOAD, 0);
mv.visitIntInsn(BIPUSH, 9);
mv.visitFieldInsn(PUTFIELD, "com/People", "size", "I");
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(16, l2);
mv.visitVarInsn(ALOAD, 0);
mv.visitLdcInsn(new Double("12.0"));
mv.visitFieldInsn(PUTFIELD, "com/People", "phone", "D");
mv.visitInsn(RETURN);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLocalVariable("this", "Lcom/People;", null, l0, l3, 0);
mv.visitMaxs(3, 1);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
这样的话,我们大概可以知道如果要改size=12,就只要把mv.visitIntInsn(BIPUSH, 9);这句话的9改为12就好了。不过ASM在原有的字节码文件中插入或删除或更改,本文未仔细研究。本文是简单粗暴的用新的字节码替换原字节码文件。
public void asm() {
try {
ClassReader classReader = new ClassReader(new FileInputStream("target/classes/com/People.class"));
ClassNode classNode = new ClassNode();
classReader.accept(classNode, ClassReader.SKIP_DEBUG);
System.out.println("Class Name: " + classNode.name);
AnnotationNode anNode = null;
if (classNode.invisibleAnnotations.size() == 1) {
anNode = classNode.invisibleAnnotations.get(0);
System.out.println("Annotation Descriptor : " + anNode.desc);
System.out.println("Annotation attribute pairs : " + anNode.values);
}
File file = new File("target/classes/com/People.class");
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(copyFromBytecode(anNode == null ? 0 : (int) anNode.values.get(1)));
} catch (IOException e) {
e.printStackTrace();
}
People people = new People();
System.out.println("people : " + people.size);
}
private byte[] copyFromBytecode(int value) {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(52, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, "com/People", null, "java/lang/Object", null);
cw.visitSource("People.java", null);
{
av0 = cw.visitAnnotation("Lcom.ann.Prinln;", false);
av0.visit("value", new Integer(12));
av0.visitEnd();
}
{
fv = cw.visitField(0, "size", "I", null, null);
fv.visitEnd();
}
{
fv = cw.visitField(0, "phone", "D", null, null);
fv.visitEnd();
}
{
fv = cw.visitField(0, "sex", "Ljava/lang/Boolean;", null, null);
fv.visitEnd();
}
{
fv = cw.visitField(0, "name", "Ljava/lang/String;", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(10, l0);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(12, l1);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitIntInsn(Opcodes.BIPUSH, value);
mv.visitFieldInsn(Opcodes.PUTFIELD, "com/People", "size", "I");
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(14, l2);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitLdcInsn(new Double("12.0"));
mv.visitFieldInsn(Opcodes.PUTFIELD, "com/People", "phone", "D");
mv.visitInsn(Opcodes.RETURN);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLocalVariable("this", "Lcom/People;", null, l0, l3, 0);
mv.visitMaxs(3, 1);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
实际上RetentionPolicy.CLASS的使用,ASM的配合很重要,所以当你在自己的框架中需要使用这种类型的注解的时候,建议还是学好ASM再尝试写此类注解,而不是像我这样全部替换。
结束语
本文注解虽然讲解的多,但是如果你能看完到这里,相信通过了几个例子的描述,你已经知道了几类注解的基本操作过程,已经让你对各种类型的注解有基本的认识,或许看了本文你真的理解了retrofit2、ButterKnife,甚至可以自己写个简单的retrofit2或ButterKnife的框架,那就再好不过了。
最后附上源码,感谢阅读。