Java注解和Java反射
# 注解是什么
Java注解(Annotation)又称Java注解,是JDK5.0引入的一种注释机制。
Java语言中的类、方法、变量、参数和包等都可以被注解。和Javadoc不同,Java注解可以通过反射获取注解内容。在编译器生成类文件时,注解可以被嵌入到字节码中。Java虚拟机可以保留注解内容,在运行时可以获取到注解内容。当然它也支持自定义Java注解。
# 一句话概括
在实例化之前就给类贴上标签
# 内置注解
Java自带7种注解,3个在java.lang
中,4个在java.lang.annotation
中。
# 在java.lang
中的注解
java.lang
中的注解都是作用于代码的,有以下几种:
@Override
:检查该方法是否是重载方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。@Deprecated
:标记过时方法。如果使用该方法,会报编译警告。@SuppressWarnings
:指示编译器去忽略注解中声明的警告。
从 Java 7 开始,额外添加了 3 个注解:
@SafeVarargs
:Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。@FunctionalInterface
:Java 8 开始支持,标识一个匿名函数或函数式接口(用于函数式编程,经它标记的函数式接口可以很容易转换为 Lambda 表达式)。
# 在java.lang.annotation
中的注解
在java.lang.annotation
中的是作用于其他注解的注解,这些注解是给程序员用来构造自定义注解的。java.lang.annotation
中的注解又称为元注解。
@Retention
:标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。@Documented
:标记这些注解是否包含在用户文档中。@Target
:标记这个注解应该是哪种 Java 成员。@Inherited
:标记这个注解是继承于哪个注解类(默认注解并没有继承于任何子类)
从 Java 8 开始,额外添加了 1 个注解:
@Repeatable
:Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
# 自定义注解
# 定义注解
下面是几个自定义注解的示例,下面几节都将围绕着这几个注解展开。
# 不带参数的注解
不带参数的注解可以看作是一个纯粹的标记,标明被注解物“是什么”。
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Inherited
public @interface ExampleAnnotation {}
2
3
4
5
# 带参数的注解
带参数的注解可以看作是用来标明被注解物“有何属性”。注解的没有参数只有接口方法,获取注解的变量都是以调用这些方法的方式完成的;注解的接口方法后边可以跟上一个default
用来指定方法默认的返回值。
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Inherited
public @interface ExampleAnnotation1 {
String exampleProp() default "";
int exampleProp1();
}
2
3
4
5
6
7
8
比如上面这个注解在调用时要写成:
@ExampleAnnotation1(exampleProp="哈哈哈",exampleProp1=1)
而如果注解的参数只有一个,则可以省去参数列表,例如下面这个注解:
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
public @interface ExampleAnnotation2 {
String exampleProp() default "";
}
2
3
4
5
6
在调用时就可以写成这两种形式:
@ExampleAnnotation1(exampleProp="哈哈哈")
@ExampleAnnotation1("哈哈哈")
2
另外有一点需要注意:全部属性都有默认值的注解在调用时可以省去注解值但不能省去括号。
# 元注解含义解释
# @Retention(RetentionPolicy.RUNTIME)
Retention 的英文意为保留期的意思。标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
它是一个有参数的注解,后面紧跟的括号中需要有一个参数,其取值有如下几种:
RetentionPolicy.SOURCE
注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。RetentionPolicy.CLASS
注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。RetentionPolicy.RUNTIME
注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
因此,上面的这个自定义注释ExampleAnnotation
可以保留到程序运行时。
# @Documented
带有这个标记的注解中的元素将被包含到Javadoc中。ez,不解释
# @Target(ElementType.TYPE)
Target 是目标的意思,@Target 指定了注解可以运用的地方。这个元注解限定了定义的注解可以注解的范围。
它是一个有参数的注解,后面紧跟的括号中需要有一个参数,其取值有如下几种:
ElementType.ANNOTATION_TYPE
:可以给一个注解进行注解ElementType.TYPE
:可以给一个类型进行注解,比如类、接口、枚举ElementType.CONSTRUCTOR
:可以给类的构造方法进行注解ElementType.METHOD
:可以给类方法进行注解ElementType.PARAMETER
:可以给一个方法内的参数进行注解ElementType.FIELD
:可以给属性进行注解(类成员变量)ElementType.LOCAL_VARIABLE
:可以给局部变量进行注解ElementType.PACKAGE
:可以给一个包进行注解
因此,上面的元注解表示ExampleAnnotation
可以给一个类型进行注解。
# @Inherited
带有@Inherited
的注解如果用来注解的了一个类A
,那么当A
被继承时且继承者没有进行其他注解时,那么这个继承者也会自动带上这个注解。如下:
- 用
@ExampleAnnotation
注解了A
:
@ExampleAnnotation
public class A{}
2
- 再用
B
继承A
,并且加任何其他注解:
public class B extends A{}
- 就相当于
B
也被@ExampleAnnotation
注解了:
@ExampleAnnotation
public class B extends A{}
2
# @Repeatable
@Repeatable
是可重复的意思,表明这个注解会多次应用。
- 什么场景下我们需要多次应用一个注解?
- 当注解的值可以同时取多个,并且具体取几个还不确定时。
例如某个小公司里一个人要干几个人的活:
@interface Persons {
Person[] value();
}
@Repeatable(Persons.class)
@interface Person{
String role() default "";
}
@Person(role="产品经理")
@Person(role="测试")
@Person(role="公关")
public class SuperMan{}
2
3
4
5
6
7
8
9
10
11
12
13
或者也可以:
@Persons({"产品经理","测试","公关"})
public class SuperMan{}
2
这样就能给一个对象标上数量不等的多个注解了。其中这里的Persons
称为容器注解,专用于放其他注解。容器注解中必须有一个名叫value
的属性,且其属性类型必须是一个被@Repeatable
注解过的注解类型数组(注意是数组)。
# 自定义注解的使用
注解的使用方法包含于反射的使用方法中,见下文。
# 反射
# 反射是什么
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
# 反射所用到的类
类 | 作用 |
---|---|
Class | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field | 代表类的成员变量(成员变量也称为类的属性) |
Method | 代表类的方法 |
Constructor | 代表类的构造方法 |
例如,现在有一个类Cls
在包com.here.is
中:
package com.here.is
public class Cls{
public String publicField;
private int privateField;
public void publicMethod(String v){}
private int privateMethod(int v){}
public Cls(int a,String b){}
private Cls(String b){}
}
2
3
4
5
6
7
8
9
那么可以进行这么一堆初始化:
import com.here.is
Class<?> cls = Class.forName("com.here.is.Cls");//反射获取一个类
Class<?> cls = Cls.class//或者这样也行
Field publicVal = cls.getField("publicField");//反射获取一个变量,仅限public
Field privateVal = cls.getDeclaredField("privateField");//反射获取任意一个变量
Method publicMeth = cls.getMethod("publicMethod",String.class);//反射获取一个方法,仅限public。输入方法名的后面跟上参数类型列表
Method privateMeth = cls.getDeclaredMethod("privateMethod",int.class);//反射获取任意一个方法。输入方法名的后面跟上参数类型列表
Constructor<?> publicConstr = cls.getConstructor(int.class,String.class);//反射获取构造方法,仅限public。输入参数类型列表
Constructor<?> publicConstr = cls.getDeclaredConstructor(String.class);//反射获取任意一个构造方法。输入参数类型列表
2
3
4
5
6
7
8
9
10
11
12
13
# 这反射类的类方法详解
# 与类的获得相关的方法
这些方法都在Class
中:
方法 | 用途 |
---|---|
forName(String className) | 根据类名返回类的对象(静态方法) |
asSubclass(Class<U> clazz) | 把传递的类的对象转换成代表其子类的对象 |
Cast | 把对象转换成代表类或是接口的对象 |
getClassLoader() | 获得类的加载器 |
getClasses() | 返回一个数组,数组中包含该类中所有公共类和接口类的对象 |
getDeclaredClasses() | 返回一个数组,数组中包含该类中所有类和接口类的对象 |
getName() | 获得类的完整路径名字 |
newInstance() | 创建类的实例 |
getPackage() | 获得类的包 |
getSimpleName() | 获得类的名字 |
getSuperclass() | 获得当前类继承的父类的名字 |
getInterfaces() | 获得当前类实现的类或是接口 |
# 与构造和实例化相关的方法
# 获得构造函数
方法 | 用途 |
---|---|
getConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的公有构造方法 |
getConstructors() | 获得该类的所有公有构造方法 |
getDeclaredConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的构造方法 |
getDeclaredConstructors() | 获得该类所有构造方法 |
# 进行实例化
Class
类中有一个newInstance()
用于实例化,Constructor
类中也有一个newInstance()
方法用于实例化,用法一样(其实Constructor
类只有这一个方法)。
上接上一节的一堆初始化,这里举几个用反射进行实例化的例子:
//通常用这个
Object obj = cls.newInstance(1,"哈哈哈");
Cls c = (Cls) obj;
//还可以用public的构造方法
Object obj1 = publicConstr.newInstance(1,"哈哈哈");
Cls c1 = (Cls) obj1;
//private的构造方法调用了setAccessible(true)之后也是可以的
privateConstr.setAccessible(true);
Object obj2 = privateConstr.newInstance("哈哈哈");
Cls c2 = (Cls) obj2;
2
3
4
5
6
7
8
9
10
11
12
# 与属性相关的方法
# 获得属性
Class
类中与属性相关的方法全部是用来获得属性的。
方法 | 用途 |
---|---|
getField(String name) | 获得某个公有的属性对象 |
getFields() | 获得所有公有的属性对象 |
getDeclaredField(String name) | 获得某个属性对象 |
getDeclaredFields() | 获得所有属性对象 |
# 操作属性
用来操作属性的方法都在Field
类中,Field
类中的方法全部是用来操作属性的方法。
方法 | 用途 |
---|---|
equals(Object obj) | 属性与obj相等则返回true |
get(Object obj) | 获得obj中对应的属性值 |
set(Object obj, Object value) | 设置obj中对应属性值 |
上接那个实例化的例子,这里举几个用反射获取变量值和设置变量的例子:
publicVal.setAccessible(true);
String publicValue = (String) publicVal.get(obj);//获取变量
publicVal.set(obj,"呵呵呵");//设置变量
privateVal.setAccessible(true);
int privateValue = (int) privateVal.get(obj);//获取变量
privateVal.set(obj,"2");//设置变量
2
3
4
5
6
7
# 与方法相关的方法
# 获得方法
Class
类中与方法相关的方法全部是用来获得方法的。
方法 | 用途 |
---|---|
getMethod(String name, Class...<?> parameterTypes) | 获得该类某个公有的方法 |
getMethods() | 获得该类所有公有的方法 |
getDeclaredMethod(String name, Class...<?> parameterTypes) | 获得该类某个方法 |
getDeclaredMethods() | 获得该类所有方法 |
# 调用方法
Method
类中只有一个方法invoke(Object obj, Object... args)
,用于调用该对象对应的方法。
上接那个实例化的例子,这里举几个用反射调用方法的例子:
publicMeth.invoke(obj,"嚯嚯嚯");//什么都不返回
privateMeth.setAccessible(true);
int result = (int) privateMeth.invoke(obj,3);//返回int
2
3
# 与注解相关的方法
与注解相关的方法在Class
、Field
、Method
和Constructor
中都有:
方法 | 用途 |
---|---|
getAnnotation(Class<? extends Annotation> annotationClass) | 返回该类中与参数类型匹配的公有注解对象 |
getAnnotations() | 返回该类所有的公有注解对象 |
getDeclaredAnnotation(Class<? extends Annotation> annotationClass) | 返回该类中与参数类型匹配的所有注解对象 |
getDeclaredAnnotations() | 返回该类所有的注解对象 |
此外,在Class
中还有两个用于判断注解类的方法:
方法 | 用途 |
---|---|
isAnnotation() | 如果这个类型是注解类型则返回true |
isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果这个类型是继承于annotationClass 类型的注解类型则返回true |
那么,上接那个一个人要干几个人的活的小公司一般社员,这里举几个反射获取注解的用法:
boolean isAnnotation = Persons.class.isAnnotation();//Persons类是注解类吗?是的。故为true
boolean isPresent = SuperMan.class.isAnnotationPresent(Persons.class);//SuperMan有被Persons类注解吗?有的。故为true
Persons superManAnnotation = SuperMan.class.getAnnotation(Persons.class);//获取注解
String[]values = superManAnnotation.value();//获取注解的值
2
3
4
再来一个方法注释的例子。例如有一方法注释:
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface MyTestMethod {}
2
3
4
然后我们告诉测试人员,这个@MyTestMethod
注释的方法可以用来测试程序是否运作正常,让测试人员将其放在测试类里面:
public class SuperManTest{
@MyTestMethod
public boolean SuperManTest1(){}
@MyTestMethod
public boolean SuperManTest2(){}
@MyTestMethod
private boolean PrivateSuperManTest1(){}
@MyTestMethod
private boolean PrivateSuperManTest2(){}
}
2
3
4
5
6
7
8
9
10
11
12
13
然后就可以这么写测试代码:
SuperManTest testObj = new SuperManTest();//先实例化
Class cls = testObj.getClass();//再获取它的类
Method[] methods = cls.getDeclaredMethods();//再获取它的方法
for(Method method:methods){//检查每一个方法
if(method.isAnnotationPresent(MyTestMethod.class)){//如果有@MyTestMethod标记
try{//那就运行它
method.setAccessible(true);
method.invoke(testObj,null);
} catch (Exception e){
//错误处理
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
从而可以运行SuperManTest
这个类中所有打了@MyTestMethod
注解的类,从而通过这一个个测试函数对程序进行测试。
这也就是Java中常用的测试工具JUnit的基本原理。
# Spring中的反射与注解
以 Spring Bean 为例,介绍反射在IoC中的应用。
这里只做原理上的简要介绍。
# 以XML文件定义的Bean组装
在 Spring 中,构成应用程序主干并由Spring IoC容器管理的对象称为bean。
——bean是一个由Spring IoC容器实例化、组装和管理的对象。
在曾经的Spring使用XML文件定义一系列Bean的组装过程。这种定义方式比较直观,并且能极大地降低代码的耦合度。
# Bean
Bean是一种Java对象。
根据bean规范编写出来的类,并由Bean容器生成的对象就是一个Bean。
# Bean容器
Bean容器,或称Spring IoC容器,主要用来管理对象和依赖,以及依赖的注入。
# Bean规范
- 所有属性为private
- 提供默认构造方法
- 提供getter和setter
- 实现serializable接口
# XML定义的 Spring Bean 组装过程
例如,现在有一个符合Bean标准的类Cls
在包com.here.is
中:
package com.here.is
public class Cls implements Serializable{
private String aField;
public String getAField(){
return aField;
}
public void setAField(String aField){
this.aField=aField;
}
private int anotherField;
public String getAnotherField(){
return anotherField;
}
public void setAnotherField(String anotherField){
this.aField=anotherField;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
然后XML文件里面有这样一段定义了一个Bean的组装:
<bean id="1" class="com.here.is.Cls">
<!--property配置需要依赖注入的属性-->
<property name="AField" value="aVal"/>
<property name="AnotherField" value="anotherVal"/>
</bean>
2
3
4
5
那么,当程序初始化的时候,Spring IoC容器会执行类似下面的操作:
Class cls = Class.forName("com.here.is.Cls");//获取类型
Object bean = cls.newInstance();//默认构造函数创建实例
//通过解析XML获取到要注入的两个属性
//然后进行注入
String property1 = "AField";
String value1 = "aVal"
Method method1 = cls.getMethod("get"+property1,value1.class);//获取setter方法
method1.invoke(bean,value1);//进行set操作,即注入
//另外一个property注入过程同上
String property2 = "AnotherField";
String value2 = "aVal"
Method method2 = cls.getMethod("get"+property2,value2.class);
method2.invoke(bean,value2);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 注解定义的Bean组装
XML方法虽然直观,但是当工程变大了之后,XML文件变得很长,不好维护,所以后来才引入了注解方法。
注解方法的可维护性好但是耦合度比较高,是为了可维护性而对耦合性做出的妥协。
# 注解定义的核心
用注解和用XML最大的不同在于,XML是独立于代码的,而注解散布在代码中(这也是注解耦合性高的主要原因)。
XML定义的Bean组装其核心是XML解析器,对应的注解定义的核心也就是注解扫描和解析器。注解扫描器用于扫描代码内指定部分的注解,交给解析器进行分析;解析器对扫描到的注解进行解析,然后就可以和XML解析器一样进行Bean的组装了。
# 几个重要注解
@Component
:是一个通用的Spring容器管理的单例bean组件,这个注解表示被标注的类是一个组件,将会被容器自动扫描并创建实例。它有三个拓展注解@Service
,@Controller
,@Repository
。用这些注解对应用进行分层之后,就能将请求处理,义务逻辑处理,数据库操作处理分离出来,为代码解耦,也方便了以后项目的维护和开发。@Repository
注解在持久层中,具有将数据库操作抛出的原生异常翻译转化为spring的持久层异常的功能。注解类作为DAO对象(数据访问对象,Data Access Objects),这些类可以直接对数据库进行操作。@Controller
层是spring-mvc的注解,注解类进行前端请求的处理,转发,重定向。包括调用Service层的方法。@Service
层是业务逻辑层注解,这个注解只是标注该类处于业务逻辑层。
@Autowired
:这个注解是一个针对成员变量的注解,表示被注解字段需要由程序来为其自动赋值。@Resource
:这个注解功能和@Autowired
相同,但是它是JSR-250标准的注解,是属于J2EE的(它们的异同点)。
上面那个Bean定义+XML就可以转化为下面这样(看,耦合了):
package com.here.is
@Component(id="1")
public class Cls implements Serializable{
@Autowired(value="aVal")
private String aField;
public String getAField(){
return aField;
}
public void setAField(String aField){
this.aField=aField;
}
@Autowired(value="anotherVal")
private int anotherField;
public String getAnotherField(){
return anotherField;
}
public void setAnotherField(String anotherField){
this.aField=anotherField;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
经过注解扫描和解析器处理后就会进行和上面XML一样的反射装配操作。