网站建设视频直播功能表,品牌购买网站,关于网站建设的电话销售话术,给个网站能用的2022Java 元注解有 5 种#xff0c;常用的是 Target 和 Retention 两个。
其中 Retention 表示保留级别#xff0c;有三种#xff1a;
RetentionPolicy.SOURCE - 标记的注解仅保留在源码级别中#xff0c;并被编译器忽略RetentionPolicy.CLASS - 标记的注解在编译时由编译器保…Java 元注解有 5 种常用的是 Target 和 Retention 两个。
其中 Retention 表示保留级别有三种
RetentionPolicy.SOURCE - 标记的注解仅保留在源码级别中并被编译器忽略RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留但 JVM 会忽略RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留因此运行时环境可以使用它
这三个值表示的生命周期为 SOURCE CLASS RUNTIME即 RUNTIME 生命周期最长涵盖了 SOURCE 和 CLASS其次是 CLASS 的生命周期涵盖了 SOURCE。
不同的保留级别有不同的应用场景 1、源码级别应用场景
1.1 APT
Annotation Processor Tools即注解处理器。它在由源文件.java被 javac 编译成为字节码文件.class这个过程中先采集到所有的注解信息并封装到 Element 对象中然后再由 javac 自动调用注解处理器无须手动调用。
接下来对使用方法做一个简介。
先创建一个保留级别为源码级别的注解
Target({ElementType.TYPE, ElementType.METHOD})
Retention(RetentionPolicy.SOURCE)
public interface Frank {int value();String id();
}然后创建一个 Java-Library 模块来编写注解处理程序。模块的代码结构如下 注册注解处理器
先注册注解处理器文件。只有注册后它才能被 javac 调用。注册方式有两种一是按照上图的路径创建同名的文件夹和文件路径名固定不要自己发挥然后在 javax.annotation.processing.Processor 文件中将注解处理器文件的全类名写在里面即可
com.frank.compiler.MyProcessor另一种方式就是使用 AutoService 自动生成。首先在模块 gradle 中加入依赖
dependencies {implementation com.google.auto.service:auto-service:1.0-rc6annotationProcessor com.google.auto.service:auto-service:1.0-rc6
}AutoService 与 gradle-5.4.1 以上版本有兼容性问题必须要向上边那样把 annotationProcessor 的依赖也写上才能避免无法自动生成 Processor 文件的情况。在 gradle-6.1.1 上亲测可行。 然后在注解处理器文件的类加上 AutoService 的注解
AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {Overridepublic boolean process(Set? extends TypeElement set, RoundEnvironment roundEnvironment) {return false;}
}AutoService 的值固定为 Processor.class。编译后在 module/build/classes/java/main/META-INF/services/ 目录下就能找到 javax.annotation.processing.Processor 文件了其内容与手动构建的是一样的。
注解处理器使用
注解处理类需要继承 AbstractProcessor 才具有处理注解的能力通过重写 process() 可以对注解进行处理
AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {Overridepublic boolean process(Set? extends TypeElement set, RoundEnvironment roundEnvironment) {Messager messager processingEnv.getMessager();messager.printMessage(Diagnostic.Kind.NOTE, 注解处理器输出);return false;}
}以上只是简单的输出了一句 log本节不会具体介绍注解处理器结合具体业务该如何编写仅介绍一般的使用流程。
想要让这句 log 能打印出来还需要在类名上边打上 SupportedAnnotationTypes 注解表示我们关心的注解类型其值为我们要处理的注解的全类名以我们前边的 Frank 注解为例就应该写成
AutoService(Processor.class)
SupportedAnnotationTypes(com.example.myapplication.Frank)
public class MyProcessor extends AbstractProcessor {// .....
}最后在需要使用注解处理器的模块的 gradle 文件中引入注解处理器模块 compiler
dependencies {annotationProcessor project(:compiler)
}这样在编译后就能看到 log 输出了 注意输出是在执行 compileDebugJavaWithJavac 任务时输出的再次说明了注解处理器由 javac 自动调用并分析了其中的源码。也印证了 APT 是源码保留级别适用的技术。
1.2 IDE 语法检查
之所以叫 IDE 语法检查是因为检测语法的是 IDE 工具或其插件并不是 Java 编译器。
在 Android 中我们需要设计接口以供使用者调用时如出现需要对方法参数进行类型限定如限定为资源 ID、布局 ID 等类型参数时可以利用 Android 为我们提供的语法检查注解来辅助进行更为直接的参数类型检查与提示。
在 androidx.annotation 包下有很多这样的注解如 DrawableRes、IdRes、LayoutRes 等等想要使用它们需要先引入依赖 implementation androidx.annotation:annotation:1.2.0-alpha01使用时可以把它们加载方法类型之前 public final Drawable getDrawable(DrawableRes int id) {return getResources().getDrawable(id, getTheme());}这样在调用 getDrawable() 方法时就必须传入一个 Drawable 的资源值IDE 就会在实参位置有红色下划线提示注意编译是不会报错的 除了上述的注解之外还有几个元注解 IntDef、LongDef、StringDef利用它们可以自定义类似于 DrawableRes、IdRes、LayoutRes 这样的注解。典型应用就是用注解替代枚举以节省内存。 Java 中 Enum 枚举的实质是特殊单例的静态成员变量在运行期所有枚举类作为单例全部加载到内存中。比常量多 5 到 10 倍的内存占用。每个枚举值也就是枚举对象的构成为 12 个字节的对象头 所有成员变量另外还有个 8 字节对齐。 IntDef 的源码
Retention(SOURCE)
Target({ANNOTATION_TYPE})
public interface IntDef {/** Defines the allowed constants for this element */int[] value() default {};boolean flag() default false;boolean open() default false;
}通过 value() 可以指定注解能包含哪些值。下面就利用这一点看看如何用注解替换枚举
public class Gender {/*enum Gender {MALE, FEMALE}*/public static final int MALE 0;public static final int FEMALE 1;private int gender;Retention(RetentionPolicy.SOURCE)Target(ElementType.PARAMETER)IntDef({MALE, FEMALE})interface GenderInt {}public void setGender(GenderInt int gender) {this.gender gender;}
}Gender 类表示性别这里我们不用枚举而是把男性和女性分别定义成两个整型常量 0 和 1。使用 IntDef 注解自定义一个 GenderInt 注解用 Target(ElementType.PARAMETER) 指定作用在参数上用 IntDef({MALE, FEMALE}) 规定可选值只有 MALE, FEMALE 两个。
在外部调用 setGender() 时如果传入的值不在 MALE, FEMALE 之中IDE 就会提醒 使用源码级别注解的典型框架是 ARouter。关于 Android 组件化以及 ARouter 原理可以参考 Android 组件化基础二—— 仿 ARouter 实现一个路由框架。
2、字节码级别应用场景
字节码增强也称为字节码插桩说白了就是在字节码文件里写代码和修改。过程大概是
.class 文件 - 用 IO 流读写 class 文件 - byte[] - 按照 .class 文件格式进行修改。
这里仅举简单例子不具体扩展。对字节码插桩感兴趣的可以去看 ASM 字节码插桩入门。
假如要统计每个方法的执行时间并且不想在源代码的方法中加入时间统计代码那就可以把需要统计的方法打上注解编译之后在字节码的方法首末加入统计运行时间的代码 ARouter、ButterKnife 都是字节码保留级别的典型应用。
3、运行时级别应用场景
最典型的就是反射。
3.1 通过反射实现 findViewById()
一个最简单的示例就是通过反射 注解实现 findViewById()。
首先定义一个运行时级别的注解 Inject
Target(ElementType.FIELD)
Retention(RetentionPolicy.RUNTIME)
public interface Inject {int value();
}值是被 Inject 修饰控件的资源 ID。
然后编写工具类拿到控件 ID 后调用 findViewById() 得到控件对象最后通过反射把对象设置给控件
public class InjectUtils {public static void inject(Activity activity) {Class? extends Activity aClass activity.getClass();Field[] fields aClass.getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(Inject.class)) {Inject annotation field.getAnnotation(Inject.class);int id annotation.value();View view activity.findViewById(id);field.setAccessible(true);try {field.set(activity, view);} catch (IllegalAccessException e) {e.printStackTrace();}}}}
}给控件打上注解并标明资源 ID进行测试
public class MainActivity extends AppCompatActivity {Inject(R.id.button)private Button button;Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);InjectUtils.inject(this);button.setText(Inject Success);}
}3.2 通过反射获取真实泛型类型
当我们对一个泛型类进行反射时需要得到泛型中的真实数据类型来完成如 Json 反序列化的操作。此时需要通过 Type 体系来完成。 Type 接口包含了一个实现类 Class 和四个实现接口他们分别是
TypeVariable 泛型类型变量可以获取泛型上下限等信息ParameterizedType 具体的泛型类型可以获得元数据中泛型签名类型泛型真实类型GenericArrayType 当需要描述的类型是泛型类的数组时比如 List[]、Map[]此接口会作为 Type 的实现WildcardType 通配符泛型获得上下限信息
下面来介绍一个获取泛型真实类型的典型应用。
典型应用 —— Gson 反序列化
对从服务器接收的响应数据进行反序列化是项目中的常规操作。我们举个例子假如使用 Gson 进行反序列化用 Response 表示响应体用 Data 表示 Response 中携带的真实数据
public class ResponseT {private T data;private int code;private String message;public Response(T data, int code, String message) {this.data data;this.code code;this.message message;}...
}public class Data {private String result;public Data(String result) {this.result result;}Overridepublic String toString() {return Data{ result result \ };}
}模拟从服务器接收数据进行反序列化的过程
public class Test {public static void main(String[] args) {// 构造一个 Response 对象模拟服务器返回的数据ResponseData response new Response(new Data(数据),1,成功);Gson gson new Gson();String json gson.toJson(response);System.out.println(json);// 反序列化由于没有提供 Response 内 data 的真实类型所以反序列化失败抛出异常ResponseData responseData gson.fromJson(json, Response.class);System.out.println(responseData.getData().getClass());}
}执行上述代码会抛出异常
Exception in thread main java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.frank.reflect.Dataat com.frank.reflect.Test.main(Test.java:16)FAILURE: Build failed with an exception.如果反序列化的 Bean 没有泛型那么可以使用 public T T fromJson(String json, ClassT classOfT) 方法进行反序列化但如果像 Response 那样带有泛型但是却没提供泛型的真实类型就会抛出上面的异常。
解决方案是使用 TypeToken 提供 ResponseData 的类型参数 Data 给 fromJson()
public class Test {public static void main(String[] args) {// 构造一个 Response 对象模拟服务器返回的数据ResponseData response new Response(new Data(数据), 1, 成功);Gson gson new Gson();String json gson.toJson(response);System.out.println(json);// 反序列化通过创建 TypeToken 的匿名子类获取到 Response 的真实类型即 DataType type new TypeTokenResponseData() {}.getType();ResponseData responseData gson.fromJson(json, type);System.out.println(responseData.getData().getClass());}
}这样就能正确输出反序列化结果了
{data:{result:数据},code:1,message:成功}
class com.frank.reflect.Data解决问题的原理
为何通过匿名子类对象获取到类型参数原因有二
Java 泛型在编译期被擦除因此运行时无法直接获取到 ResponseData 上的类型参数匿名子类会保存类型参数。因为编译器会生成一个合成类来表示匿名子类的类型并且在合成类中会保留泛型信息。这样做的目的是为了在匿名子类中能够正确地访问和使用泛型类型
基于以上两点我们来看 Gson 源码是如何通匿名子类获取类型参数的
public class TypeTokenT {final Class? super T rawType;final Type type;final int hashCode;/*** 客户端创建一个空的匿名子类。这样做可以将类型参数嵌入到匿名类的类型层次结构中* 因此我们可以在运行时重新构造它尽管它被删除了*/protected TypeToken() {// 1.对匿名子类调用 getSuperclassTypeParameter() 获取类型参数this.type getSuperclassTypeParameter(getClass());this.rawType (Class? super T) $Gson$Types.getRawType(type);this.hashCode type.hashCode();}static Type getSuperclassTypeParameter(Class? subclass) {// 2.getGenericSuperclass() 会返回 subclass 所表示的实体类、接口、原始类型或 void的// 直接父类的类型这个类型是带有泛型信息的Type superclass subclass.getGenericSuperclass();if (superclass instanceof Class) {throw new RuntimeException(Missing type parameter.);}ParameterizedType parameterized (ParameterizedType) superclass;// 3.取所有泛型类型的首个类型做规范化转换后返回return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);}public final Type getType() {return type;}
}public final class $Gson$Types {public static Type canonicalize(Type type) {if (type instanceof Class) {Class? c (Class?) type;return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;} else if (type instanceof ParameterizedType) {// 如果是一个参数化类型就返回一个 ParameterizedTypeImpl 的实例ParameterizedType p (ParameterizedType) type;return new ParameterizedTypeImpl(p.getOwnerType(),p.getRawType(), p.getActualTypeArguments());} else if (type instanceof GenericArrayType) {GenericArrayType g (GenericArrayType) type;return new GenericArrayTypeImpl(g.getGenericComponentType());} else if (type instanceof WildcardType) {WildcardType w (WildcardType) type;return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());} else {// type is either serializable as-is or unsupportedreturn type;}}
}实际上 TypeToken 构造方法上官方给的注释已经提示了使用匿名子类的原因。具体的实现步骤可以分三步
将 TypeToken 的构造方法声明为 protected也就是说在 TypeToken 所在的 com.google.gson.reflect 包之外想要拿到 TypeToken 或其子类对象就无法通过 new TypeToken() 的方法而只能使用创建匿名子类对象的方式即 new TypeToken(){}。getClass() 拿到的就是这个匿名子类class com.frank.reflect.Test$1在 Test 内声明的匿名子类然后对这个匿名子类调用 getSuperclassTypeParameter()在 getSuperclassTypeParameter() 内先对匿名子类调用 getGenericSuperclass() 获取到带有泛型信息的父类即 TypeTokenResponseData将父类对象转换为 ParameterizedType并调用其 getActualTypeArguments() 获取到真实的类型参数数组取数组的第 0 个在这个例子中就是 ResponseData传入 G s o n Gson GsonTypes.canonicalize() 将结果进行规范化并返回
TypeToken 设计的巧妙之处就在于用 protected 的构造方法迫使你使用匿名子类才能构建其对象然后在 getSuperclassTypeParameter() 方法中用这个匿名子类对象对应的 Class 去获取含有泛型的直接父类就是 TypeTokenT。使用匿名子类获取真实类型参数的做法也很值得借鉴。
4、练习
通过反射 注解实现被启动 Activity 自动获取 Intent 中携带的数据。详细点说就是在 Activity1 通过 Intent 启动 Activity2 时可能会给 Intent 通过 putXxxExtra() 的形式附带一些参数。在 Activity2 中拿到 Intent 对象之后需要一个一个地把数据从 Intent 中拿出来过程繁琐。我们要优化的就是从 Intent 中手动拿数据这个过程。
首先在 MainActivity 中建立一些要传递的数据包含多种类型
public class MainActivity extends AppCompatActivity {// 需要作为 Intent 的参数被传递的变量要提出来做成员变量private String string;private String attr;private int[] array;private UserParcelable userParcelable;private UserParcelable[] userParcelables;private ListUserParcelable userParcelableList;private UserSerializable userSerializable;Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Intent intent new Intent(MainActivity.this, SecondActivity.class);ArrayListParcelable parcelables new ArrayList();parcelables.add(new UserParcelable(user3));intent.putExtra(StringKey, testString).putExtra(attr, attr).putExtra(array, new int[]{1, 2}).putExtra(userParcelable, new UserParcelable(cat)).putExtra(userParcelables, new UserParcelable[]{new UserParcelable(user1),new UserParcelable(user2)}).putExtra(userParcelableList, parcelables).putExtra(userSerializable, new UserSerializable(user4));startActivity(intent);}
}其中 UserParcelable 与 UserSerializable 两个类型分别模拟实现了 Parcelable 和 Serializable 接口的 JavaBean 类型
public class UserParcelable implements Parcelable {String name;public UserParcelable(String name) {this.name name;}protected UserParcelable(Parcel in) {name in.readString();}public static final CreatorUserParcelable CREATOR new CreatorUserParcelable() {Overridepublic UserParcelable createFromParcel(Parcel in) {return new UserParcelable(in);}Overridepublic UserParcelable[] newArray(int size) {return new UserParcelable[size];}};Overridepublic int describeContents() {return 0;}Overridepublic void writeToParcel(Parcel dest, int flags) {dest.writeString(name);}
}
------------------------------------------------------------------
public class UserSerializable implements Serializable {String name;public UserSerializable(String name) {this.name name;}
}然后新建一个注解 InjectExtras
Target(ElementType.FIELD)
Retention(RetentionPolicy.RUNTIME)
public interface InjectExtras {String value() default ;
}用法是标记接收 Intent 数据一方的成员变量value() 默认为空为空时说明 MainActivity 在存数据的时候key 和变量名是一样的如果不一样就把 key 作为 value
public class SecondActivity extends AppCompatActivity {InjectExtras(StringKey)private String string;InjectExtrasprivate String attr;InjectExtrasprivate int[] array;InjectExtrasprivate UserParcelable userParcelable;InjectExtras(userParcelables)private UserParcelable[] userParcelables;InjectExtrasprivate ListUserParcelable userParcelableList;InjectExtrasprivate UserSerializable userSerializable;Overrideprotected void onCreate(Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);InjectUtils.injectExtra(this);}
}最后实现工具方法 /*** 拿到 activity 后遍历所有成员变量筛选出被 InjectExtras 注释的成员* 获得其对应的属性 Key 值然后通过 getIntent().getExtras() 获取到对方* 发送的参数 Bundle然后根据 Key 从中取出 Value最后通过反射的方式为接* 收方的成员变量赋值。** param activity 接收参数的 Activity*/public static void injectExtra(Activity activity) {Class? extends Activity clazz activity.getClass();Field[] fields clazz.getDeclaredFields();Bundle extras activity.getIntent().getExtras();for (Field field : fields) {if (field.isAnnotationPresent(InjectExtras.class)) {InjectExtras annotation field.getAnnotation(InjectExtras.class);if (annotation null) {Log.e(TAG, Get annotation InjectExtras failed.);return;}// 找到 field 在发送方存入参数的 key并取出对应的 valueString key TextUtils.isEmpty(annotation.value()) ? field.getName() : annotation.value();Object object extras.get(key);// value 如果是 Parcelable[] 则不能直接赋值其它类型可以所以通过 getComponentType()// 获取数组元素类型如果不是数组则返回 nullClass? componentType field.getType().getComponentType();if (componentType ! null field.getType().isArray() Parcelable.class.isAssignableFrom(componentType)) {// 确定了 object 是 Parcelable[]进行转换Object[] objArray (Object[]) object;// 使用工具类将数组内元素转换成 field 对应的类型即 UserParcelable// 第三个参数实际上要获取的是 UserParcelable[].class 这个类型只能通过反射获取object Arrays.copyOf(objArray, objArray.length, (Class? extends Object[]) field.getType());}field.setAccessible(true);try {field.set(activity, object);} catch (IllegalAccessException e) {e.printStackTrace();}}}}获取 Extra 中 key 的规则是如果 Field 被 InjectExtras 注解指定了 value那么就用指定的值否则默认使用变量名。
拿到 key 之后获取对应的 value如果 value 类型不是 Parcelable[] 的话就可以直接设置给对应的 Field 了如果是的话需要进行一个转化再赋值。这一步中需要注意如果 field 是 UserParcelable[]那么 field.getType() 得到的是 class [Lxxx.xxx.UserParcelable再调用 getComponentType() 就拿到了数组内的元素类型 class xxx.xxx.UserParcelable。由于不能将 object 直接做类型强转为 Parcelable[]所以借用 Arrays.copyOf() 做这个类型转换第三个参数 field.getType() 的结果不能直接用编译不过需要将 class [Lxxx.xxx.UserParcelable 转换为 Class? extends Object[]因为 field.getType() 返回的类型实际上是一个 Class?强转成 Class? extends UserParcelable[] 会有 unchecked warning但是转成 Class? extends Object[] 就不会。