注解与反射

338 阅读6分钟

经过几周的Java学习,《重学Java》系列文章终于进入了注解与反射章节,该章节将对Java的两大特点注解与反射进行讲解,让读者对Java的特性有更深入的了解。

注解

注解(Annotation)是JDK1.5引入的一种注释机制,在Java语言中类、方法、变量、参数和包等都可以被标注,而所标注的内容均能通过反射机制获取标注内容。

注解分为两种:内置注解,自定义注解。

内置注解

内置注解表示该注解为Java自己定义的注解,共7个3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中,可以直接使用。

内置注解中具有代表性的有@Override,@SuppressWarnings,@Deprecated。这三种注解都是在Java编程中我们所常用的注解。

@Override:检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。

@SuppressWarnings:指示编译器去忽略注解中声明的警告。

@Deprecated:标记过时方法。如果使用该方法,会报编译警告。

还剩下4种注解为元注解主要用于用户编写自定义注解时使用在内置注解部分就不多做介绍。

自定义注解

谈到自定义注解首先我们得说说元注解,元注解也是Java中所内置的注解,其作用主要是用来标识自定义注解。其分别有@Retention,@Documented,@Target,@Inherited。

@Retention:标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。

@Documented:标记这些注解是否包含在用户文档中。

@Target:标记这个注解应该是哪种 Java 成员。

@Inherited:标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)。

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Logger {
    boolean isLogger() default true;
}

上面就是一个简单的注解的例子,这里有几点值得我们注意的。

1)ElementType代表了该注解所作用的区域,在JDK中对于Target的定义如下。

public enum ElementType {
	TYPE,
	FIELD,  // 字段
	METHOD, // 方法
	PARAMETER, // 参数
	CONSTRUCTOR, // 构造函数
	LOCAL_VARIABLE,
	ANNOTATION_TYPE,
	PACKAGE, // 包
	TYPE_PARAMETER,
	TYPE_USE
}

2)描述运行环境,一般大部分使用RUNTIME即可。

public enum RetentionPolicy {
	SOURCE,  // 此注解类型的信息只会记录在源文件中,编译时将被编译器丢弃
	CLASS, // 编译器将注解记录在类文件中,但不会加载到JVM中。
	RUNTIME // 注解信息会保留在源文件、类文件中,在执行的时也加载到Java的JVM中,因此可以反射性的读取。
}

注解使用

前面的文章中我们已经定义了Logger注解,在本段将重点讲述如何使用。

public class Test {
    @Logger()
    private LoggerFactory loggerFactory;

    public void demo() {
        loggerFactory.info("test func!");
    }
}

上图就是一个简单的使用方法,我们将之前定义的Logger的Target为method,所以我们只需要把编写好的注解加到了demo这个方法之中即可,当然可以通过修改Target的指向挂到对于的Class上。

写好了那么怎么去使用呢?此时就要引入文章的第二部分反射,在反射部分将讲解如何通过反射去使用我们所写好的注解。

反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

在深入谈论反射之前,我们先介绍一些反射常用的接口API,这些API都是位于Cass.java文件中:

// 获取类类型信息
forName(String className);
// 创建一个类
newInstance();
// 获取类中所有字段
getDeclaredFields();
// 获取类中指定字段
getDeclaredField(String field);
// 获取构造函数
getConstructors();
// 获取当前类的所有方法
getMethods();

通过反射创建一个类:

public class AnnotationManager {

    public static void reflectObject(String className) throws Exception {
        // 获取类型信息
        Class clazz = Class.forName(className);
	// 调用newInstance创建
        LoggerFactory loggerFactory = (LoggerFactory) clazz.newInstance();
	// 触发日志方法
        loggerFactory.info("test");
    }

    public static void main(String[] args) throws Exception {
	// 反射获取对象
        AnnotationManager.reflectObject("annotation.LoggerFactory");
    }
}

上述代码示例就是通过反射创建一个类的过程,十分简单。

我们在实现了通过反射创建类过后,我们再次尝试通过反射去检查指定类是否有没有带指定注解。

public void registerLogger(T t, LoggerFactory loggerFactory) throws Exception {
	// 获取待注入的类型对象
	Class clazz = ((Object)t).getClass();
	// 获取待注入的字段
        Field[] fields = clazz.getDeclaredFields();
	// 遍历字段
        for (Field field : fields) {
	    // 获取注解配置
            Logger logger =  field.getAnnotation(Logger.class);
            // 判断字段是否满足赋值条件
            if(logger.isLogger() && field.getType().getSimpleName().equals("LoggerFactory")) {
                field.setAccessible(true);
                // 赋值
		field.set(t, loggerFactory);
            }
        }
}

假如我们的注解是加载给字段赋值,只需要调用getAnnotation接口传入对应的class类型即可。上述代码就是一个给指定字段赋值的操作。

实战

最后我们学完了注解和反射我们来结合所学的知识来实战一番。

目标:我们通过指定注解给指定字段赋值,最后并成功调用。

本次实战将分为3步:定义注解,定义测试类,定义注解注入器。

定义注解:

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Logger {
    boolean isLogger() default true;
}

定义了一个Logger注解,Target标记为作用在字段属性上,Retention选择为运行时,同时提供默认值为true。

定义测试类:

注解定义完成,我们就需要定义一个完整的测试类来验证。测试类分为两部分,一部分是待注入的对象,另一部分是待注入的方法。

public class Test {

    @Logger()
    private LoggerFactory loggerFactory; // 待注入的对象

    public void demo() { // 待测试的方法
        loggerFactory.info("test func!");
    }
}

上述代码就是所定义的一个简单的测试类,其中loggerFactory为待注入的对象,之后我们将通过Logger注解以及注解解析器完成对loggerFactory的初始化工作,最后在在demo function中进行校验。

定义注解解析器

public class LoggerManager<T> {

    public static LoggerFactory registerObject(String className) throws Exception {
        Class clazz = Class.forName(className);
        return (LoggerFactory) clazz.newInstance();
    }

    public void registerLogger(T t, LoggerFactory loggerFactory) throws Exception {
	// 获取传入对象的类型信息
        Class clazz = ((Object)t).getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            Logger logger =  field.getAnnotation(Logger.class);
	    // 判断注入是否满足条件
            if(logger.isLogger() && field.getType().getSimpleName().equals("LoggerFactory")) {
                field.setAccessible(true);
		// 注入loggerFactory工具类
                field.set(t, loggerFactory);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        LoggerFactory loggerFactory = LoggerManager.registerObject("annotation.LoggerFactory");
        LoggerManager<Object> loggerManager = new LoggerManager<Object>();
        Test test = new Test();
        loggerManager.registerLogger(test, loggerFactory);
	// 调用方法验证
        test.demo();
    }
}

上述就是一个完整的注解解析器,我们通过registerObject把LoggerFactory获取到,然后公共registerLogger注入到Test对象中去最后再通过test.demo调用即可。

总结

通过今天的描述我们可以大概知道了注解和反射在Java中的地位,注解自JDK1.5发布开始之间已经过去多年,其发展已经十分壮大,在各种框架中得到了应用,其中最具有代表性的就是Spring系列框架,其将注解和反射运用到了极致,后续为也会做Spring相关的分析,输出相关文章,期待与大家一同进步。