本文将从如下几点为你介绍java注解以及如何自定义java
Java注解在平常开发中常常遇到,但一般咱们只是用它,难道你不会好奇注解是怎么实现的吗?为何@Data的注解能够生成getter和setter呢?为何@BindView能够作到不须要findViewById呢?为何retrofit2只要写个接口就能够作网络请求呢?本文将为你一一解答其中的奥妙。另外注解依赖于反射,我相信绝大多数的Java开发者都写过反射,也都知道反射是咋回事,因此若是你还不理解反射,请先花几分钟熟悉后再阅读本文。git
Annotation也叫元数据,是代码层面的说明。它在JDK1.5之后被引入,与类、接口、枚举是在同一个层次。它能够声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。咱们能够简单的理解注解只是一种语法标注,它是一种约束、标记。程序员
在JDK1.5之前,描述元数据都是使用xml的,可是xml老是表现出松散的关联度,致使文件过多时难以维护,好比Spring早期的注入xml。所以Java1.5引入注解最大的缘由就是想把元数据与代码强关联,好比Spring 2.0大多转为注解注入。 虽然注解作到了强关联,可是一些常量参数使用xml会显结构更加清晰,因此在平常使用时,老是会把xml和Annotation结合起来使用以达到最优使用。github
一般咱们会按照注解的运行机制将其分类,可是在按照注解的运行机制分类以前,咱们先按基本分类来看一遍注解。bash
主要是按照元注解中的@Retention参数将其分为三类markdown
咱们能够简单的把Java程序从源文件建立到程序运行的过程看做为两大步骤网络
那么被标记为RetentionPolicy.SOURCE的注解只能保留在源码级别,即最多只能在源码中对其操做,被标记为RetentionPolicy.CLASS的被保留到字节码,因此最多只到字节码级别操做,那么对应的RetentionPolicy.RUNTIME能够在运行时操做。app
前面的都是司空见惯的知识点,可能你们都知道或有有了解过,可是一说到自定义,估计够呛,那么下面就按运行机制的分类,每一类都自定义一个注解看下注解究竟是怎么回事。框架
运行时注解一般须要先经过类的实例反射拿到类的属性、方法等,而后再遍历属性、方法获取位于其上方的注解,而后就能够作相应的操做了。 好比如今有一个Person的接口以及其实现类Student。ide
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的实现,能够看到很是简单,仅仅是先拿到类的全部属性,而后对其遍历,发现属性使用了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参数注入
你们或许对@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<Name, TypeMirror> 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<Name, TypeMirror> fieldMap, Writer writer) throws IOException { // 检查属性是否以 "get", "set", "is", "has" 这样的关键字开头,若是有这样的 属性就报错 String[] errorPrefixes = {"get", "set", "is", "has"}; for (Map.Entry<Name, TypeMirror> 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<Name, TypeMirror> 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<Name, TypeMirror> 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框架。
字节码级别的实际上对应用程序员来讲没多大做用,由于此类注解通常是在字节码文件上进行操做,咱们通常理解整个过程是在.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 <init>()V
L0
LINENUMBER 12 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()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, "<init>", "()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", "<init>", "()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, "<init>", "()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", "<init>", "()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再尝试写此类注解,而不是像我这样所有替换。
本文注解虽然讲解的多,可是若是你能看完到这里,相信经过了几个例子的描述,你已经知道了几类注解的基本操做过程,已经让你对各类类型的注解有基本的认识,或许看了本文你真的理解了retrofit二、ButterKnife,甚至能够本身写个简单的retrofit2或ButterKnife的框架,那就再好不过了。 最后附上源码,感谢阅读。
本文版权属于再惠研发团队,欢迎转载,转载请保留出处。@Dpuntu