Java注解和反射

一、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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值