Java的反射机制
今天来研究一下Java的反射机制。反射机制是很多的框架与插件的基础.这部分由于会涉及到JVM的底层机制,会比较晦涩,还是要慢慢学习的
Java的类加载机制
在讲反射之前,我们先来了解一下Java的类加载机制。在运行状态下JVM会将类的信息提取之后在内存内生成一个Class对象,这个对象全局唯一
,我们对static资源的操作实际上就是对Class对象的操作.但是,Class对象与一般的对象不同的是它hi记录了类的各种信息包括类所属的父类,实现的接口,所在的包等等
反射机制是什么
Java的反射机制(Reflection)是一种动态获取程序内部信息和操作对象的能力。通过反射,程序可以在运行时查看类的结构、字段、方法、构造函数等元数据,并对这些元素进行操作。反射是通过 java.lang.reflect
包中的类来实现的。
或许你现在仍然无法理解反射机制有什么用,但请耐心的学下去,很快你就能意识到它的强大功能。
让我们先从Class对象学起
Class对象的获取
首先我们要获取Class对象
1 2 3 4 5
| public static void main(String[] args) throws ClassNotFoundException { Class<String> clazz = String.class; Class<?> clazz2 = Class.forName("java.lang.String"); Class<?> clazz3 = new String("cpdd").getClass(); }
|
有一点需要注意,尽管后两种方法的返回都是泛型的,但事实上这三个方法返回的都是同一个Class对象
我们都知道,如果希望对基本数据类型的数据进行对象操作,我们要将其转换为对应的包装类,但是基本数据类型也存在自身的Class对象
1 2 3 4
| public static void main(String[] args) { Class<?> clazz = int.class; Class<?> clazz2 = Integer.class; }
|
我们可以发现,基本数据类型的Class是定义在其对应的包装类中的
1 2 3 4
| public final class Integer extends Number implements Comparable<Integer>, Constable, ConstantDesc { public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
|
所以我们也可以通过
1
| Class<?> clazz2 = Integer.TYPE;
|
来获取包装类对应的基本类型的Class对象(注意,包装类的Class对象与基本数据类型的Class对象是两个不同的对象)
此外,类对应的数组类型也拥有单独的Class对象
1
| Class<?> clazz2 = int[].class;
|
Class类与类型比较
一般的情况下我们要判断一个对象是否属于某个类型可以用instanceof
关键字来进行比较,现在我们也可以这么比较
1 2 3
| People p = new People(); System.out.println(p instanceof People); System.out.println(p.getClass()== People.class);
|
此外,我们如果希望判断某个对象是否为某个接口/抽象类的实现或者是某个父类的子类,可以这么做
1
| p.getClass().asSubclass(People.class)
|
进一步的我们可以获取这个对象所属类的父类的Class对象
1 2
| p.getClass().getSuperclass(); p.getClass().getGenericSuperclass();
|
同样的可以用来获取父接口
1 2 3 4 5 6 7 8 9 10
| public static void main(String[] args) { Integer i = 10; for (Class<?> anInterface : i.getClass().getInterfaces()) { System.out.println(anInterface.getName()); }
for (Type genericInterface : i.getClass().getGenericInterfaces()) { System.out.println(genericInterface.getTypeName()); } }
|
创建类对象
我们在前面提到过,我们创建对象的模版实际上是Class对象而不是类,那么我们是不是可以反过来直接创建出某个Class对象,再用这个Class去创建对象,哪怕实际的代码中不存在这样的类?当然可以
1 2 3 4
| public static void main(String[] args) throws InstantiationException, IllegalAccessException { Class<?> clazz = People.class; Object people=clazz.newInstance(); }
|
注意,newInstance
方法或抛出两个异常,如果使用带参构造,则抛出InstantiationException
,这个方法只支持无参构造,如果原来的类是非public的,则抛出IllegalAccessException
.但是这个方法现在已经不再被推荐
现在主要的创建对象的方式是通过构造器,此时是支持带参构造的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Main { public static void main(String[] args) { Class<?> clazz = People.class; try { Object people=clazz.getConstructor(String.class).newInstance("你好"); } catch ( InvocationTargetException | NoSuchMethodException |InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); }
} } public class People { public String name; public People(String name) { this.name = name; } }
|
此外,如果构造方法是非public的,我们也不是没有办法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Main { public static void main(String[] args) { Class<?> clazz = People.class; try { Constructor<?> constructor = clazz.getDeclaredConstructor(String.class); constructor.setAccessible(true); People people= (People) constructor.newInstance("你好"); } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); }
} } public class People { public String name; private People(String name) { this.name = name; } }
|
调用方法
接下来我们要尝试用反射机制调用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Main { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { Class<?> clazz=Class.forName("People"); Constructor<?> constructor=clazz.getConstructor(String.class); People people= (People) constructor.newInstance("John Doe"); Method setName=clazz.getMethod("setName", String.class); setName.invoke(people,"aaa"); System.out.println(people.getName()); } } public class People { private String name; public People(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
|
同样的如果一个方法是非public的,我们也可以强行调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class Main { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { Class<?> clazz=Class.forName("People"); Constructor<?> constructor=clazz.getConstructor(String.class); People people= (People) constructor.newInstance("John Doe"); Method method=clazz.getDeclaredMethod("test"); method.setAccessible(true); method.invoke(people); } } public class People { public String name; public People(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } private void test(){ System.out.println("这里无法被访问"); } }
|
如果一个方法需要多个参数,直接用逗号一直加就可以了
如果一个方法中有可变长参数,那么可以用对应类型的数组类型的Class对象声明参数类型
修改类中属性
这段我们就直接点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Main { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { Class<?> clazz=Class.forName("People"); Constructor<?> constructor=clazz.getConstructor(String.class); People people= (People) constructor.newInstance("John Doe"); Field[] fields=clazz.getDeclaredFields(); for(Field f:fields){ f.setAccessible(true); System.out.println(f.getName()); System.out.println(f.getType()); System.out.println(f.get(people)); } fields[0].set(people,"as"); } } public class People { private String name; private int age; public People(String name) { this.name = name; } }
|
如果愿意,我甚至能够直接修改final字段.但是,具体的方法我就不放在这里了.因为自Java9之后添加了对这种修改的限制,需要修改虚拟机配置才能对final字段进行修改.而Java9之前只要setAccessible(true)
就能直接修改
类加载器
如果我们将Main方法写在jdk提供的类中程序是否能正常运行?答案是不行,因为jdk提供的类通过BootstarpClassLoade
加载的,而我们自己编写的类是通过AppClassLoade
进行加载的,所以AppClassLoade
找不到我们的Main方法就会报错
换句话说我们其实可以选择直接用类加载器来创造一个不存在的类(以下代码看看就好,不用太过深究,到时候讲框架相关内容的时候再细讲)
我先创建一个class文件,其反编译的内容为
1 2 3 4 5 6 7
| public class Test { public Test() { } public void test() { System.out.println("hello"); } }
|
接下来我可以直接将这个类文件加载到当前的程序中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MyClassLoader extends ClassLoader { public Class<?> defineClass(String name, byte[] b) { return defineClass(name, b, 0, b.length); } }
public class Main { public static void main(String[] args) throws Exception { MyClassLoader myClassLoader=new MyClassLoader(); FileInputStream fis=new FileInputStream("E:\\test12\\out\\production\\test12\\Test.class"); byte[] buffer=new byte[fis.available()]; fis.read(buffer); fis.close(); Class<?> clazz=myClassLoader.defineClass("Test", buffer); Object object= clazz.getConstructor().newInstance(); Method method=clazz.getMethod("test"); method.invoke(object); } }
|
这个套路其实是用来防破解的,在发布的程序中缺失部分关键类,在使用时再通过网络将编译好二进制数据加密后传输给用户,用户再将这个类加载到内存中
结语
反射机制的讲解暂时高一段落,主要是涉及的复杂内容太多了,我一直在努力的把握避免太过复杂的东西塞进来,反射机制的具体作用最好还是先学学后面的内容再来接触,但是不提前了解一点又不太好,所以只好这样七零八落的讲一讲
算了,放张图结束