Java反射与注解
反射Reflect
常规我们使用类的方法都是在代码中创建指定类型的实例如Apple apple = new Apple()
,这种写法要求我们在程序执行之初就确定好要使用的类和具体方法,扩展性和灵活性不足。
反射机制(Reflect)则是一种程序执行时冬天获取类和执行方法的能力,该方法突破了Java编译时确定类型的限制,为动态编程提供了底层支持,很多主流框架如Spring
的底层都使用反射实现。
首先通过一个例子直观感受下正向和反射的区别:
public class Reflect {public static void main(String[] args) throws Exception{Person p = new Person("Kevin",19);System.out.println(p.toString());// 反向构造,获取类Class c = Class.forName("Reflect.Person");// 创建实例Object pe = c.newInstance();// 获取方法Method sout = c.getMethod("toString");System.out.println(sout.invoke(pe));}
}
下面详细介绍反射的使用方法。
创建class对象
JVM会为每一个类型生成一个元数据对象class
,该对象存储了类型的完整信息,成员、方法等,反射机制通过该元数据对象实现对类的操作,进而实现动态编程,因为该过程需要借助class
进行,好像一面镜子,所以该操作称为反射。
创建class
对象有三种方法:
Class.forName()
全限定类名。示例为Class c=Class.forName("Reflect.Person");
,全限定类名表示包含包位置的类路径,类似相对位置,但省略表示路径的符号和文件类型标记。- 实例的
getclass
方法。示例Class p = new Person().getClass();
使用实例对象的getClass
方法获取元数据对象。 - 类的
class
属性。实例Class p1=Person.class;
,使用类的class
属性赋值。
获得构造信息及调用
// 获得构造函数,默认无参
Constructor c1=c.getConstructor();
// 针对重载函数,限定参数为指定类型的class
Constructor c2=c.getConstructor(String.class,int.class);
// 使用构造函数创建实例,默认无参构造
Object a=c2.newInstance("kevin",25);
获取类的属性及赋值
// 获取公开属性
Field name=c.getField("name");
// 获取私有属性
Field age=c.getDeclaredField("age");
// 获取属性列表
Field[] list=c.getFields();
Field[] list=c.getDeclaredFields()
// 打破私有封装
age.setAccessible(true);
// 修改实例的指定属性
age.set(a,26)
获取方法信息及调用
// 获取方法
Method getName=c.getMethod("getName");
// 执行方法
getName.invoke(对象,参数)
// 重载方法的获取,方法名,参数类型.class
Method setName=c.getMethod("setName",String.class);
在获取方法的getMethod
源码我们看到是这样定义的public Method getMethod(String name, Class<?>... parameterTypes)
,所以这里补充一个知识,不定长参数。
不定长参数可以表示为类型...参数名
,这种定义方法表示输入该类型的参数有任意个都可以,简单示例如下:
public static void out(String...para){for(int i=0;i<para.length;i++){System.out.println(para[i]);}}public static void main(String[] args) throws Exception{// 参数传多少个字符串都可以out("hello","world");}
可变长参数本质是基于列表实现的,该参数没有强制性,执行时可传可不传,但要放在参数列表的最后。
其他常用方法
作用 | 用法 |
---|---|
类加载器 | Classloader cl=class.getClassLoader() |
获取类名 | getSimpleName()简类名,getName()获取全类名 |
获取修饰符 | getModifiers()获取修饰编号,Modifier.toString()转为修饰字符串 |
获取数据类型 | getReturnType()方法返回值类型、getParameterTypes()获得方法参数类型,getType()属性类型 |
获取父接口 | getInterfaces() |
获取父类 | getSuperclass() |
注解Annotation
注解是一种描述程序元素(类、属性、方法等)的机制,注解本身不影响程序运行,但程序可以获取注解以进行更改运行模式等操作,简单来说就是程序可读的注释。
注解的使用方法为@注解名
,我们在多线程部分重写run方法时,方法上的@override
就是重写注解。
系统注解
编码过程中常用的注解有:
interface
定义注解;
Override
重写注解,有该注解标识的方法编译器会检查是否真的覆写了父类方法;
Deprecated
不建议使用的方法,该注解标识的方法在调用时会有删除线显示,显示效果如下:
自定义注解
注解是Java的基本文件之一,通过创建类给出的文件类型就能看出:
自定义注解的方法是使用@interface
关键字,直接给出示例:
public @interface MyAnnotation {// 注解内的成员变量需要(),可用default 设置默认值,否则需在添加注解时显式指定String value() default "nobody";
}
// 注解修饰Person类,当注解成员为value时可省
@MyAnnotation("person1")
public class Person {private String name;private int age;public static void main(String[] args) {Class clazz = Person.class;Annotation annotation=clazz.getAnnotation(MyAnnotation.class);System.out.println(annotation.toString());}
}
// 输出为@Annotation.MyAnnotation(value=person1)
元注解
即修饰注解的注解,元注解限制了注解能修饰哪些类型的元素,类、成员或者方法,同时规定了注解的作用域。
Target
注解,表明注解的作用域。参数为ElementType
,多个作用域需用{}
包围,其关键字表示不同作用域,分别为:
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
Retention
注解,表明注解的声明周期。参数为RetentionPolicy
,关键字表示的不同生命周期为:
RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃。
RetentionPolicy.CLASS 注解只被保留到编译进行的时候,即保存到class字节码文件中,它并不会被加载到 JVM 中。
RetentionPolicy.RUNTIME 注解可以保留到程序运行,它会被加载进入到 JVM 中,所以在程序运行时可以通过反射获取。
案例—反射获取添加Controller注解的私有成员
通过上面的学习我们了解到,注解应该是与反射共同作用,注解可作为程序元素的进一步补充说明,在反射过程中被获取执行,我们尝试结合反射与注解编写一个案例,首先自定义Controller
注解,用于修饰指定的类,反射读取类的过程中检查是否有该注解修饰,有则获取其私有成员id
。
背景为有Person
和Animal
两个类,Controller
注解有一成员value
默认为nobody
,修饰两个类时该成员会变为person
和animal
,要求利用反射机制,找到Controller
注解修饰后成员为person
的类的有私有成员id
的类,找到后给出提示。
实现的大致思路为:
- 读取配置文件,获取要识别类的信息,使用
Properties
和流对象FileReader
实现。 - 将读取到的类信息按
;
分割成字符串数组,for
循环分别判定。 - 根据类信息反射读取
class
对象,并获得注解信息。 - 注解内循环寻找类型为
Controller
的注解,找到后判定值是否为person
。 - 获取所有成员变量,循环检测权限修饰符是否有
private
,成员名称是否为id
。 - 找到符合条件的输出提示信息。
Animal类雷同,实现代码如下:
@Controller("person")
public class Person {private String name;private int age;private int id;public Person(String name, int age, int id) {this.name = name;this.age = age;this.id = id;}public String getName() {return name;}public int getAge() {return age;}public int getId() {return id;}public static void main(String[] args) {try {FileReader fr =new FileReader("config.properties");Properties prop = new Properties();prop.load(fr);String name = prop.getProperty("classname");String[] classNameArray = name.split(";");for(String className : classNameArray) {Class aclass=Class.forName(className);Annotation[] annotations = aclass.getAnnotations();for(Annotation annotation : annotations) {if(annotation instanceof Controller) {Controller myAnnotation = (Controller) annotation;if(myAnnotation.value().equals("person")) {Field[] declaredField=aclass.getDeclaredFields();for(Field field : declaredField) {if(field.getName().equals("id") && Modifier.toString(field.getModifiers()).contains("private")) {System.out.println("发现目标,当前类为:"+aclass.getTypeName());}}}}}}} catch (FileNotFoundException e) {throw new RuntimeException(e);} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}
}
总结
本文介绍了Java动态操作类的方法反射,以及给类添加信息的注解,反射提供了不同于传统正向先定义再使用类的方法,允许开发者逆向构建实例,并根据注解选择要执行的操作,其本质仍然是基于提高扩展性的设计。
提高扩展性比较简单的方法是使用多态,二者之间有什么区别和联系呢?
首先回顾多态。允许父类接受子类实例,这也允许我们无需精准定义类型即可允许程序,只是不能调用子类方法,我们通过instanceof
判断类型并向下转型即可解决,好像也很方便,但多态的扩展性是有前提的——继承链。
多态的类型宽松仅针对已知继承链内的类,这要求开发者在开发时就设计好所有类的继承关系,并体现在代码中,反射机制可以实现真正突破编译限制,允许在运行时动态操作任意类的元数据,直接调用子类方法而无需转型,甚至直接操作私有成员,是更自由的操作手段。
反射在提供自由的同时也带来了安全风险:
- 打破封装。封装性是面向对象三大核心特征之一,反射操作私有成员的能力直接打破了这一特性,私有成员的更改可能导致类的内部问题。
- 代码注入。恶意代码可能通过
Class.forName("恶意类名")
加载和执行其他类,绕过编译检查。 - 性能开销。反射结构需要动态解析类的结构,带来效率问题,性能敏感场景可能不符合要求。
所以总得来说,反射还是应该少用,不得已要用的地方也要做好校验。