1. 简介
注解在Java开发中扮演很重要的角色,特别在一些框架或开源库中可以看到大量注解的运用,如果对注解不够熟悉,那么阅读这些框架或开源库的代码也是十分艰难的。本篇文章将从基本概念、常用注解及自定义注解三个方面来对注解进行一次全面总结,其实也是自己在深入学习注解过程中的一些心得,希望对想了解Java注解的学者有所帮助。
2. 基本概念
2.1 什么是注解
官方给予的解释是:
Annotations were introduced into Java in Java SE 5, providing a form of syntactic metadata that can be added to program constructs. Annotations can be processed at compile time, and they have no direct effect on the operation of the code.
翻译成中文大概意思就是:注解在Java SE5就被引入到Java中,提供一种以元数据的形式添加到程序代码中。注解可以在编译时存在,并且对代码的执行没有直接的影响。
相信初学者看了这段话还是处于一种懵逼状态,简单的说就是通过注解可以标注一个类、方法、变量具有某种特殊含义,例如使用@deprecation, 作用在方法上表示该方法过时了等,这仅仅只是某一方面,通过注解借用反射可以实现非常强大的功能,下面在自定义注解的时候会讲解到,如果对反射还不够了解,建议先看看有关Java反射的使用看这一篇就够了这篇文章。
2.2 注解分类
- 按照运行机制来分
- 源码注解:Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该Annotation信息了,也就是说注解只在源码中存在,编译成.class文件就不存在了。
- 编译时注解:注解在源码和.class文件中都存在。
- 运行时注解:在运行阶段还起作用,甚至会影响运行逻辑,编译器将Annotation存储于class文件中,并且可由JVM读入加载到JVM中,通常我们可以通过反射来获取注解信息。
- 按照来源来分
- JDK自带的注解。
- 第三方提供的注解。
- 我们自定义的注解。
2.3 元注解
所谓元注解即给注解进行注解的注解,主要用来自定义注解。看概念依然难以理解,下面我们具体来讲解。元注解主要有以下几种类型:
元注解 说明 参数 @Target Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数) 1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention 定义了该Annotation被保留的时间长短 1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留)
@Inherited 运行子注解继承, 也就是说某个父类添加了注解,对于其子类,该注解仍然有效,在自定义注解处会再次说明。 @Documented 生成文档信息,平常比较少用。
3. Java常用注解
-
@Override
该注解表示的意思是覆写,一般出现在继承关系中,子类某个方法覆写父类中的方法。
-
@deprecation
表示这个方法过时了,一般在父类或接口中标注,当然是可以在任何类中的方法上标注的。
- @Suppvisewarnings
它表示忽视警告,例如当某个类中用了过时的方法,则编译器会提示警告,要去除警告这可以添加该注解。
下面看如下Demo:
class Person {
public String getName() {
return "name";
}
@Deprecated // 标注该方法已过时,其他地方调用会划横线,并给予警告
public int getAge() {
return 0;
}
}
public class Student extends Person{
@Override // 覆写父类的方法
public String getName() {
return super.getName();
}
@SuppressWarnings("deprecation") // 忽视警告,如果没有该注解,则下面的调用会出现警告
public void printAge(){
System.out.println(getAge());
}
}
4. 自定义注解
我们在自定义注解的时候,有以下几点需要注意:
第一,只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;
第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和String,Enum,Class,annotations 等数据类型,以及这一些类型的数组.例如,String value();这里的参数成员就为String;
第三,如果只有一个参数成员,最好把参数名称设为"value",后加小括号。
如果是初学者,看到这几个注意点,估计处于完全懵逼状态,不知所云,下面用一个实例来说明,一定能让你恍然大悟。
@Target({ElementType.METHOD, ElementType.TYPE}) // 可以作用在方法(METHOD)和类、接口(包括注解类型) 或enum(TYPE)上
@Retention(RetentionPolicy.RUNTIME) // 定义注解的类型;SOURCE,CLASS,RUNTIME
@Inherited // 运行子注解继承
@Documented // 生成文档信息;
public @interface Description { // 使用@interface关键字定义注解;
/**
* 成员的类型是受限的,合法的类型包括基本类型及String,Class,Annotation,Enumeration
* 如果注解只有一个成员,则成员名必须取名为value(),在使用时可以忽略成员名和赋值号(=)
* 注解类可以没有成员,没有成员的注解称为标识注解
*/
String desc(); // 只能用public或默认(default)这两个访问权修饰
public String author();
int age() default 18; // 可以用default为成员指定一个默认值;
}
这里我们自定义了一个Description 类型注解,定义一个注解使用关键字@interface, 上面的注释非常详细,这里我在说说这个注解的意思,该注解可以作用在方法、类和接口上,在程序运行时有效,父类添加了此注解同样对子类也有效,同时还可以根据javadoc命令对类生成文档信息,接下来我们定义了三个注解的成员,分别为desc、author和age,其中age设置了默认值。
至此,相信大家都对自定义注解有一个全面的了解了,此时相信大家还是有一个疑惑,我已经知道怎么自定义注解了,可是这有什么用呢?哪些地方需要用到注解?
别着急,我们继续。。。
我们使用定义的注解添加到创建的Person类中,如下:
@Description(author = "lvjie", desc = "2016.8.13")
public class Person {
@Description(author = "jack", desc = "2016.04.10", age=20)
public String getName() {
return "name";
}
public int getAge() {
return 0;
}
}
下面再实现一个测试类:
public class MainTest {
public static void main(String[] args) {
try {
Class c = Class.forName("annotation.demo1.Person");
boolean isExist = c.isAnnotationPresent(Description.class);
if(isExist){
Description d = (Description) c.getAnnotation(Description.class); // 找到类上的注解;
System.out.println(d.author()+" "+d.desc()+" "+d.age());
}
// 找到方法上的注解;
Method[]ms = c.getMethods();
for (Method method : ms) {
boolean isMExit = method.isAnnotationPresent(Description.class);
if(isMExit){
Description md = method.getAnnotation(Description.class);
System.out.println(md.author()+" "+md.desc()+" "+md.age());
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行之后,可以看到如下输出:
lvjie 2016.8.13 18
jack 2016.04.10 20
这时候是否有那么一点茅塞顿开的感觉,我们通过反射可以获取哪些类哪些方法添加了此注解(这里再次说明一下,对反射不理解的建议去看看有关Java反射的使用看这一篇就够了这篇文章),到这里我们是否可以联想到Android中的一些开源库例如EventBus、DBFlow等用到注解的意图了,如果没有明白,没关系,接下来我们使用一个具体例子来说明注解给我们带来的好处。
使用过DBFlow的开发者,相信大家都为DBFlow如此简单的使用而惊叹,例如创建表只需要在JavaBean的类及字段添加相关注解即可,其实DBFlow使用注解在编译的时候为我们创建相关数据库表,下面我们就通过注解来实现sql查询语句的生成。
首先我们定义好两个注解分别为Table和Column,具体如下:
@Target(ElementType.TYPE) // 只作用于类或接口
@Retention(RetentionPolicy.RUNTIME) // 运行时
public @interface Table {
String value();
}
@Target(ElementType.FIELD) // 只作用于字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String value();
}
下面再定一个JavaBean即TUserInfo类
@Table("UserInfo")
public class TUserInfo {
@Column("id")
private int id;
@Column("userName")
private String userName;
@Column("age")
private int age;
@Column("email")
private String email;
@Column("mobile")
private String mobile;
// 省略 get set 函数
}
限于篇幅,此处省略相关get 和 set 函数。
再定义一个工具类,主要通过注解及结合反射来实现查询sql语句的实现,具体如下:
public class SqlUtils {
public static String querySql(Object object){
StringBuilder sb = new StringBuilder();
// 获得class
Class<? extends Object> c = object.getClass();
// 获得table的名字
boolean exists = c.isAnnotationPresent(Table.class); // 获取该类是否具有Table注解
if(!exists){
return null;
}
Table t = c.getAnnotation(Table.class);
String tableName = t.value(); // 获取对应注解的值
sb.append("select * from ").append(tableName).append(" where 1=1");
// 遍历所有字段
Field[] f = c.getDeclaredFields();
for (Field field : f) {
//4 处理每个字段对应的sql
// 4.1 拿到字段名;
boolean fExists = field.isAnnotationPresent(Column.class);
if(!fExists){
continue;
}
Column colum = field.getAnnotation(Column.class);
String columName = colum.value();
// 4.2 拿到字段值
String fieldName = field.getName();
String getMethodName = "get"+fieldName.substring(0, 1).toUpperCase()+fieldName.substring(1);
Object fieldValue = null;
try {
Method getMethod = c.getMethod(getMethodName);
fieldValue = getMethod.invoke(object);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 4.3 拼接sql
if(fieldValue==null || (fieldValue instanceof Integer && (Integer)fieldValue==0)){
continue;
}
sb.append(" and ").append(columName);
if(fieldValue instanceof String){
if(((String) fieldValue).contains(",")){
String[] values = ((String) fieldValue).split(",");
sb.append(" in(");
for (String string : values) {
sb.append("'").append(string).append("'").append(",");
}
sb.deleteCharAt(sb.length()-1);
sb.append(")");
}else{
sb.append("='").append(fieldValue).append("'");
}
}else if(fieldValue instanceof Integer){
sb.append("=").append(fieldValue);
}
}
return sb.toString();
}
}
下面编写测试类:
public class MainTest {
public static void main(String[] args) {
TUserInfo userInfo = new TUserInfo();
userInfo.setAge(20);
userInfo.setUserName("lvjie");
userInfo.setEmail("123@qq.com, 234@qq.com, 345@qq.com");
String sql = SqlUtils.querySql(userInfo);
System.out.println(sql);
}
}
运行,打印数据如下:
select * from UserInfo where 1=1 and userName='lvjie' and age=20 and email in('123@qq.com',' 234@qq.com',' 345@qq.com')
是不是很简单,针对这个demo,如果此时我们有增加了一个部门表,例如下:
@Table("TDepartment")
public class TDepartment {
@Column("id")
private int id;
@Column("departmentName")
private String departmentName;
@Column("departmentLeader")
private String departmentLeader;
// 省略set get 函数
}
TDepartment department = new TDepartment();
department.setId(10001);
department.setDepartmentLeader("lvjie");
System.out.println(SqlUtils.querySql(department));
输出入下:
select * from TDepartment where 1=1 and id=10001 and departmentLeader='lvjie'
此时,相信大家已经对注解刮目相看了吧,尽然能做到如此通用性,结合反射的使用,简直就是双剑合璧。
5. 其他
分析到这里,对注解应当有一定的认识了,那么大家有没有思考一个问题,上面这些例子标识的注解都是运行时注解,那CLASS和SOURCE注解有有什么作用呢?或者说在什么场景下会使用到这两种注解呢?
其实我们查看系统为我们提供的Override注解,我们可以发现它是RetentionPolicy.SOURCE类型,当我们的方法添加了Override注解,表示覆写了父类中的方法,表明父类肯定也是有该方法的,代码在编译期间会进行语法检查,因此源码型注解可以做这类事情,当Java文件编译成class文件后,源码型注解就不存在了。
对于CLASS编译类型注解,我们在一些开源库例如ButterKnife、Dagger等等都用到了编译时注解,编译时注解通常都需要和注解处理器(Annotation Processor)结合一起使用,如果在结合开源库JavaPoet,则能够在编译期间创建Java源代码,编译完成之后生成对应的class文件(其实ButterKnife、Dagger这两个库的实现就很好的运用了上面的原理,例如ButterKnife利用编译时注解及结合JavaPoet,我们在使用ButterKnife提供的注解时,当编译我们的程序,就会生成对应的"类名_ViewBinding"的class文件)。下篇文章将会从注解的角度去分析ButterKnife的实现原理。学习完注解,再来探索下ButterKnife的实现原理
编译型注解需要使用到APT(Annotation Processing Tool)技术,就是通过编译期解析注解,并且生成Java代码的一种技术。
什么是注解处理器?
注解处理器是一个在javac编译期处理注解的工具,你可以创建注解处理器并注册,在编译期你创建的处理器以Java代码作为输入,生成文件.java文件作为输出。javac去调起注解处理器,执行我们的注解处理程序。如何调试注解处理器:https://blog.csdn.net/heng615975867/article/details/105089595
总结
本篇文章对注解做了一个全面解析,从基本概念出发到最后的实例运用,只有在实例中才能体会注解的真谛,希望对大家有所帮助,同时也让自己对注解的理解更加深刻。
参考文献
https://www.cnblogs.com/huajiezh/p/5263849.html
http://www.oracle.com/technetwork/articles/java/ma14-architect-annotations-2177655.html
https://www.cnblogs.com/understander/p/5528789.html
https://blog.csdn.net/github_35180164/article/details/52121038