探秘Java注解及在android中的使用


最近看了几个开源框架,发现很多框架都使用到了注解,其中Butterknife是很多人都比较熟知的,还有retrofit,eventbus等等。因为对注解没有深入了解,因此花了点时间学习了一下,顺便用博客记录一下以免过后忘记。

什么是注解?

我们最先接触注解应该是在Java中,Java是在JDK1.5开始允许通过注解方式@Override @Deprecated 进行程序的定义,先来看看如何定义一个注解。

public @interface Test {
}

通过上述代码我们使用@interface便可以定义一个@Test注解,注解的定义看起来很像接口的定义,事实上,注解也会被编译成Class文件。我们现在定义的注解很像一个空接口,其实注解也会需要一些元注解。

元注解

Java目前内置了四种元注解,元注解专门负责注解其他注解

元注解表示
@Target表示该注解可以用于干什么地方,可能的ElementType参数包括:
CONSTRUCTOR:构造器的声明
FIELD:域声明(包括enum实例)
LOCAL_VARIABLE: 局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类,接口或enum声明
@Retention表示需要在什么级别保存该注释信息,可选的Retention参数包括:
SOURCE:注解将被编译器丢弃
CLASS:注解在Class文件中可用,但会被VM丢弃
RUNTIME:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息
@Documented将此注解包含在JavaDoc中
@InHerited允许子类继承父类中的注解

因为元注解的存在,我们可以将注解保存信息配置,因此就存在于两种注解,一种是编译期注解,一种为运行期注解,前者通过注解解释器来生成代码,另一种就需要通过反射来获取这种因为用到了反射所以对性能会有影响。而我们熟悉的ButterKnife就是通过编译期注解来生成代码,这样便不会影响性能,因为是编译期注解所以@Retention应该配置为CLASS,@Target应该是字段FIELD,那么我们看一下ButterKnife的注解。

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

果然和我们想的一样,但是里面还多了一行,其实这是定义注解时一般都会包含一些元素表示某些值,在使用ButterKnife的时候我们会传入控件的id,当我们分析处理注解时,程序或工具就可以利用这些值,这看起来像一个接口的方法其实你还可以给他指定默认的值,比如,

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserCase {
    public int id();
    public String description() default "no description";
}

注解元素

在UserCase注解中我们定义了两个元素,包含int元素的id和一个String元素的description这两个元素就是注解元素,注解元素可用的类型有:

类型
所有基本类型(int,float,boolean等)
String
Class
enum
Annotation
以上类型的数组

在使用注解的时候通过键值对的方式给元素赋值,当然如果你只有一个元素可以将元素定义为value这样就可以不用键值对的方式直接给元素赋值。

注解使用

我们使用定义好的注解,在通过反射获取注解元素

public class PasswordUtils {
    @UserCase(id = 47,description = "密码必须包含至少一个数字")
    public boolean validatePassword(String password){
        return (password.matches("\\w*\\d\\w*"));
    }
    @UserCase(id = 48)
    public String encryptPassword(String password){
        return new StringBuilder(password).reverse().toString();
    }
    @UserCase(id = 49,description = "新密码与之前的密码不重合")
    public boolean checkForNewPassword(List<String> prevPasswords, String password){
        return !prevPasswords.contains(password);
    }
}
public class UseCaseTracker {
    public static void trackUserCases(Class<?> cls){
        for(Method m:cls.getDeclaredMethods()){
            UserCase uc = m.getAnnotation(UserCase.class);
            if(uc != null){
                System.out.println("注解id"+uc.id() + "注解description"+uc.description());
            }
        }
    }
    public static void main(String[] args){
        trackUserCases(PasswordUtils.class);
    }
}

我们可以轻易的拿到注解的元素,但是反射往往是不好的,以上的例子只是演示注解的获取。

apt处理注解

apt注解处理工具,他被设计为操作Java源文件,而不是编译后的类,在使用apt生成注解处理器时,我们无法利用Java反射机制,因为我们操作的是源代码,而不是编译后的类。apt如何生成注解处理器?
通过继承AbstractProcessor来实现。

@AutoService(Processor.class)
public class BindViewProcessors extends AbstractProcessor {
    private Filer mFiler;
    private Messager mMessager;
    private Elements mElementUtils;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        return annotations;
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

关于上面这几个函数的意义:

init(ProcessingEnvironment processingEnv)
被注解处理工具调用,参数ProcessingEnvironment 提供了Element,Filer,Messager等工具
getSupportedAnnotationTypes()
指定注解处理器是注册给那一个注解的,它是一个字符串的集合,意味着可以支持多个类型的注解,并且字符串是合法全名。
getSupportedSourceVersion
指定Java版本
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
这个也是最主要的,在这里扫描和处理你的注解并生成Java代码,信息都在参数RoundEnvironment 里了。

其中最重要的还是process了,主要生成代码就是通过这个函数。下面我们来看看如何生成一个最简单的代码。

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);  //拿到所有的注解
        for (Element element : bindViewElements) {
            //1.获取包名
            PackageElement packageElement = mElementUtils.getPackageOf(element);
            String pkName = packageElement.getQualifiedName().toString();
            
            //2.获取包装类类型
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String enclosingName = enclosingElement.getQualifiedName().toString();
        
            //因为BindView只作用于filed,所以这里可直接进行强转
            VariableElement bindViewElement = (VariableElement) element;
            //3.获取注解的成员变量名
            String bindViewFiledName = bindViewElement.getSimpleName().toString();
            //3.获取注解的成员变量类型
            String bindViewFiledClassType = bindViewElement.asType().toString();

            //4.获取注解元数据
            BindView bindView = element.getAnnotation(BindView.class);
            int id = bindView.value();
           
            //4.生成文件
            createFile(enclosingElement, bindViewFiledClassType, bindViewFiledName, id);
            return true;
        }
        return false;
    }
    //生成java文件
     private void createFile(TypeElement enclosingElement, String bindViewFiledClassType, String bindViewFiledName, int id) {
        String pkName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
        try {
            JavaFileObject jfo = mFiler.createSourceFile(pkName + ".ViewBinding", new Element[]{});
            Writer writer = jfo.openWriter();
            writer.write(brewCode(pkName, bindViewFiledClassType, bindViewFiledName, id));
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
	//拼代码
    private String brewCode(String pkName, String bindViewFiledClassType, String bindViewFiledName, int id) {
        StringBuilder builder = new StringBuilder();
        builder.append("package " + pkName + ";\n\n");
        builder.append("//Auto generated by apt,do not modify!!\n\n");
        builder.append("public class ViewBinding { \n\n");
        builder.append("public static void main(String[] args){ \n");
        String info = String.format("%s %s = %d", bindViewFiledClassType, bindViewFiledName, id);
        builder.append("System.out.println(\"" + info + "\");\n");
        builder.append("}\n");
        builder.append("}");
        return builder.toString();
    }

在brewCode方法我们通过字符串拼接的方式生成了代码,看着比较low不过已经有框架帮忙解决了这个问题,还可以使用javapoet

结合JavaPoet来试着实现一个Butterknife

1.定义注解
上面已经定义过了。这里不再重复
2. 定义注解解释器,使用javapoet生成代码

  @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Map<Element, List<Element>> map = new HashMap<>();
        for (TypeElement typeElement : set) {
            for (Element fieldElement : roundEnvironment.getElementsAnnotatedWith(typeElement)) {
                Element classElement = fieldElement.getEnclosingElement();
                if (fieldElement.getModifiers().contains(Modifier.PRIVATE))
                    throw new RuntimeException("@BindView can't annotate private field with " + fieldElement.getSimpleName()+" in "+classElement.getSimpleName());
                List<Element> list;
                if (map.containsKey(classElement))
                    list = map.get(classElement);
                else {
                    list = new ArrayList<>();
                    map.put(classElement, list);
                }
                list.add(fieldElement);
            }
        }
        for (Map.Entry<Element, List<Element>> entry : map.entrySet()) {
            MethodSpec.Builder constructorBuilder = MethodSpec.methodBuilder("bindViews")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)     //修饰符
                    .addParameter(ClassName.get(entry.getKey().asType()), "obj"); //参数
            for (Element fieldElement : entry.getValue()) {
                constructorBuilder.addStatement("$L.$L = $L.findViewById($L)", "obj", fieldElement.getSimpleName(), "obj", fieldElement.getAnnotation(BindView.class).value());
            }

            TypeSpec helloWorld = TypeSpec.classBuilder(entry.getKey().getSimpleName() + "_Binding")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(constructorBuilder.build())
                    .build();

            JavaFile javaFile = JavaFile.builder(((PackageElement) entry.getKey().getEnclosingElement()).getQualifiedName().toString(), helloWorld)
                    .build();
            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }
  1. 使用反射调用生成代码
public class ButterKnife {
    public static void bind(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class bindViewClass = Class.forName(clazz.getName() + "_Binding");
            Method method = bindViewClass.getMethod("bindViews", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

这样我们就可以像使用butterknife一样来使用我们自己写好的。用法与butterknife类似,在setcontentview之后通过反射调用我们生成的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值