反射是Java语言中的一个关键特性,它允许程序在运行时访问类、接口、字段和方法的信息,并且可以动态地调用方法或改变字段值。
以下是关于Java反射的一些主要方面:
-
类加载器(Class Loader):
- Java反射开始于获取类的
Class
对象。这个对象可以通过多种方式获得,如通过类名直接获取(Class.forName("className")
),通过对象实例(object.getClass()
),或者已知的类直接调用.class
方法。
- Java反射开始于获取类的
-
获取信息:
- 一旦有了
Class
对象,你可以获取该类的各种信息,比如类名、父类、接口、构造函数、字段(Field)、方法和注解等。
- 一旦有了
-
动态操作:
- 使用反射API可以动态地创建类的实例、调用方法以及访问和修改字段值。
- 例如,可以使用
Class
对象的newInstance()
方法来创建一个类的实例,或者通过Method
对象的invoke()
方法来调用一个特定的方法。
-
性能:
- 反射通常比直接的Java代码要慢,因为它涉及到动态解析类型,而不是静态编译时的类型检查。因此,在性能敏感的应用中,过度使用反射可能会影响性能。
-
安全性:
- 反射可能违反Java的访问控制机制,因为它可以访问私有成员。因此,在使用反射时需要谨慎,避免破坏封装性和潜在的安全风险。
-
应用场景:
- 框架开发:很多现代Java框架,如Spring和Hibernate,广泛使用反射来实现灵活的依赖注入和持久化。
- 自动化测试:反射允许测试框架动态调用私有方法或设置私有字段的值,以进行单元测试。
- 动态加载插件或扩展:可以在运行时加载未知的类或接口,并执行相应的功能。
-
限制:
- 某些Java环境可能对反射API的使用施加限制,特别是在安全性和模块化方面。
反射是一个非常强大但也需要谨慎使用的工具,它可以提供极大的灵活性和动态性,但也可能导致代码难以理解和维护,以及潜在的性能和安全问题。
反射的概述
通俗理解:
-
利用反射创建的对象可以无视修饰符调用类里面的内容
-
可以跟配置文件结合起来使用,把要创建的对象信息和方法写在配置文件中。
读取到什么类,就创建什么类的对象
读取到什么方法,就调用什么方法
此时当需求变更的时候不需要修改代码,只要修改配置文件即可。
获取Java类的字节码文件对象有三种常见方式:
- 通过对象实例:如果有一个对象实例,可以直接调用该对象的
getClass()
方法来获取对应的Class
对象。这种方式是在有对象实例的情况下最直接的方法。
对象.getClass
- 使用类名:可以直接使用类名后跟
.class
来获取Class
对象。例如,String.class
会返回String
类的Class
对象。这种方式适用于已知类名且不需要创建对象实例的情况。
类名.class
- 通过Class类的forName方法:可以使用
Class
类的静态方法forName(String className)
来获取Class
对象。这个方法接受一个完全限定类名(包括包名)作为参数,并返回对应的Class
对象。这种方式在动态加载类时非常有用,尤其是在类的全名已知但在编译时不确定类是否存在的场景下。
Class.forName("全类名")
public class test5 {
public static void main(String[] args) throws ClassNotFoundException {
//最常用
Class<?> clazz1 = Class.forName("com.itheima.test5.Student");
System.out.println(clazz1);//class com.itheima.test5.Student
//参数传递
Class clazz2 = Student.class;
System.out.println(clazz2);//class com.itheima.test5.Student
System.out.println(clazz1 == clazz2);//true
//有对象使用
Student s = new Student();
Class clazz3 = s.getClass();
System.out.println(clazz1 == clazz2);//true
System.out.println(clazz2 == clazz3);//true
}
}
字节码文件和字节码文件对象
java文件:就是我们自己编写的java代码。
字节码文件:就是通过java文件编译之后的class文件(是在硬盘上真实存在的,用眼睛能看到的)
字节码文件对象:当class文件加载到内存之后,虚拟机自动创建出来的对象。
这个对象里面至少包含了:构造方法,成员变量,成员方法。
而我们的反射获取的是什么?字节码文件对象,这个对象在内存中是唯一的。
获取构造方法
获取构造方法
规则:
get表示获取
Declared表示私有
最后的s表示所有,复数形式
如果当前获取到的是私有的,必须要临时修改访问权限,否则无法使用
方法名 | 说明 |
---|---|
Constructor<?>[] getConstructors() | 获得所有的构造(只能public修饰) |
Constructor<?>[] getDeclaredConstructors() | 获得所有的构造(包含private修饰) |
Constructor<T> getConstructor(Class<?>... parameterTypes) | 获取指定构造(只能public修饰) |
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) | 获取指定构造(包含private修饰) |
public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.获得整体(class字节码文件对象)
Class clazz = Class.forName("com.itheima.test6.Student");
//2.获取构造方法对象
//获取所有构造方法(public)
Constructor[] constructors1 = clazz.getConstructors();
for (Constructor constructor : constructors1) {
System.out.println(constructor);
}
//public com.itheima.test6.Student()
//public com.itheima.test6.Student(java.lang.String,int)
System.out.println("=======================");
//获取所有构造(带私有的)
Constructor[] constructors2 = clazz.getDeclaredConstructors();
for (Constructor constructor : constructors2) {
System.out.println(constructor);
}
//public com.itheima.test6.Student(java.lang.String)
//public com.itheima.test6.Student()
//protected com.itheima.test6.Student( int)
//private com.itheima.test6.Student(java.lang.String, int)
System.out.println("=======================");
//获取指定的空参构造
Constructor con1 = clazz.getConstructor();
System.out.println(con1);
// public com.itheima.test6.Student()
Constructor con2 = clazz.getConstructor(String.class);
System.out.println(con2);
//public com.itheima.test6.Student(java.lang.String)
System.out.println("=======================");
//获取指定的构造(所有构造都可以获取到,包括public包括private)
Constructor con3 = clazz.getDeclaredConstructor();
System.out.println(con3);
//public com.itheima.test6.Student()
//了解 System.out.println(con3 == con1);
//每一次获取构造方法对象的时候,都会新new一个。
Constructor con4 = clazz.getDeclaredConstructor(String.class);
System.out.println(con4);
//public com.itheima.test6.Student(java.lang.String)
System.out.println("================");
//表示临时取消权限校验
//Constructor con3 = clazz.getDeclaredConstructor(String.class,int.class);
con3.setAccessible(true);
Student stu = (Student) con3.newInstance("张三", 23);
System.out.println(stu.getName() + stu.getAge());
//张三23
}
}
获取成员变量
规则:
get表示获取
Declared表示私有
最后的s表示所有,复数形式
如果当前获取到的是私有的,必须要临时修改访问权限,否则无法使用
方法名:代码示例:
方法名 | 说明 |
---|---|
Field[] getFields() | 返回所有成员变量对象的数组(只能拿public的) |
Field[] getDeclaredFields() | 返回所有成员变量对象的数组,存在就能拿到 |
Field getField(String name) | 返回单个成员变量对象(只能拿public的) |
Field getDeclaredField(String name) | 返回单个成员变量对象,存在就能拿到 |
代码示例:
public class MyReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//获取成员变量对象
//1.获取class对象
Class clazz = Class.forName("com.itheima.test7.Student");
//2.获取成员变量的对象(Field对象)只能获取public修饰的
Field[] fields1 = clazz.getFields();
for (Field field : fields1) {
System.out.println(field);
}
System.out.println("===============================");
//获取成员变量的对象(public + private)
Field[] fields2 = clazz.getDeclaredFields();
for (Field field : fields2) {
System.out.println(field);
}
//如果获取的属性是不存在的,
System.out.println("===============================");
//获得单个成员变量对象那么会报异常
//Field field3 = clazz.getField("aaa");
//System.out.println(field3);//NoSuchFieldException
Field name = clazz.getDeclaredField("name");
System.out.println(name);
//获取权限修饰符
int modifiers = name.getModifiers();
System.out.println(modifiers);//2
System.out.println("===============================");
//获取成员变量的名字
String n = name.getName();
System.out.println(n);
System.out.println("===============================");
//获取成员变量的数据类型
Class<?> type = name.getType();
System.out.println(type);
//获取成员变量的值
Student s = new Student("Zhangsan", 23, "男");
name.setAccessible(true);
String value = (String) name.get(s);
System.out.println(value);
//Zhangsan
//修改对象里面里面记录的值
name.set(s, "lisi");
System.out.println(s.getName());
//lisi
}
}
public class Student {
private String name;
private int age;
public String gender;
public Student() {
}
public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
获取成员方法
规则:
get表示获取
Declared表示私有
最后的s表示所有,复数形式
如果当前获取到的是私有的,必须要临时修改访问权限,否则无法使用
方法名 | 说明 |
---|---|
Method[] getMethods() | 返回所有成员方法对象的数组(只能拿public的) |
Method[] getDeclaredMethods() | 返回所有成员方法对象的数组,存在就能拿到 |
Method getMethod(String name, Class<?>... parameterTypes) | 返回单个成员方法对象(只能拿public的) |
Method getDeclaredMethod(String name, Class<?>... parameterTypes) | 返回单个成员方法对象,存在就能拿到 |
代码示例:
public class MyReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//1.获取class对象
Class<?> clazz = Class.forName("com.itheima.test8.Student");
//2.获取方法
//getMethods可以取父类中p获ublic修饰的方法
Method[] methods1 = clazz.getMethods();
for (Method method : methods1) {
System.out.println(method);
}
System.out.println("===========================");
//获取所有的方法(包含私有)
//但是只能获取自己类中的方法
Method[] methods2 = clazz.getDeclaredMethods();
for (Method method : methods2) {
System.out.println(method);
}
System.out.println("===========================");
//获取指定的唯一方法
Method method3 = clazz.getDeclaredMethod("eat", String.class);
System.out.println(method3);
//获取方法的修饰符
int modifiers = method3.getModifiers();
System.out.println(modifiers);
//获取方法的名字
String name = method3.getName();
System.out.println(name);
//eat
//获取方法的形参
Parameter[] parameters = method3.getParameters();
for (Parameter parameter : parameters) {
System.out.println(parameter);
}
//java.lang.String arg0
//方法运行
/*
Method类中用于创建对象的方法
Object =invoke(Object obj , Object.. args)运行方法
参数一: 用obj对象调用该方法
参数二: 调用方法的传递的参数
返回值:方法的返回值
*/
Student s = new Student();
//参数一s: 表示方法的调用者
//参数二: 调用方法的传递的实际参数
method3.setAccessible(true);
method3.invoke(s, "汉堡包");
}
}
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void sleep() {
System.out.println("睡觉");
}
private void eat(String something) {
System.out.println("在吃" + something);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
泛型擦除
集合中的泛型只在java文件中存在,当编译成class文件之后,就没有泛型了。
修改字符串的内容
字符串,在底层是一个byte类型的字节数组,名字叫做value
private final byte[] value;
真正不能被修改的原因:final和private
private修饰value而且没有对外提供getvalue和setvalue的方法。所以,在外界不能获取或修改value记录的地址值。
如果要强行修改可以用反射:
String s = "abc";
String ss = "abc";
// private final byte[] value= {97,98,99};
// 没有对外提供getvalue和setvalue的方法,不能修改value记录的地址值
// 如果我们利用反射获取了value的地址值。
// 也是可以修改的,final修饰的value
// 真正不可变的value数组的地址值,里面的内容利用反射还是可以修改的,比较危险
//1.获取class对象
Class clazz = s.getClass();
//2.获取value成员变量(private)
Field field = clazz.getDeclaredField("value");
//但是这种操作非常危险
//JDK高版本已经屏蔽了这种操作,低版本还是可以的
//临时修改权限
field.setAccessible(true);
//3.获取value记录的地址值
byte[] bytes = (byte[]) field.get(s);
bytes[0] = 100;
System.out.println(s);//dbc
System.out.println(ss);//dbc
反射和配置文件结合动态获取的练习
需求: 利用反射根据文件中的不同类名和方法名,创建不同的对象并调用方法。
分析:
①通过Properties加载配置文件
②得到类名和方法名
③通过类名反射得到Class对象
④通过Class对象创建一个对象
⑤通过Class对象得到方法
⑥调用方法
代码示例:
public class ReflectDemo9 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.读取配置文件的信息
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("day14-code\\prop.properties");
//load(InputStream in),加载属性文件为Properties对象
prop.load(fis);
fis.close();
System.out.println(prop);
String classname = prop.get("classname") + "";
String methodname = prop.get("methodname") + "";
//2.获取字节码文件对象
Class clazz = Class.forName(classname);
//3.要先创建这个类的对象
Constructor con = clazz.getDeclaredConstructor();
con.setAccessible(true);
Object o = con.newInstance();
System.out.println(o);
//4.获取方法的对象
Method method = clazz.getDeclaredMethod(methodname);
method.setAccessible(true);
//5.运行方法
method.invoke(o);
}
}
配置文件中的信息:
classname=com.itheima.a02reflectdemo1.Student
methodname=sleep
利用反射保存对象中的信息
动态代理
好处:
无侵入式的给方法增强功能
1, 真正干活的对象
2,代理对象
3,利用代理调用方法
切记一点:代理可以增强或者拦截的方法都在接口中,接口需要写在newProxyInstance的第二个参数里。
代码实现
public class Test {
public static void main(String[] args) {
/*
需求:
外面的人想要大明星唱一首歌
1. 获取代理的对象
代理对象 = ProxyUtil.createProxy(大明星的对象);
2. 再调用代理的唱歌方法
代理对象.唱歌的方法("只因你太美");
*/
//1. 获取代理的对象
BigStar bigStar = new BigStar("鸡哥");
Star proxy = ProxyUtil.createProxy(bigStar);
//2. 调用唱歌的方法
String result = proxy.sing("只因你太美");
System.out.println(result);
}
}
public interface Star {
//我们可以把所有想要被代理的方法定义在接口当中
//唱歌
public abstract String sing(String name);
//跳舞
public abstract void dance();
}
public class BigStar implements Star {
private String name;
public BigStar() {
}
public BigStar(String name) {
this.name = name;
}
//唱歌
@Override
public String sing(String name){
System.out.println(this.name + "正在唱" + name);
return "谢谢";
}
//跳舞
@Override
public void dance(){
System.out.println(this.name + "正在跳舞");
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
public String toString() {
return "BigStar{name = " + name + "}";
}
}
/*
*
* 类的作用:
* 创建一个代理
*
* */
public class ProxyUtil {
/*
*
* 方法的作用:
* 给一个明星的对象,创建一个代理
*
* 形参:
* 被代理的明星对象
*
* 返回值:
* 给明星创建的代理
*
*
*
* 需求:
* 外面的人想要大明星唱一首歌
* 1. 获取代理的对象
* 代理对象 = ProxyUtil.createProxy(大明星的对象);
* 2. 再调用代理的唱歌方法
* 代理对象.唱歌的方法("只因你太美");
* */
public static Star createProxy(BigStar bigStar){
/* java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
参数一:用于指定用哪个类加载器,去加载生成的代理类
参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
参数三:用来指定生成的代理对象要干什么事情*/
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),//参数一:用于指定用哪个类加载器,去加载生成的代理类
new Class[]{Star.class},//参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
//参数三:用来指定生成的代理对象要干什么事情
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
* 参数一:代理的对象
* 参数二:要运行的方法 sing
* 参数三:调用sing方法时,传递的实参
* */
if("sing".equals(method.getName())){
System.out.println("准备话筒,收钱");
}else if("dance".equals(method.getName())){
System.out.println("准备场地,收钱");
}
//去找大明星开始唱歌或者跳舞
//代码的表现形式:调用大明星里面唱歌或者跳舞的方法
return method.invoke(bigStar,args);
}
}
);
return star;
}
}
额外扩展
动态代理,还可以拦截方法
比如:
在这个故事中,经济人作为代理,如果别人让邀请大明星去唱歌,打篮球,经纪人就增强功能。
但是如果别人让大明星去扫厕所,经纪人就要拦截,不会去调用大明星的方法。
/*
* 类的作用:
* 创建一个代理
* */
public class ProxyUtil {
public static Star createProxy(BigStar bigStar){
//public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("cleanWC".equals(method.getName())){
System.out.println("拦截,不调用大明星的方法");
return null;
}
//如果是其他方法,正常执行
return method.invoke(bigStar,args);
}
}
);
return star;
}
}
动态代理的练习
对add方法进行增强,
public class MyProxyDemo1 {
public static void main(String[] args) {
//动态代码可以增强也可以拦截
//1.创建真正干活的人
ArrayList<String> list = new ArrayList<>();
//2.创建代理对象
//参数一:类加载器。当前类名.class.getClassLoader()
// 找到是谁,把当前的类,加载到内存中了,我再麻烦他帮我干一件事情,把后面的代理类,也加载到内存
//参数二:是一个数组,在数组里面写接口的字节码文件对象。
// 如果写了List,那么表示代理,可以代理List接口里面所有的方法,对这些方法可以增强或者拦截
// 但是,一定要写ArrayList真实实现的接口
// 假设在第二个参数中,写了MyInter接口,那么是错误的。
// 因为ArrayList并没有实现这个接口,那么就无法对这个接口里面的方法,进行增强或拦截
//参数三:用来创建代理对象的匿名内部类
List proxyList = (List) Proxy.newProxyInstance(
//参数一:类加载器
MyProxyDemo1.class.getClassLoader(),
//参数二:是一个数组,表示代理对象能代理的方法范围
new Class[]{List.class},
//参数三:本质就是代理对象
new InvocationHandler() {
@Override
//invoke方法参数的意义
//参数一:表示代理对象,一般不用(了解)
//参数二:就是方法名,我们可以对方法名进行判断,是增强还是拦截
//参数三:就是下面第三步调用方法时,传递的参数。
//举例1:
//list.add("1");
//此时参数二就是add这个方法名
//此时参数三 args[0] 就是 1
//举例2:
//list.set(1, "aaa");
//此时参数二就是set这个方法名
//此时参数三 args[0] 就是 1 args[1]"aaa"
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//对add方法做一个增强,统计耗时时间
if (method.getName().equals("add")) {
long start = System.currentTimeMillis();
//调用集合的方法,真正的添加数据
method.invoke(list, args);
long end = System.currentTimeMillis();
System.out.println("耗时时间:" + (end - start));
//需要进行返回,返回值要跟真正增强或者拦截的方法保持一致
return true;
}else if(method.getName().equals("remove") && args[0] instanceof Integer){
System.out.println("拦截了按照索引删除的方法");
return null;
}else if(method.getName().equals("remove")){
System.out.println("拦截了按照对象删除的方法");
return false;
}else{
//如果当前调用的是其他方法,我们既不增强,也不拦截
method.invoke(list,args);
return null;
}
}
}
);
//3.调用方法
//如果调用者是list,就好比绕过了第二步的代码,直接添加元素
//如果调用者是代理对象,此时代理才能帮我们增强或者拦截
//每次调用方法的时候,都不会直接操作集合
//而是先调用代理里面的invoke,在invoke方法中进行判断,可以增强或者拦截
proxyList.add("aaa");
proxyList.add("bbb");
proxyList.add("ccc");
proxyList.add("ddd");
proxyList.remove(0);
proxyList.remove("aaa");
//打印集合
System.out.println(list);
}
}
对remove方法进行拦截,对其他方法不拦截也不增强
案例代码来源:https://www.bilibili.com/video/BV17F411T7Ao