Java 注解
注解基础知识点
定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
作用分类:
-
编写文档:通过代码里标识的元数据生成文档【生成文档doc文档】
-
代码分析:通过代码里标识的元数据对代码进行分析【使用反射】
-
编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查【Override】
Java注解,也叫元数据,外文名Annotation,是一种代码级别的说明
Annotation(注解)是JDK1.5及以后版本引入的。它可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。注解是以'@注解名'在代码中存在的,根据注解参数的个数,我们可以将注解分为:标记注解,单值注解,完整注解三类。他们都不会直接影响到程序的语义,只是作为注解(标识)存在,我们可以通过反射机制变成实现对这些元数据(用来描述数据的数据)的访问。另外,你可以再编译时选择代码里的注解是否存在于源代码级,或者它也能在class文件、或者运行时中出现。
@Override,限定重写父类方法:
@Override 是一个标记注解类型,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种Annotation在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示。这个annotaton常常在我们试图覆盖父类方法而确又写错了方法名时发挥威力。使用方法极其简单:在使用此annotation时只要在被修饰的方法前面加上@Override即可。下面的代码是一个使用@Override修饰一个企图重载父类的displayName()方法,而又存在拼写错误的实例:
public class Fruit {
public void displayName(){
System.out.println("水果的名字是:***");
}
}
public class Apple extends Fruit{
@Override
public void displayname(){
System.out.println("水果的名字是:苹果");
}
}
public class Orange extends Fruit{
@Override
public void displayName(){
System.out.println("水果的名字是:桔子");
}
}
Orange 类编译不会有任何问题,Apple 类在编译的时候会提示相应的错误。@Override注解只能用于方法,不能用于其他程序元素。
@Deprecated,标记已过时:
同样Deprecated也是一个标记注解。当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。而且这种修饰具有一定的 "延续性":如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为 @Deprecated,但编译器仍然要报警。
值得注意,@Deprecated这个annotation类型和javadoc中的 @deprecated这个tag是有区别的:前者是java编译器识别的,而后者是被javadoc工具所识别用来生成文档(包含程序成员为什么已经过时、它应当如何被禁止或者替代的描述)。
在java5.0,java编译器仍然象其从前版本那样寻找@deprecated这个javadoc tag,并使用它们产生警告信息。但是这种状况将在后续版本中改变,我们应在现在就开始使用@Deprecated来修饰过时的方法而不是 @deprecated javadoc tag。
下面一段程序中使用了@Deprecated注解标示方法过期,同时在方法注释中用@deprecated tag 标示该方法已经过时,代码如下:
public class AppleService {
public void displayName(){
System.out.println("水果的名字是:苹果");
}
/**
* @deprecated 该方法已经过期,不推荐使用
*/
@Deprecated
public void showTaste(){
System.out.println("水果的苹果的口感是:脆甜");
}
public void showTaste(int typeId){
if(typeId==1){
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(0);
}
}
AppleService类的showTaste() 方法被@Deprecated标注为过时方法,在FruitRun类中使用的时候,编译器会给出该方法已过期,不推荐使用的提示。
SuppressWarnnings,抑制编译器警告:
@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的成员赋值。实例如下:public class FruitService {
@SuppressWarnings(value={"rawtypes","unchecked"})
public static List<Fruit> getFruitList(){
List<Fruit> fruitList=new ArrayList();
return fruitList;
}
@SuppressWarnings("unused")
public static void main(String[] args) {
List<String> strList=new ArrayList<String>();
}
}
在这个例子中SuppressWarnings annotation类型只定义了一个单一的成员,所以只有一个简单的value={...}作为name=value对。又由于成员值是一个数组,故使用大括号来声明数组值。注意:我们可以在下面的情况中缩写annotation:当annotation只有单一成员,并成员命名为"value="。这时可以省去"value="。比如将上面方法getFruit()的SuppressWarnings annotation就是缩写的。
SuppressWarnings注解的常见参数值的简单说明:
1.deprecation:使用了不赞成使用的类或方法时的警告;
2.unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
3.fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
4.path:在类路径、源文件路径等中有不存在的路径时的警告;
5.serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
6.finally:任何 finally 子句不能正常完成时的警告;
7.all:关于以上所有情况的警告。要深入学习注解,我们就必须能定义自己的注解,并使用注解,在定义自己的注解之前,我们就必须要了解Java为我们提供的元注解和相关定义注解的语法。
元注解:
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
1.@Target,
2.@Retention,
3.@Documented,
4.@Inherited
这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们看一下每个元注解的作用和相应分参数的使用说明。@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声明@Target(ElementType.TYPE)
public @interface Table {
public String tableName() default "className";
}
@Target(ElementType.FIELD)
public @interface NoDBColumn {
}
注解Table 可以用于注解类、接口(包括注解类型) 或enum声明,而注解NoDBColumn仅可用于注解类的成员变量。
@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的枚举类型值。具体实例如下:
@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,这样注解处理器可以通过反射,获取到该注解的属性值,从而去做一些运行时的逻辑处理
@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;
}
@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类型被发现,或者到达类继承结构的顶层。
实例代码:
@Inherited
public @interface Greeting {
public enum FontColor{BULE,RED,GREEN};
String name();
FontColor fontColor() default FontColor.GREEN;
}
自定义注解:
使用@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",后加小括号.例:下面的例子FruitName注解就只有一个参数成员。简单的自定义注解和使用注解实例:
/**
* 颜色属性
* @author user
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
public enum Color{BULE,RED,GREEN};
Color fruitColor() default Color.GREEN;
}
/**
* 水果名称注解
* @author user
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}
public class Apple {
@FruitName("Apple")
private String appleName;
@FruitColor(fruitColor=Color.RED)
private String appleColor;
@FruitProvider(id=1,name="陕西红富士集团",address="陕西省西安市延安路89号红富士大厦")
private String appleProvider;
public void setAppleColor(String appleColor) {
this.appleColor = appleColor;
}
public String getAppleColor() {
return appleColor;
}
public void setAppleName(String appleName) {
this.appleName = appleName;
}
public String getAppleName() {
return appleName;
}
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
}
public void displayName(){
System.out.println("水果的名字是:苹果");
}
}
注解元素的默认值:
注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。例如:
/**
* 水果供应者注解
* @author user
*
*/
@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 "";
}
如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。
注解处理器类库(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 is AnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.
方法4:Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。一个简单的注解处理器:
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz){
String strFruitName=" 水果名称:";
String strFruitColor=" 水果颜色:";
String strFruitProvicer="供应商信息:";
Field[] fields = clazz.getDeclaredFields();
for(Field field :fields){
if(field.isAnnotationPresent(FruitName.class)){
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
strFruitName=strFruitName+fruitName.value();
System.out.println(strFruitName);
}
else if(field.isAnnotationPresent(FruitColor.class)){
FruitColor fruitColor= (FruitColor) field.getAnnotation(FruitColor.class);
strFruitColor=strFruitColor+fruitColor.fruitColor().toString();
System.out.println(strFruitColor);
}
else if(field.isAnnotationPresent(FruitProvider.class)){
FruitProvider fruitProvider= (FruitProvider) field.getAnnotation(FruitProvider.class);
strFruitProvicer=" 供应商编号:"+fruitProvider.id()+" 供应商名称:"+fruitProvider.name()+" 供应商地址:"+fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
}
}
public class Test {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
}
}
水果名称:Apple
水果颜色:RED
供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延安路89号红富士大厦
自定义注解实战
引言
Java Web开发中,对框架的理解和掌握是必须的。而在使用大多数框架的过程中,一般有两种方式的配置,一种是基于xml的配置方式,一种是基于注解的方式。然而,越来越多的程序员(我)在开发过程中享受到注解带来的简便,并义无反顾地投身其中。ORM框架,像Hibernate,Mybatis就提供了基于注解的配置方式。我们接下来就使用自定义注解实现袖珍版的Mybatis,袖珍版的Hibernate。
这很重要
说明:实战的代码会被文章末尾附上。而实际上在之前做袖珍版框架的时候并没有想到会拿来做自定义注解的Demo。因此给出的代码涉及了其他的一些技术,例如数据库连接池,动态代理等等,比较杂。
在这个篇幅我们只讨论关于自定义注解的问题,至于其他的技术后面会开多几篇博文阐述。(当然这么多前辈面前不敢造次,有个讨论学习的氛围是很好的~)那么在自定义注解框架前,我们需要花点时间浏览以下几个和Annotation相关的方法。
方法名
用法
Annotation getAnnotation(Class annotationType)
获取注解在其上的annotationType
Annotation[] getAnnotations()
获取所有注解
isAnnotationPresent(Class annotationType)
判断当前元素是否被annotationType注解
Annotation[] getDeclareAnnotations()
与getAnnotations() 类似,但是不包括父类中被Inherited修饰的注解
Mybatis 自定义注解
本节目标:自定义注解实现Mybatis插入数据操作。
本节要求:细心观察使用自定义注解的步骤。Step 1 :声明自定义注解。
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)
public @interface Insert {
publicStringvalue();
}
Step 2 : 在规定的注解使用范围内使用我们的注解
publicinterface UserMapper {
@Insert("insert into user (name,password) values (?,?)")
publicvoid addUser(String name,String password);
}
Step 3 : 通过method.getAnnotation(Insert.class).value()使用反射解析自定义注解,得到其中的sql语句
//检查是否被@Insert注解修饰if (method.isAnnotationPresent(Insert.class)) {
//检查sql语句是否合法
//method.getAnnotation(Insert.class).value()取得@Insert注解value中的Sql语句
sql = checkSql(method.getAnnotation(Insert.class).value(),
Insert.class.getSimpleName());
//具体的插入数据库操作
insert(sql, parameters);
}
Step 4 : 根据实际场景调用Step 3的方法
UserMapper mapper = MethodProxyFactory.getBean(UserMapper.class);
mapper.addUser("walidake","665908");
运行结果:
mybatis.png
以上节选自annotation中Mybatis部分。具体CRUD操作请看源码。
总结一下从上面学到的东西:
1.声明自定义注解,并限制适用范围(因为默认是通用)
2.规定范围内使用注解
3.isAnnotationPresent(Insert.class)检查注解,getAnnotation(Insert.class).value()取得注解内容
4.根据实际场景应用
Hibernate 自定义注解
本节目标:自定义注解使实体自动建表(即生成建表SQL语句)
本节要求:动手操作,把未给全的代码补齐。
本节规划:仿照Hibernate,我们大概会需要@Table,@Column,还有id,我们这里暂且声明为@PrimaryKey
仿照自定义Mybatis注解的步骤:
/**
* 可根据需要自行定制功能
*/@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public@interface Table {
String name() default "";
}
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public@interface Column {
// 列名 默认为""
String name() default "";
// 长度 默认为255
int
length() default 255;
// 是否为varchar 默认为true
boolean
varchar() default
true;
// 是否为空 默认可为空
boolean
isNull() default
true;
}
/**
* 有需要可以拆分成更小粒度
* @author walidake
*
*/@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public@interface PrimaryKey {
String name() default "";
}
完成Step 1,接下来是Step 2。
@Tablepublicclass
Person {
@PrimaryKey
privateint id;
@Column(isNull = false, length = 20)
private String username;
...
}
Step 3,新建一个叫做SqlUtil的类,使用Class(实体类).isAnnotationPresent(Table.class)取到@Table注解的内容。
而我们如何取到@Column和@PrimaryKey的内容?
使用反射,我们可以很容易做到。
// 反射取得所有Field
Field[] fields = clazz.getDeclaredFields();
...
...// 获取注解对象
column = fields[i].getAnnotation(Column.class);// 设置访问私有变量
fields[i].setAccessible(true);// 取得@Column的内容
columnName = "".equals(column.name()) ? fields[i].getName(): column.name();
反射的内容后面再写。(感觉每一篇都给自己挖了很多坑后面去填)
Step 4套入使用场景
String createSql = SqlUtil.createTable(clazz);
...
connection.createStatement().execute(createSql);
运行结果:
hibernate.png
运行结果正确!
自此我们完成了实战模块的内容。当然关于Hibernate的CRUD也可以用同样的方法做到,更进一步还可以把二级缓存整合进来,实现自己的一个微型框架。尽管现有的框架已经很成熟了,但自己实现一遍还是能收获很多东西。
可以看出来,注解简化了我们的配置。每次使用注解只需要@注解名就可以了,就跟吃春药一样"爽"。不过由于使用了反射,后劲太"猛",jvm无法对代码优化,影响了性能。这一点最后也会提及。
另外提一点,之前想格式化hibernate生成的SQL,做大量搜索后被告知"Hibernate 使用的是开源的语法解析工具 Antlr,需要进行 SQL 语法解析,将 SQL 语句整理成语法树"。也算一个坑吧~
不过后来找到一个除了建表SQL以外的格式化工具类,觉得还不错就也分享了。可以在源码中找到。
最后说点什么
可以发现我们使用运行时注解来搭建我们的袖珍版ORM框架,因为运行时注解来搭建框架相对容易而且适用性也比较广,搭建的框架使用起来也比较简单。但在此基础上因为需要用到反射,其效率性能相对不高。因此,多数Web应用使用运行时注解,而像Android等对效率性能要求较高的平台一般使用源码级别注解来搭建。下一节我们讨论怎么玩一玩源码级注解。