一、Java注解的概念
\qquad
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。
\quad
接下来介绍几个常见的注解:
@Override
:检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误@Deprecated
:标记过时方法,如果使用该方法,会报编译警告@SuppressWarnings
:指示编译器去忽略注解中声明的警告
二、元注解
\quad 元注解:作用在其他注解上的注解称为元注解,可以用在我们自定义的注解上,即解释注解的注解。常见的元注解有以下几个:
@Doucument
:标记这些注解是否包含在用户文档中,即描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息@Target
:描述注解的使用范围,即被修饰的注解可以用在什么地方
Target注解用来说明那些被它所注解的注解类可修饰的对象范围:注解可以用于修饰 packages、types(类、接口、枚举、注解类)、类成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数),在定义注解类时使用了@Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中。
public enum ElementType {
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}
@Retention
:描述注解保留的时间范围,即被描述的注解在它所修饰的类中可以被保留到何时。
public enum RetentionPolicy {
SOURCE, // 源文件保留
CLASS, // 编译期保留,默认值
RUNTIME // 运行期保留,可通过反射去获取注解信息
}
@Inherited
:使被它修饰的注解具有继承性,如果某个类使用了被@Inherited
修饰的注解,则其子类将自动具有该注解
\quad
任何注解都至少有@Target
和@Retention
两个元注解修饰。
三、如何写一个注解
\quad
假设我们定义一个名为Name的注解,用于检测给出的名字字符串长度是否超过5,我们首先需要指明该注解的作用范围是FIELD
,表明适用于成员变量;接着指明注解保留的时间范围。如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
int length() default 3;
}
public class Animal {
@Name(length = 5)
private String name;
@Deprecated
public void eat(){
System.out.println("eat food...");
}
public static void main(String[] args) {
Animal animal = new Animal();
animal.eat();
}
}
四、反射
\quad 单独注解基本没啥用,需要结合反射才能发挥其作用。反射是在运行状态中(不是编译时期),对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意方法和属性。这种动态获取信息以及动态调用对象的方法的功能称为java语言反射机制。反射机制能完成下列事情:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法,生成动态代理
\quad
想要完成上述功能,我们就需要有一个类,该类可以获取其他类的各种信息,比如类的全类名,类的方法,成员变量,构造器,并能直接new一个其他类的对象。Java中的Class
类就能完成上述功能,Class
类用于获取任意一个类的详细信息,并以此实现对其他类的操作,这就是反射的起源,针对任何一个你想勘探的类,你只有先为它生成一个Class类的对象,接下来才能通过Class对象获取其详细内容。
获取类对象的三种方法
// 获取Animal的Class对象的三种方式
// 1. 使用目标类的.class属性得到其类对象
Class clazz1 = Animal.class;
// 2. 使用目标类的全类名得到
Class clazz2 = Class.forName("注解.Animal");
// 3. 使用目标类的对象的.getClass()方法得到
Animal animal = new Animal();
Class clazz3 = animal.getClass();
// 三种方式对于同一个目标类创建的均为同一个类对象
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true
根据类对象操作任意一个类
a). 获取一个类的字段Field
类对象.getFields()
:获得所有public修饰的字段类对象.getDeclaredFields()
:获得所有的字段- 上述操作返回
Fields[]
数组,里面存放着符合要求的所有字段对应的Field
类,Field
类常见的操作是取出该字段的名字getName()
,取出某个对象对应该字段的值get(某个对象)
,设置某个对象该字段的值set(某个对象, val)
- 对于私有属性,如果要赋值,需要开启权限:
field.setAccessible(true)
\quad
声明一个Animal
类,然后在GetFields
类中测试上述方法,如下:
public class Animal {
@Name(length = 5)
public String name;
private int age;
@Deprecated
public void eat(){
System.out.println("eat food...");
}
}
public class GetField {
public static void main(String[] args) throws Exception{
// 获取Animal类的Class对象
Class<Animal> animalClass = Animal.class;
// 获取字段
Field[] fields = animalClass.getFields(); // 获取public修饰的字段
for (Field field : fields) {
System.out.println(field.getName()); // 获得字段的名字。仅有name字段
}
Field[] declaredFields = animalClass.getDeclaredFields(); // 拿到所有的字段
for (Field declaredField : declaredFields) {
System.out.println(declaredField.getName()); // name和age字段均有
}
// 设置字段信息
Animal animal = new Animal();
Field field = animalClass.getField("name");
field.setAccessible(true); // 开启权限,这样才能访问私有属性
System.out.println(field.get(animal)); // 获取animal对象字段name中的值,未赋值则为null
field.set(animal, "张三"); // 给animal对象的name字段设置值为“张三”
System.out.println(field.get(animal)); // 张三
}
}
b). 获取一个类的方法Method
类对象.getMethod(String类型的方法名,方法参数的类型)
:返回对应的方法Method
类- 调用
Method.invoke(测试对象,可变长参数)
:该方法可以调用对象的函数
\quad
我们给Animal
类加入新的eat
方法,并允许传入参数。接下来创建GetMethods
进行上述功能的测试,如下所示:
public class GetMethods {
public static void main(String[] args) throws Exception{
Animal animal = new Animal();
Class<Animal> animalClass = Animal.class;
Method eat = animalClass.getMethod("eat");
eat.invoke(animal); // 调用该方法
// 调用有参数的eat方法
Method eatFood = animalClass.getMethod("eat", String.class);
eatFood.invoke(animal, "骨头");
}
}
c). 获取一个类的构造方法Constructor
- 获取构造器的目的在于new对象
Constructor<Animal> constructor = animalClass.getConstructor();
可以获得animal类的无参构造器Animal animal = constructor.newInstance();
可以获得该类对象- 个人感觉可以不用学,既然获得构造方法目的在于new某个类的对象,其实直接用
类对象.newInstance()
方法可以直接获得对象 newIInstance()
是无参构造器,如果想调用有参构造器,还是需要使用Constructor
public class GetConstructor {
public static void main(String[] args) throws Exception {
Class<Animal> animalClass = Animal.class;
Constructor<Animal> constructor = animalClass.getConstructor();
Animal animal = constructor.newInstance(); // 获得对象
animal.eat();
Animal animal1 = animalClass.newInstance(); // 可以直接获得对象
animal1.eat();
}
}
五、注解+反射大显神通
实战一:声明一个名为Name
的注解,里面包含字段fullName
,该注解可以作用于任意一个类的成员变量上。我要要实现的功能是:给一个类的某个成员变量加入该注解后,可以把注解里面的fullName
字段的值赋值给该变量。(这就是常见的用注解赋值)
a). 实现Name
注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
String fullName() default "张三";
}
b). 实现类Animal
,并将该注解作用于其中的name
成员变量
public class Animal {
@Name(fullName = "李四")
public String name;
}
c). 在TestAnnotation
类中实现将注解中fullName
的值注入对应成员变量的功能
public class TestAnnotation {
public static void main(String[] args) throws Exception{
Animal animal = new Animal();
Class<Animal> animalClass = Animal.class;
// 获取Animal类的注解
A annotation = animalClass.getAnnotation(A.class);
// 获取Animal类name字段的注解
Field name = animalClass.getDeclaredField("name");
name.setAccessible(true);
Name nameAnnotation = name.getAnnotation(Name.class);
// 获取name字段注解的值
String fullName = nameAnnotation.fullName();
System.out.println(fullName); // 李四
// 将注解中的值赋值给animal对象的name字段
name.set(animal, fullName);
System.out.println(name.get(animal)); // 李四
}
}
实战二:写一个配置文件config.properties
,利用Properties
类获得文件中的键值对的值,并取出其中的值用于创建对象。
package ReflectAndAnnotation.sql;
public interface Source {
void select();
}
package ReflectAndAnnotation.sql;
public class Mysql implements Source{
@Override
public void select(){
System.out.println("从mysql中查找");
}
}
package ReflectAndAnnotation.sql;
public class MongoDB implements Source{
@Override
public void select() {
System.out.println("从mongodb中查找");
}
}
package ReflectAndAnnotation.sql;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
public class Test {
@org.junit.Test
public void testProperties() throws Exception{
// 将文件中的key-value信息读入,继承自HashMap
Properties properties = new Properties();
InputStream inputStream = new FileInputStream("C:\\Users\\chengyong\\IdeaProjects\\Java单元测试注解反射\\src\\config.properties");
properties.load(inputStream);
System.out.println(properties.getProperty("target"));
properties.setProperty("c", "d"); // 输入键值对
System.out.println(properties.getProperty("c")); // d
}
public static void main(String[] args) throws Exception{
// 1. 获得配置文件中的键值对信息,并取出需要的信息
Properties properties = new Properties();
InputStream inputStream = new FileInputStream("src/config.properties");
properties.load(inputStream);
String target = properties.getProperty("target"); // ReflectAndAnnotation.sql.Mysql
// 2. 根据取出的类名创建对象,并调用方法
Class<?> aClass = Class.forName(target);
Source o = (Source) aClass.newInstance();
o.select(); // 从mysql中查找
}
}
实战三:创建一个Connection
类,里面包含连接数据库需要的四个字段作为成员变量,现在我们需要从配置文件jdbc.properties
中读取这四个变量的值,让如Connection
的对象中。
jdbc.properties
文件里内容如下:
type=connection.Connection
mysql.host=localhost
mysql.port=3306
mysql.username=root
mysql.password=123456
Connection
类内容:
package connection;
public class Connection {
private String host;
private int port;
private String username;
private String password;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Connection{" +
"host='" + host + '\'' +
", port=" + port +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
- 核心逻辑:
package connection;
import java.util.Properties;
public class Test {
public static void main(String[] args) throws Exception{
// 1. 加载配置文件
Properties properties = new Properties();
properties.load(Test.class.getClassLoader().getResourceAsStream("jdbc.properties"));
// 2. 利用反射创建类对象
Class<?> aClass = Class.forName(properties.getProperty("type"));
Connection connection = (Connection) aClass.newInstance();
// 3. 将配置文件中的值传入connection对象
connection.setHost(properties.getProperty("mysql.host"));
connection.setPort(Integer.parseInt(properties.getProperty("mysql.port")));
connection.setPassword(properties.getProperty("mysql.password"));
connection.setUsername(properties.getProperty("mysql.username"));
System.out.println(connection);
}
}