筑基#注解的意义与应用场景

49 阅读7分钟

java筑基#注解

java筑基#多线程编程

java筑基#序列化

java筑基#内存模型

java筑基#泛型

一.注解的意义与应用场景

1.注解的定义

java注解又称java标注,是jdk1.5引入的注释机制,是元数据的一种形式,提供有关于程序,但是又不属于程序本身的数据。注解对她们注解的代码的操作没有直接影响。

注解本身是没有意义的,单独的注解只是一种注释,需要结合APT、反射、插装技术才有意义。

2.常用注解与元注解

常用注解如下图:

我们说的元注解是注解在注解上的注解,比如:

@RetentionPolicy :标记注解的保留时,它的参数类型是RetentionPolicy。它的值有三种:SOURCE、 CLASS、RUNTIME,分别对应的是编译源码阶段、class级别是保留到class,但是会被JVM抛弃,并且 android的class会被打包成dex,所以注解会被擦除掉,RUNTIME是保留到虚拟机级别。

@Target :用来标记注解作用的目标,它的参数类型是ElementType数组,字段有:TYPE、METHOD、 FIELD等

3.注解的应用场景

注解保留级别技术使用场景
SOURCEAPT编译器能捕获注解与注解声明的类包括类中的所有成员信息,一般用于生成额外的辅助类。
CLASS字节码增强编译生成class后,通过修改class数据以实现修改代码逻辑,可以使用注解来标记来区分活修改不同的逻辑。
RUNTIME反射程序运行时通过反射技术老获取注解及其原色,从而完成不同的判定。

1.源码级别的注解

  • 如下面的IntDef 注解的使用:就会在源码阶段做警告

@IntDef(value = {IntDefTest.SUNDAY,IntDefTest.MONDAY}) 
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.PARAMETER)
public @interface WeekDayAnn {
}
public class IntDefTest {
   public  final static int SUNDAY=1;
   public final static int MONDAY=2;
   /**
    *
    * 可以代理枚举取值
    */
   public void setWeekDay(@WeekDayAnn int day){
      System.out.println("setWeekDay:"+day);
   }

   public  void setTestDay(){
      new IntDefTest().setWeekDay(92);//这里就会有警告,会做语法检查
   }
}
  • 搭配APT使用

APT是annotation process tools的简称,它是javac提供的工具,也就是java编译器,我们在控制台上可以敲入javac命令查看:

当我们使用 Java 编译器(如 javac )编译 Java 代码时,编译器会在编译过程中识别并处理注解。编译器会检查类路径中是否存在注解处理器,并触发注解处理阶段。

下面我们就通过一个样例来使用APT帮助我们生成一个类:

步骤1:创建一个anntation的库,并且创建一个注解@TestAnnotation

@Retention(RetentionPolicy.SOURCE) //指定自定义注解级别
@Target(ElementType.TYPE)
public @interface TestAnnotation {
}

步骤2:创建compile的库用来处理注解,创建自定义注解处理器:

@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class TestProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Messager messager = processingEnv.getMessager();
        Filer filer = processingEnv.getFiler();

        for (TypeElement annotation : set) {
            messager.printMessage(Diagnostic.Kind.NOTE, "Processing annotation: " + annotation.getSimpleName());
        }
  
        if (!set.isEmpty()) {
            String packageName = "com.seven.annotation";
            String className = "AptTest";
            String code = generateClassCode(packageName, className);
            OutputStream outputStream =null;
            try {
                JavaFileObject aptClass = filer.createSourceFile(packageName+"."+className);
                outputStream= aptClass.openOutputStream();
                outputStream.write(code.getBytes());
                outputStream.flush();
            } catch (IOException e) {
//                messager.printMessage(Diagnostic.Kind.ERROR, "Failed to generate class file: " + e.getMessage());
                e.printStackTrace();
                System.out.println("error:===>>>"+e.getMessage());
            }finally {
                if(null!=outputStream){
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return true;
    }

    private String generateClassCode(String packageName, String className) {
        StringBuilder codeBuilder = new StringBuilder();
        codeBuilder.append("package ").append(packageName).append(";\n");
        codeBuilder.append("public class ").append(className).append(" {\n");
        codeBuilder.append("}");
        return codeBuilder.toString();
    }

  
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add("com.seven.annotation.TestAnnotation");
        return types;
    }
}

并且创建清单文件,文件路径为:\main\resources\META-INF\services

com.seven.compile.TestProcessor 

步骤3:我们在项目中依赖这两个库:

annotationProcessor project(':compile')
implementation project(':annotation')

步骤4:我们build下app,就可以看到生成的java文件

2.字节码级别的注解

这个主要是应对的字节码插装,比如我们在ARouter路由中,想要把各个子路由模块加入到路由表中,如果我们不使用字节码,我们可能就需要通过注解反射技术来实现路由的添加,即在子路由模块中用注解标注,同时在路由模块中,通过遍历所有类的注解来来生成实例添加到路由表中,而注解+字节码插装就可以在class中帮助我们插入代码,这个后面再介绍ARouter原理时可以细说。

3.运行时级别的注解

这个主要是结合反射来使用。这个也不展开说说了,主要针对反射来介绍反射的几个特性:

1.反射是否可以修改final修饰的变量?

基本数据和字符串数据类型不可以改,因为有编译器自动优化。

代码演示:

public class ReflectTest {

    final int a=1;

    public int getA() {
        return a;
    }

    @Test
    public void test(){
        ReflectTest reflectTest=new ReflectTest();
        try {
            Field a1 = reflectTest.getClass().getDeclaredField("a");
            a1.setAccessible(true);
            a1.setInt(reflectTest,2);
            int ax=reflectTest.getA();
            System.out.println("reflectTest.a="+ax);//这里不会修改
            Field a2 = reflectTest.getClass().getDeclaredField("a");
            a2.setAccessible(true);
            int o = a2.getInt(reflectTest);//通过反射会获取
            System.out.println("reflectTest.a2="+o);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

我们看下编译后的class文件

public class ReflectTest {
    final int a = 1;

    public ReflectTest() {
    }

    public int getA() {
        return 1; //直接返回1
    }

    @Test
    public void test() {
        ReflectTest reflectTest = new ReflectTest();

        try {
            Field a1 = reflectTest.getClass().getDeclaredField("a");
            a1.setAccessible(true);
            a1.setInt(reflectTest, 2);
            int ax = reflectTest.getA();
            System.out.println("reflectTest.a=" + ax);
            Field a2 = reflectTest.getClass().getDeclaredField("a");
            a2.setAccessible(true);
            int o = a2.getInt(reflectTest);
            System.out.println("reflectTest.a2=" + o);
        } catch (NoSuchFieldException var6) {
            var6.printStackTrace();
        } catch (IllegalAccessException var7) {
            var7.printStackTrace();
        }

    }
}

我们再看看不用final修饰后编译的class文件

public class ReflectTest {
    int a = 1;

    public ReflectTest() {
    }

    public int getA() {
        return this.a;
    }

    @Test
    public void test() {
        ReflectTest reflectTest = new ReflectTest();

        try {
            Field a1 = reflectTest.getClass().getDeclaredField("a");
            a1.setAccessible(true);
            a1.setInt(reflectTest, 2);
            int ax = reflectTest.getA();
            System.out.println("reflectTest.a=" + ax);
            Field a2 = reflectTest.getClass().getDeclaredField("a");
            a2.setAccessible(true);
            int o = a2.getInt(reflectTest);
            System.out.println("reflectTest.a2=" + o);
        } catch (NoSuchFieldException var6) {
            var6.printStackTrace();
        } catch (IllegalAccessException var7) {
            var7.printStackTrace();
        }

    }
}

2.反射为什么效率低?

比如我们调用某个方法,正常调用我们直接对象.方法,可是反射我们先要获取对象的类,再获取对象的方法,再使用invoke来调用对象的方法,并且invoke中的参数必须是包装类,这样就会多了很多操作,并且还要检测方法是否存在、方法参数是否匹配等,这些都增加了时间,并且反射不能使用编译器的优化,比如上面的样例。

二.java动态代理原理

1.代理是什么?

在设计模式中,代理它是一个结构性设计模式,代理就是使用一个对象代理另外一个对象,从而控制对象的访问权限,并且可以为其增加别的特性。

代理的结构:

代理和真实的对象都继承特定的接口,并且代理持有真实对象的引用。

代码展示:

public interface Component {
    void singSong();
}
public class Star implements Component{

    @Override
    public void singSong() {
        System.out.println("start sing song");
    }
}

public class ProxyStar implements Component {
    private Component component;
    @Override
    public void singSong() {
        System.out.println("proxy singSong");
        component=new Star();
    
        component.singSong();
       
    }
}

2.动态代理的原理

当我们的java代码编译成class文件后会放到内存/磁盘中,我们的动态代理就是根据class再内存中生成一份

字节码:

       Component component = new Star();//被代理的对象

        Component proxy = (Component) Proxy.newProxyInstance(Component.class.getClassLoader(),
                new Class[]{Component.class}, new InvocationHandler() {

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args)
                    throws   Throwable {
                      
                        Object invoke = method.invoke(component);
                  
                        return  invoke;
                    }
                });
        proxy.singSong();

三.三者结合使用的样例

我们仿照ButterKnife来给android中的控件通过注解、反射加上动态代理实现自动查找和点击事件:

你可能会遇到Attribute value must be constant 的问题的解决办法://在gradle.properties中添加android.nonFinalResIds=false

代码展示:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
     @IdRes int viewId() default -1;
}



public class InjectTools {
    public static void inject(AppCompatActivity activity) {
        injectBindView(activity);
        injectEventCommon(activity);
    }

 
    private static void injectBindView(AppCompatActivity object) {
      
        Class<? extends AppCompatActivity> clz = object.getClass();
        Field[] fields = clz.getDeclaredFields();
        for (Field field : fields) {
            BindView annotation = field.getAnnotation(BindView.class);
            if (annotation!=null) {
                int valueId = annotation.viewId();
                try {
                    Method method = clz.getMethod("findViewById", int.class);
                    Object invoke = method.invoke(object, valueId);//findViewById()
                    field.setAccessible(true);
                    field.set(object, invoke);//设置button=findViewById()
                    System.out.println("inject id success");
                } catch (NoSuchMethodException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    private static void injectEventCommon(Activity activity) {


        Class<? extends Activity> activityClz = activity.getClass();

    
        Method[] declaredMethods = activityClz.getDeclaredMethods();

        for (Method declaredMethod : declaredMethods) {

            declaredMethod.setAccessible(true);

    

            Annotation[] declaredAnnotations = declaredMethod.getDeclaredAnnotations();


            for (Annotation declaredAnnotation : declaredAnnotations) {


                Class<? extends Annotation> annotationType = declaredAnnotation.annotationType();

        
                OnBaseCommon annotation = annotationType.getAnnotation(OnBaseCommon.class);
                if (annotation == null) {

                    continue;
                }
                try {
          
                    Method valueMethod = annotationType.getDeclaredMethod("value");
             
                    int invokeValueId = (int)valueMethod.invoke(declaredAnnotation);

         
                    Method findViewById = activityClz.getMethod("findViewById", int.class);
                    Object button = findViewById.invoke(activity, invokeValueId);
                    String event = annotation.event();
                    Class eventClz = annotation.eventClzSource();
                    String callMethod = annotation.onEventMethod();

                    Method methodSetOnClickList = button.getClass().getMethod(event, eventClz);
                    methodSetOnClickList.setAccessible(true);

            
                    methodSetOnClickList.invoke(button, Proxy.newProxyInstance(eventClz.getClassLoader(),
                            new Class[]{eventClz}, new InvocationHandler() {
                                @Override
                                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                                    System.out.println("invoke it");
                                    return declaredMethod.invoke(activity);
                                }
                            }));

                } catch (Exception e) {
                    e.printStackTrace();
                }

            }


        }

    }


}

四.总结

通过今天的学习,我们大概掌握了以下内容:

1.注解只是一种标记作用,它是在jdk1.5之后加上的,注解也分为元注解和自带的注解以及自定义注解,元注解是标记注解的注解;

2.注解它有作用对象和作用时间,我们一般会用它结合apt、字节码插装以及反射来为我们解决一些问题;

3.后面我们也学习到了动态代理,并且通过注解、反射和动态代理的结合帮助我们给android中的控件增加点击事件,通过在注解中标记事件的类型和设置监听的接口类型,我们可以通过动态代理来生成对应点击事件中接口的实例来保证我们的代码简洁性,从而避免对于不同的点击事件,需要来new不同的点击事件回调接口实例。