教你如何使用注解

277 阅读6分钟

一. 综述

  1. 注解其实是一个高级版的注释,如果没有解析它,那么它的代码并不比注释高级多少。

  2. 解析注解的方法:分为编译期扫码和运行期反射;我们可以利用apt技术在编译期去动态生成代码,利用反射在运行期去调用代码。(apt 全称annotation processing tool 注解处理工具,)

  3. 注解的代价:侵入式编程,增加了耦合性。反射破坏了封装。更难以debug

  4. 元注解:在注解类上面配置一些参数。元注解是利用apt tools 技术来实现的。其中有一个@Retention 表示注解的生命周期三种, source表示源文件中有效,例如Overrite 。class 会保存在class中,不会加载进JVM。runtime 在运行时一直都有效,可以通过反射来获取其注解。比如 在使用Retrofit 你传给它一个apiService里面定义了接口地址,它通过反射来取数据,例如是@GET or @POST 。

二. 如何使用

当使用注解在运行时获取注解的属性。

第一个例子

2.1 [-> DeprecatedTracker.java]

public static void trackDeprecated(Class<?> cl) {
    
    Field[] fields = cl.getDeclaredFields();
    for (Field field : fields) {
        //读取到带注解的字段,如果使用注解就返回true【见小节3.1】
        if (field.getAnnotation(Deprecated.class) != null) {
            System.out.println("field: " + field);
        }
    }

    Method[] methods = cl.getDeclaredMethods();
    for (Method method : methods) {
        //读取到带注解的方法 
        if (method.getAnnotation(Deprecated.class) != null) {
            System.out.println("method: " + method);
        }
    }

    Constructor<?>[] constructors = cl.getDeclaredConstructors();
    for (Constructor<?> constructor : constructors) {
        //读取到带注解的构造方法 
        if (constructor.getAnnotation(Deprecated.class)!=null){
            System.out.println("constructor: "+constructor);
        }
    }
}

public static void main(String[] args) {
    DeprecatedTracker.trackDeprecated(VideoItem.class);//类【见小节2.2】
}

打印结果:

field: private java.lang.String com.company.annotation.VideoItem.id
method: public void com.company.annotation.VideoItem.setId(java.lang.String)
constructor: public com.company.annotation.VideoItem(java.lang.String)

2.2 [-> VideoItem.java]

public class VideoItem {

    @Deprecated
    private String id;

    private String videoName;

    private String videoUrl;

    @Deprecated
    public VideoItem(String id) {
        this.id = id;
    }

    public VideoItem() {
    }

    @Deprecated
    public void setId(String id) {
        this.id = id;
    }
}

第二个例子

2.3 [-> RegexValidTest.java]

public class RegexValidTest {

    public static void main(String[] args) throws Exception {
        //告诉系统把注解代理类编译后显示在项目目录中【见小节4.1】
        System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        RegexValidTest.User user = new RegexValidTest.User("小明", "2021-01-23", "327047419@qq.com", "1531348294");
        if (RegexValidUtil.check(user)) {
            System.out.println(user + "正则校验通过");
        }

    }

    public static class User {
        private String name;
        @RegexValid(policy = Policy.DATE)//定义注解,一定时机有人去解析它
        private String date;
        @RegexValid(policy = Policy.MAIL)
        private String email;
        @RegexValid("0?(13|14|15|18|17)[0-9]{9}")
        private String phone;
        @RegexValid("0?(13|14|15|18|17)[0-9]{9}")
        private int phone2;

        public User(String name, String date, String email, String phone) {
            this.name = name;
            this.date = date;
            this.email = email;
            this.phone = phone;
        }

        public String toString() {
            return "User{name='" + this.name + "', date='" + this.date + "', email='" + this.email + "', phone='" + this.phone + "'}";
        }
    }
}

2.4 [-> RegexValid.java]

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegexValid {

    RegexValid.Policy policy() default RegexValid.Policy.EMPTY;

    String value() default "";
    
    public static enum Policy {
        EMPTY((String)null),
        DATE("\d{4}(\-|\/|.)\d{1,2}\1\d{1,2}"),
        MAIL("\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}");

        private String policy;

        private Policy(String policy) {
            this.policy = policy;
        }

        public String getPolicy() {
            return this.policy;
        }
    }
}

注解中的数据类型是有限制的。支持如下

  1. 基本类型
  2. String类型
  3. Class类型
  4. enum
  5. Annotation
  6. 以上类型的数组

注解的属性必须有确认的值。建议指定默认值

2.5 [-> RegexValidUtil.java]

public class RegexValidUtil {

    public static boolean check(Object obj) throws Exception {
        boolean result = true;
        StringBuilder sb = new StringBuilder();
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            //判断obj类当前字段是否使用了RegexValid的注解【见小节3.1】
            if (field.isAnnotationPresent(RegexValid.class)) { 
                RegexValid regexValid = field.getAnnotation(RegexValid.class);【见小节3.2】
                String value = regexValid.value();
                if ("".equals(value)) {
                    value = regexValid.policy().getPolicy();
                }

                field.setAccessible(true);
                Object fieldObj = null;
                try {
                    fieldObj = field.get(obj);
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    e.printStackTrace();
                }

                if (fieldObj == null) {
                    sb.append("\n").append(String.format(" %s 类中的 %s 字段不能为空!", obj.getClass().getName(), field.getName()));
                    result = false;
                } else {
                    if (fieldObj instanceof String) {
                        String text = (String) fieldObj;
                        result = Pattern.compile(value).matcher(text).matches();
                        if (!result) sb.append("\n").append(String.format("%s 不是合法的 %s!", text, field.getName()));
                    } else {
                        sb.append("\n").append(String.format("%s 不是String 无法校验!", fieldObj));
                    }
                }
            }
        }

        if (sb.length() > 0) throw new Exception(sb.toString());
        return result;
    }
}

当使用注解去动态生成代码。我们来去仿butterknife(在Android使用)。

第三个例子

2.3 [-> BindView.java]

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

2.4 [-> ButterKnife.java]

public class ButterKnife {
    //利用反射来调用注解生成的类,来绑定view的id(赋值其成员view对象)
    public static void bind(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        try {
            Class<?> bindViewClass = Class.forName(clazz.getName() + "ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (Exception e) {
            e.printStackTrace();
            Log.d("liuyuzhe", "ButterKnife.bind: "+e.getMessage());
        }
    }
}

还需要这2个module

  • Java library: apt_annotation 定义注解
  • Java library: apt_processor 定义注解处理器来生成Java类

RegexValid regexValid = field.getAnnotation(RegexValid.class) //getAnnotation创建了AnnotationInvocationHandler对象并且把接口的数据内容存到了map中

三. 源码解析

运行期注解使用@interface来实现,编译后会变成一个接口(@会被去掉),并且实现了Annotation接口。而我们通过反射返回获取到的对象其实是一个代理类。这代理类是包装类,本质是通过AnnotationInvocationHandler来调用方法的,它内部维护了一个map,map的key是方法名 value是方法对应的值。

3.1 [-> AccessibleObject.java]

@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
    //这里转发给父类调用
    return AnnotatedElement.super.isAnnotationPresent(annotationClass);
}

3.1.1 [-> AnnotatedElement.java]

default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
    return getAnnotation(annotationClass) != null;【小节3.2】
}

3.2 [-> Field.java]

public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
    Objects.requireNonNull(annotationClass);
    return annotationClass.cast(declaredAnnotations().get(annotationClass));
}

Field 它重写了getAnnotation方法,从map尝试取字段上是否有annotationClass这个注解对象。

3.3 [-> Field.java]

private Map<Class<? extends Annotation>, Annotation> declaredAnnotations() {
    Map<Class<? extends Annotation>, Annotation> declAnnos;
    if ((declAnnos = declaredAnnotations) == null) {
        synchronized (this) {
            if ((declAnnos = declaredAnnotations) == null) {
                Executable root = (Executable)getRoot();
                if (root != null) {
                    declAnnos = root.declaredAnnotations();
                } else {
                    //如果没有缓存数据,则会解析注解的数据,把对象存到declAnnos的map中【见小节3.3.1】
                    declAnnos = AnnotationParser.parseAnnotations( 
                            getAnnotationBytes(),
                            SharedSecrets.getJavaLangAccess().
                                    getConstantPool(getDeclaringClass()),
                            getDeclaringClass()
                    );
                }
                declaredAnnotations = declAnnos;
            }
        }
    }
    return declAnnos;
}

3.3.1 [-> AnnotationParser.java]

parseAnnotations ->会转发调用parseAnnotation2

@SuppressWarnings("unchecked")
private static Annotation parseAnnotation2(ByteBuffer buf,
                                          ConstantPool constPool,
                                          Class<?> container,
                                          boolean exceptionOnMissingAnnotationClass,
                                          Class<? extends Annotation>[] selectAnnotationClasses) {
    Map<String, Object> memberValues =
        new LinkedHashMap<String, Object>(type.memberDefaults());//创建缓存map

    int numMembers = buf.getShort() & 0xFFFF;
    for (int i = 0; i < numMembers; i++) {
        int memberNameIndex = buf.getShort() & 0xFFFF;
        String memberName = constPool.getUTF8At(memberNameIndex);
        Class<?> memberType = memberTypes.get(memberName);

        if (memberType == null) {
            // Member is no longer present in annotation type; ignore it
            skipMemberValue(buf);
        } else {
            //去解析获取注解里的值
            Object value = parseMemberValue(memberType, buf, constPool, container);
            if (value instanceof AnnotationTypeMismatchExceptionProxy)
                ((AnnotationTypeMismatchExceptionProxy) value).
                    setMember(type.members().get(memberName));
            memberValues.put(memberName, value);//方法名和其值存起来,待调用时返回给客户端
        }
    }
    return annotationForMap(annotationClass, memberValues);【见小节3.4】
}

3.4 [-> AnnotationParser.java]

我们直接从parseAnnotations方法 一直跟进到annotationForMap方法

/**
 * Returns an annotation of the given type backed by the given
 * member {@literal ->} value map.
 */
public static Annotation annotationForMap(final Class<? extends Annotation> type,
                                          final Map<String, Object> memberValues)
{
    //这里利用JDK动态代理,type是注解class,memberValues是缓存map key是方法(接口里属性)名 value是;当使用注解调用方法时,会走AnnotationInvocationHandler的invoke方法
    return AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
        public Annotation run() {
            return (Annotation) Proxy.newProxyInstance(
                type.getClassLoader(), new Class<?>[] { type },
                new AnnotationInvocationHandler(type, memberValues));
        }});
}

3.5 [-> AnnotationInvocationHandler.java]

public Object invoke(Object proxy, Method method, Object[] args) {
    //取出方法名字
    String member = method.getName();
    int parameterCount = method.getParameterCount();

    // Handle Object and Annotation methods
    if (parameterCount == 1 && member == "equals" &&
            method.getParameterTypes()[0] == Object.class) {
        return equalsImpl(proxy, args[0]);
    }
    if (parameterCount != 0) {
        throw new AssertionError("Too many parameters for an annotation method");
    }
    //如果是系统自带方法直接返回默认实现
    if (member == "toString") {
        return toStringImpl();
    } else if (member == "hashCode") {
        return hashCodeImpl();
    } else if (member == "annotationType") {
        return type;
    }

    // Handle annotation member accessors
    Object result = memberValues.get(member);//如果是客户端自定义方法从缓存的Map中取出。如何存的【见小节3.4】

    if (result == null)
        throw new IncompleteAnnotationException(type, member);

    if (result instanceof ExceptionProxy)
        throw ((ExceptionProxy) result).generateException();

    if (result.getClass().isArray() && Array.getLength(result) != 0)
        result = cloneArray(result);

    return result;
}

4.1 [-> $Proxy1.java]

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.sun.proxy;

import com.company.annotation.RegexValid;
import com.company.annotation.RegexValid.Policy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy1 extends Proxy implements RegexValid {
    private static Method m0;
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m4;
    private static Method m5;

    public $Proxy1(InvocationHandler param1) {
        super(var1);
    }

    public final int hashCode() {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    //这些hashcode 和 toString 等都是默认生成的
    public final boolean equals(Object var1) {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    //这value是我们自定义的方法,h是AnnotationInvocationHandler调用了invoke走到了【见小节3.5】
    public final String value() {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final Policy policy() {
        try {
            return (Policy)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final Class annotationType() {
        try {
            return (Class)super.h.invoke(this, m5, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.company.annotation.RegexValid").getMethod("value");
            m4 = Class.forName("com.company.annotation.RegexValid").getMethod("policy");
            m5 = Class.forName("com.company.annotation.RegexValid").getMethod("annotationType");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

四. 小结

经过源码分析发现注解的动态代理的处理逻辑和Retrofit的动态代理一样,难道Retrofit作者仿注解思路来写的🤣。

开发中编译后可以在build->source->apt里找到编译生成的代码。

Screen Shot 2021-12-10 at 3.21.21 PM.png

项目地址:github.com/hy-liuyuzhe…