一、概念
Annotation(注解)就是Java提供了一种源程序中的元素关联任何信息和任何元数据(metadata)的途径和方法。
定义:注解(Annotation),也叫元数据,是一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释,创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查等。从某些方面看,Annotation就像修饰符一样被使用,并应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中。这些信息被存储在Annotation的“name=value”结构对中。
Annotation(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotation对象,然后通过Annotation对象来获取注解里面的元数据。
1、Annotation和Annotation类型
Annotation:
Annotation使用了在java5.0所带来的新语法,它的行为十分类似public、final这样的修饰符。每个Annotation具有一个名字和成员个数>=0。每个Annotation的成员具有被称为name=value对的名字和值(就像javabean一样),name=value装载了Annotation的信息。
Annotation类型:
Annotation类型定义了Annotation的名字、类型、成员默认值。一个Annotation类型可以说是一个特殊的java接口,它的成员变量是受限制的,而声明Annotation类型时需要使用新语法。当我们通过java反射API访问Annotation时,返回值将是一个实现了该 Annotation类型接口的对象,通过访问这个对象我们能方便的访问到其Annotation成员。
Annotation的成员在Annotation类型中以无参数的方法的形式被声明。其方法名和返回值定义了该成员的名字和类型。在此有一个特定的默认语法:允许声明任何Annotation成员的默认值:一个Annotation可以将name=value对作为没有定义默认值的Annotation成员的值,当然也可以使用name=value对来覆盖其它成员默认值。这一点有些近似类的继承特性,父类的构造函数可以作为子类的默认构造函数,但是也可以被子类覆盖。
2、什么是metadata(元数据)
元数据从metadata一词译来,就是“关于数据的数据”的意思。
元数据的功能作用有很多,比如:你可能用过javadoc的注释自动生成文档。这就是元数据功能的一种。总的来说,元数据可以用来创建文档,跟踪代码的依赖性,执行编译时格式检查,代替已有的配置文件。如果要对于元数据的作用进行分类,目前还没有明确的定义,不过我们可以根据它所起的作用,大致可分为三类:
1)、编写文档:通过代码里标识的元数据生成文档;
2)、代码分析:通过代码里标识的元数据对代码进行分析;
3)、编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查。
在Java中元数据以标签的形式存在于Java代码中,元数据标签的存在并不影响程序代码的编译和执行,它只是被用来生成其它的文件或正在运行时知道被运行代码的描述信息。
综上所述:
第一,元数据以标签的形式存在于Java代码中。
第二,元数据描述的信息是类型安全的,即元数据内部的字段都是有明确类型的。
第三,元数据需要编译器之外的工具额外的处理用来生成其它的程序部件。
第四,元数据可以只存在于Java源代码级别,也可以存在于编译之后的Class文件内部。
二、Java中常见的注解
1、注解的分类
1)、按照运行机制分
源码注解:注解只在源码中存在,编译成class文件之后就不存在了;
编译时注解:注解在源码和class文件中都存在(@Override、@Deprecated、@Suppvisewarnings);
运行时注解:在运行阶段还在起作用,甚至会影响程序的运行逻辑。
2)、按照来源划分
来自JDK的注解
来自第三方的注解(使用最多)
自定义注解
3)、另外还有一种较特殊的注解
元注解:注解的注解。
2、JDK自带注解
注解的语法比较简单,除了@符号的使用外,他基本与Java固有的语法一致,JavaSE中内置三个标准注解,定义在java.lang中:
@Override:用于修饰此方法覆盖了父类的方法;
@Deprecated:用于修饰已经过时的方法;
@SuppressWarnnings:用于通知java编译器禁止特定的编译警告。
2.1、@Override
@Override,限定重写父类方法:@Override 是一个标记注解类型,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。 如果我们使用了这种Annotation在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示。这个Annotation常常在我们试图覆盖父类方法而确又写错了方法名时发挥威力(因此建议在重载父类方法时加上该注解)。使用方法极其简单:在使用此Annotation时只要在被修饰的方法前面加上@Override即可。下面的代码是一个使用@Override修饰一个企图重载父类的displayName()方法,而又存在拼写错误的实例:
/**
* @Description: @Override注解的使用
* @author: zxt
* @time: 2019年1月24日 下午6:53:12
*/
public class Fruit {
public void displayName() {
System.out.println("水果的名称是:***!");
}
}
class Orange extends Fruit {
@Override
public void displayName() {
System.out.println("水果的名称是:桔子!");
}
}
/**
* @Description: 当没有重载父类方法(或者写错方法名)时使用了@Override将报编译时错误
*
*/
class Apple extends Fruit {
// @Override
public void displayname() {
System.out.println("水果的名称是:苹果!");
}
@Override
public void displayName() {
System.out.println("水果的名称是:苹果!");
}
}
2.2、@Deprecated
标记已过的方法,同样@Deprecated也是一个标记注解。当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。 而且这种修饰具有一定的“延续性”:如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为 @Deprecated,但编译器仍然要报警。
值得注意的是,@Deprecated这个Annotation类型和javadoc中的 @deprecated这个tag是有区别的:前者是java编译器识别的,而后者是被javadoc工具所识别用来生成文档(包含程序成员为什么已经过时、它应当如何被禁止或者替代的描述)。
在java5.0,java编译器仍然像其从前版本那样寻找@deprecated这个javadoc的tag,并使用它们产生警告信息。但是这种状况将在后续版本中改变,我们应在现在就开始使用@Deprecated来修饰过时的方法而不是 @deprecated这个javadoc tag。
/**
* @Description: @Deprecated注解的使用
* @author: zxt
* @time: 2019年1月24日 下午7:54:40
*/
class AppleService {
public void displayName() {
System.out.println("水果的名称是:苹果!");
}
/**
* @Description:@Deprecated标记方法已经过时,不推荐使用
*/
@Deprecated
public void showTaste() {
System.out.println("水果的苹果的口感是:脆甜");
}
public void showTaste(int typeId) {
if (typeId == 1) {
System.out.println("水果的苹果的口感是:酸涩");
} else if (typeId == 2) {
System.out.println("水果的苹果的口感是:脆甜");
} else {
System.out.println("水果的苹果的口感是:超甜");
}
}
}
public class FruitRun {
public static void main(String[] args) {
Apple apple = new Apple();
apple.displayName();
AppleService appleService = new AppleService();
appleService.showTaste();
appleService.showTaste(1);
appleService.showTaste(2);
}
}
2.3、@Suppvisewarnings
抑制编译器警告,@SuppressWarnings 被用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。在java5.0,sun提供的javac编译器为我们提供了-Xlint选项来使编译器对合法的程序代码提出警告,此种警告从某种程度上代表了程序错误。例如当我们使用一个generic collection类而又没有提供它的类型时,编译器将提示出"unchecked warning"的警告,通常当这种情况发生时,我们就需要查找引起警告的代码。如果它真的表示错误,我们就需要纠正它。例如如果警告信息表明我们代码中的switch语句没有覆盖所有可能的case,那么我们就应增加一个默认的case来避免这种警告。
有时我们无法避免这种警告,例如,我们使用必须和非generic的旧代码交互的generic collection类时,我们不能避免这个unchecked warning。此时@SuppressWarning就要派上用场了,在调用的方法前增加@SuppressWarnings修饰,告诉编译器停止对此方法的警告。
@SuppressWarning不是一个标记注解。它有一个类型为String[]的成员,这个成员的值为被禁止的警告名。对于javac编译器来讲,被-Xlint选项有效的警告名也同样对@SuppressWarings有效,同时编译器忽略掉无法识别的警告名。
Annotation语法允许在Annotation名后跟括号,括号中是使用逗号分割的name=value对用于为Annotation的成员赋值。实例如下:
/**
* @Description: @SuppressWarnings注解的使用
* @author: zxt
* @time: 2019年1月24日 下午8:48:52
*/
public class FruitService {
/**
* @Description:可以放在方法的前面,也可以放在有编译警告的语句前面;可以使用value={}的形式,也可以省略value
*/
public static void main(String[] args) {
@SuppressWarnings("unused")
List<String> strList = new ArrayList<String>();
}
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public static List<Fruit> getFruitList() {
List<Fruit> fruitList = new ArrayList();
return fruitList;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public static List<Fruit> getFruit() {
List<Fruit> fruitList = new ArrayList();
return fruitList;
}
}
在这个例子中SuppressWarnings annotation类型只定义了一个单一的成员,所以只有一个简单的value={...}作为name=value对。又由于成员值是一个数组,故使用大括号来声明数组值。注意:我们可以在下面的情况中缩写Annotation:当Annotation只有单一成员,并成员命名为"value="。这时可以省去"value="。比如将上面方法getFruit()的SuppressWarnings Annotation就是缩写的。
@SuppressWarnings注解的常见参数值的简单说明:
1、deprecation:使用了不赞成使用的类或方法时的警告;
2、unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
3、unused:抑制没被使用过的代码的警告;
4、rawtypes:使用generics时忽略没有指定相应的类型;
5、fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
6、path:在类路径、源文件路径等中有不存在的路径时的警告;
7、serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
8、finally:任何 finally 子句不能正常完成时的警告;
9、all:关于以上所有情况的警告。
3、常见的第三方注解
第三方的注解主要在使用一些框架的时候会用到,例如:
三、元注解
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
1、@Target;
2、@Retention;
3、@Documented;
4、@Inherited。
这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们看一下每个元注解的作用和相应分参数的使用说明。
1、@Target
@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)。
取值(ElementType)有:
1、CONSTRUCTOR:用于描述构造器;
2、FIELD:用于描述域;
3、LOCAL_VARIABLE:用于描述局部变量;
4、METHOD:用于描述方法;
5、PACKAGE:用于描述包;
6、PARAMETER:用于描述参数;
7、TYPE:用于描述类、接口(包括注解类型) 或enum声明。
使用实例:
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface Table {
/**
* @Description:数据表名称注解,默认值为类名称
* @return
*/
public String tableName() default "className";
}
@Target(ElementType.FIELD)
public @interface NoDBColumn {
}
注解Table 可以用于注解类、接口(包括注解类型) 或enum声明,而注解NoDBColumn仅可用于注解类的成员变量。
2、@Retention
@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)。
取值(RetentionPoicy)有:
1、SOURCE:在源文件中有效(即源文件保留);
2、CLASS:在class文件中有效(即class保留);
3、RUNTIME:在运行时有效(即运行时保留)。
Retention meta-annotation类型有唯一的value作为成员,它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值。具体实例如下:
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
Column注解的的RetentionPolicy的属性值是RUTIME,这样注解处理器可以通过反射,获取到该注解的属性值,从而去做一些运行时的逻辑处理。
3、@Inherited
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
4、@Documented
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
四、自定义注解
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
定义注解格式:
public @interface 注解名 {定义体}
注解参数的可支持数据类型:
1、所有基本数据类型(int,float,boolean,byte,double,char,long,short);
2、String类型;
3、Class类型;
4、enum类型;
5、Annotation类型;
6、以上所有类型的数组;
Annotation类型里面的参数该怎么设定:
第一,只能用public或默认(default)这两个访问权修饰。例如,String value();这里把方法设为defaul默认类型;
第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,Annotations等数据类型,以及这一些类型的数组。例如,String value();这里的参数成员就为String;
第三,如果只有一个参数成员,最好把参数名称设为"value",后加小括号。
1、注解元素的默认值
注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此,使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,以此表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 水果供应者注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**
* 供应商编号
* @return
*/
public int id() default -1;
/**
* 供应商名称
* @return
*/
public String name() default "";
/**
* 供应商地址
* @return
*/
public String address() default "";
}
2、注解处理器
定义了注解,并在需要的时候给相关类,类属性加上注解信息,如果没有响应的注解信息处理流程,注解可以说是没有实用价值。如何让注解真正的发挥作用,主要就在于注解处理方法。
注解处理器类库(java.lang.reflect.AnnotatedElement):
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
Class:类定义
Constructor:构造器定义
Field:类的成员变量定义
Method:类的方法定义
Package:类的包定义
java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:
方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
方法2:Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
方法3:boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。
方法4:Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注解。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。
3、项目实战
需求:有一张用户表,字段有用户ID、用户名、昵称、年龄、性别、所在城市、邮箱、手机号。现在需要完成的是,方便得对每个字段或者字段的组合条件进行索引,并打印出SQL。
package com.zxt.test;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Table注解的作用域为类或者接口
@Target(ElementType.TYPE)
// 在运行时解析
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
// 只有一个值标明表名即可
String value();
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Column注解的作用域为属性
@Target(ElementType.FIELD)
// 在运行时解析
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
// 标注属性名
String value();
}
package com.zxt.test;
/**
* @Description: 首先要做的就是将这个表与数据库表进行一个映射
* @author: zxt
* @time: 2019年2月2日 下午11:14:28
*/
@Table("user")
public class Filter {
@Column("id")
private int id;
@Column("user_name")
private String userName;
@Column("nick_name")
private String nickName;
@Column("age")
private int age;
@Column("city")
private String city;
@Column("email")
private String email;
@Column("mobile")
private String mobile;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
}
package com.zxt.test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
Filter f1 = new Filter();
// 查询用户id为10的用户
f1.setId(10);
Filter f2 = new Filter();
// 查询用户名为lucy 且 年龄为12 的用户
f2.setUserName("lucy");
f2.setAge(12);
Filter f3 = new Filter();
// 查询邮箱为其中之一的用户
f3.setEmail("123@qq.com,abc@163.com,7777@sina.com");
String sql1 = query(f1);
String sql2 = query(f2);
String sql3 = query(f3);
System.out.println(sql1);
System.out.println(sql2);
System.out.println(sql3);
}
/**
* @Description:query方法的实现,通过反射机制来读取注解
* @param f
* @return
*/
@SuppressWarnings("unchecked")
private static String query(Filter f) {
StringBuilder sb = new StringBuilder();
// 解析注解
// 1.获取class
@SuppressWarnings("rawtypes")
Class clazz = f.getClass();
// 2.获取到table的名称
boolean tableExist = clazz.isAnnotationPresent(Table.class);
if (!tableExist) {
return null;
}
Table t = (Table) clazz.getAnnotation(Table.class);
String tableName = t.value();
sb.append("select * from ").append(tableName).append(" where 1 = 1 ");
// 3.遍历所有的字段,如果有值就加到查询条件中
Field[] fieldArray = clazz.getDeclaredFields();
for (Field field : fieldArray) {
// 4.处理每个字段相对应的sql
boolean fieldExist = field.isAnnotationPresent(Column.class);
if (!fieldExist) {
continue;
}
// 4.1.获取字段名
String fieldName = field.getName();
// 4.2.拿到字段的值(通过字段名相应的get方法)
String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase() +
fieldName.substring(1);
Object fieldValue = null;
try {
Method getMethod = clazz.getMethod(getMethodName);
// 反射调用
fieldValue = getMethod.invoke(f);
} catch (Exception e) {
e.printStackTrace();
}
// 4.3.拼接sql
if(fieldValue == null ||
(fieldValue instanceof Integer && (Integer) fieldValue == 0)) {
continue;
}
sb.append("and ").append(fieldName);
if(fieldValue instanceof String) {
if(((String) fieldValue).contains(",")) {
String[] fieldValues = ((String) fieldValue).split(",");
sb.append(" in (");
for(String value : fieldValues) {
sb.append("'").append(value).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();
}
}