首页 > 代码库 > Java的反射机制和动态代理
Java的反射机制和动态代理
介绍Java注解的时候,多次提到了Java的反射API。与javax.lang.model不同的是,通过反射API可以获取程序在运行时刻的内部结构。反射API中提供的动态代理也是非常强大的功能,可以原生实现AOP中 的方法拦截功能。正如英文单词reflection的含义一样,使用反射API的时候就好像在看一个Java类在水中的倒影一样。知道了Java类的内部 结构之后,就可以与它进行交互,包括创建新的对象和调用对象中的方法等。这种交互方式与直接在源代码中使用的效果是相同的,但是又额外提供了运行时刻的灵活性。使用反射的一个最大的弊端是性能比较差。相同的操作,用反射API所需的时间大概比直接的使用要慢一两个数量级。不过现在的JVM实现中,反射操作的性能已经有了很大的提升。在灵活性与性能之间,总是需要进行权衡的。应用可以在适当的时机来使用反射API。
基本用法
Java 反射API的第一个主要作用是获取程序在运行时刻的内部结构。这对于程序的检查工具和调试器来说,是非常实用的功能。只需要短短的十几行代码,就可以遍历出来一个Java类的内部结构,包括其中的构造方法、声明的域和定义的方法等。这不得不说是一个很强大的能力。只要有了java.lang.Class类 的对象,就可以通过其中的方法来获取到该类中的构造方法、域和方法。对应的方法分别是getConstructor、getField和getMethod。这三个方法还有相应的getDeclaredXXX版本,区别在于getDeclaredXXX版本的方法只会获取该类自身所声明的元素,而不会考虑继承下来的。Constructor、Field和Method这三个类分别表示类中的构造方法、域和方法。这些类中的方法可以获取到所对应结构的元数据。
反射API的另外一个作用是在运行时刻对一个Java对象进行操作。 这些操作包括动态创建一个Java类的对象,获取某个域的值以及调用某个方法。在Java源代码中编写的对类和对象的操作,都可以在运行时刻通过反射API来实现。考虑下面一个简单的Java类。
class MyClass { public int count; public MyClass(int start) { count = start; } public void increase(int step) { count = count + step; }}
使用一般做法和反射API都非常简单。
MyClass myClass = new MyClass(0); //一般做法myClass.increase(2);System.out.println("Normal -> " + myClass.count);try { Constructor constructor = MyClass.class.getConstructor(int.class); //获取构造方法
MyClass myClassReflect = constructor.newInstance(10); //创建对象
Method method = MyClass.class.getMethod("increase", int.class); //获取方法
method.invoke(myClassReflect, 5); //调用方法
Field field = MyClass.class.getField("count"); //获取域
System.out.println("Reflect -> " + field.getInt(myClassReflect)); //获取域的值
} catch (Exception e) {
e.printStackTrace();
}
由于数组的特殊性,Array类提供了一系列的静态方法用来创建数组和对数组中的元素进行访问和操作。
Object array = Array.newInstance(String.class, 10); //等价于 new String[10]Array.set(array, 0, "Hello"); //等价于array[0] = "Hello"Array.set(array, 1, "World"); //等价于array[1] = "World"System.out.println(Array.get(array, 0)); //等价于array[0]
使用Java反射API的时候可以绕过Java默认的访问控制检查,比如可以直接获取到对象的私有域的值或是调用私有方法。只需要在获取到Constructor、Field和Method类的对象之后,调用setAccessible方法并设为true即可。有了这种机制,就可以很方便的在运行时刻获取到程序的内部状态。
处理泛型
Java 5中引入了泛型的概念之后,Java反射API也做了相应的修改,以提供对泛型的支持。由于类型擦除机制的存在,泛型类中的类型参数等信息,在运行时刻是不存在的。JVM看到的都是原始类型。对此,Java 5对Java类文件的格式做了修订,添加了Signature属性,用来包含不在JVM类型系统中的类型信息。比如以java.util.List接口为例,在其类文件中的Signature属性的声明是<E:Ljava/lang/Object;>Ljava/lang/Object;Ljava/util/Collection<TE;>;; ,这就说明List接口有一个类型参数E。在运行时刻,JVM会读取Signature属性的内容并提供给反射API来使用。
比如在代码中声明了一个域是List<String>类型的,虽然在运行时刻其类型会变成原始类型List,但是仍然可以通过反射来获取到所用的实际的类型参数。
Field field = Pair.class.getDeclaredField("myList"); //myList的类型是List Type type = field.getGenericType(); if (type instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) type; Type[] actualTypes = paramType.getActualTypeArguments(); for (Type aType : actualTypes) { if (aType instanceof Class) { Class clz = (Class) aType; System.out.println(clz.getName()); //输出java.lang.String } } }
动态代理
熟悉设计模式的人对于代理模式可 能都不陌生。 代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持。这种模式在RMI和EJB中都得到了广泛的使用。传统的代理模式的实现,需要在源代码中添加一些附加的类。这些类一般是手写或是通过工具来自动生成。JDK 5引入的动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。在运行时刻,可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接 口的实现。当使用者调用了代理对象所代理的接口中的方法的时候,这个调用的信息会被传递给InvocationHandler的invoke方法。在 invoke方法的参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数。invoke方法的返回值被返回给使用者。这种做法实际上相 当于对方法调用进行了拦截。熟悉AOP的人对这种使用模式应该不陌生。但是这种方式不需要依赖AspectJ等AOP框架。
下面的代码用来代理一个实现了List接口的对象。所实现的功能也非常简单,那就是禁止使用List接口中的add方法。如果在getList中传入一个实现List接口的对象,那么返回的实际就是一个代理对象,尝试在该对象上调用add方法就会抛出来异常。
public List getList(final List list) { return (List) Proxy.newProxyInstance(DummyProxy.class.getClassLoader(), new Class[] { List.class }, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("add".equals(method.getName())) { throw new UnsupportedOperationException(); } else { return method.invoke(list, args); } } }); }
这里的实际流程是,当代理对象的add方法被调用的时候,InvocationHandler中的invoke方法会被调用。参数method就包含了调用的基本信息。因为方法名称是add,所以会抛出相关的异常。如果调用的是其它方法的话,则执行原来的逻辑。
使用案例
Java 反射API的存在,为Java语言添加了一定程度上的动态性,可以实现某些动态语言中的功能。比如在JavaScript的代码中,可以通过 obj["set" + propName]()来根据变量propName的值找到对应的方法进行调用。虽然在Java源代码中不能这么写,但是通过反射API同样可以实现类似 的功能。这对于处理某些遗留代码来说是有帮助的。比如所需要使用的类有多个版本,每个版本所提供的方法名称和参数不尽相同。而调用代码又必须与这些不同的版本都能协同工作,就可以通过反射API来依次检查实际的类中是否包含某个方法来选择性的调用。
Java 反射API实际上定义了一种相对于编译时刻而言更加松散的契约。如果被调用的Java对象中并不包含某个方法,而在调用者代码中进行引用的话,在编译时刻就会出现错误。而反射API则可以把这样的检查推迟到运行时刻来完成。通过把Java中的字节代码增强、类加载器和反射API结合起来,可以处理一些对灵 活性要求很高的场景。
在 有些情况下,可能会需要从远端加载一个Java类来执行。比如一个客户端Java程序可以通过网络从服务器端下载Java类来执行,从而可以实现自动更新 的机制。当代码逻辑需要更新的时候,只需要部署一个新的Java类到服务器端即可。一般的做法是通过自定义类加载器下载了类字节代码之后,定义出 Class类的对象,再通过newInstance方法就可以创建出实例了。不过这种做法要求客户端和服务器端都具有某个接口的定义,从服务器端下载的是 这个接口的实现。这样的话才能在客户端进行所需的类型转换,并通过接口来使用这个对象实例。如果希望客户端和服务器端采用更加松散的契约的话,使用反射API就可以了。两者之间的契约只需要在方法的名称和参数这个级别就足够了。服务器端Java类并不需要实现特定的接口,可以是一般的Java类。
动态代理的使用场景就更加广泛了。需要使用AOP中的方法拦截功能的地方都可以用到动态代理。Spring框架的AOP实现默认也使用动态代理。不过JDK中的动态代理只支持对接口的代理,不能对一个普通的Java类提供代理。不过这种实现在大部分的时候已经够用了。
----------------------------------------------------------------------------------
反射:在程序运行的时候,动态的获取某个类中的属性和方法,并且能够调用(很多框架能自动识别你写的类,然后调用一些共同的方法,靠的就是它)。
代理:给类包装上一层壳,通过这个壳去操作这个类,使得你在操作这个类之前之后可以做一些你想做的事情。
反射介绍
反射比较简单,主要使用Class类,Class类提供了运行时获取或调用某个类具体内容的方法。如下代码作实例:
待调用的类MyClass:
- public class MyClass {
- private int myInt;
- private String myString;
- public MyClass(){
- }
- public MyClass(int a){
- this.myInt = a;
- }
- public void Method2Void(){
- }
- public int Method2Int(){
- System.out.println("Method2Int has run");
- return 0;
- }
- public String Method2String(){
- return "";
- }
- public Object Method2Object() {
- return new Date();
- }
- public void Method3Param(int a, String b){
- System.out.println("Method3Param has run with param-a:" + a + " and param-b:" + b);
- }
- }
Main函数
- public static void main(String[] args){
- MyClass myClass = new MyClass();
- Class cls = myClass.getClass();//获取一个类的Class
- System.out.println("1:" + cls.getName());//名字
- System.out.println("2:" + cls.getSimpleName());
- System.out.println("3:" + cls.getPackage());
- try{
- Method m = cls.getMethod("Method2Int", new Class[]{});//获取一个类的方法
- m.invoke(myClass, new Object[]{});//精髓所在,调用这个类的方法
- Method m2 = cls.getMethod("Method3Param", new Class[]{int.class, String.class});
- m2.invoke(myClass, new Object[]{5, "fake"});//调用这个类的方法,带参数的
- }catch(Exception e){
- e.printStackTrace();
- }
- Method[] ms = cls.getMethods();
- for(Method m:ms){
- System.out.println(m.getName());
- }
- try{
- Class cls2 = Class.forName("MyClass");//这种方法也能获取一个类的Class,同时能动态载入这个类
- }catch(Exception e){
- e.printStackTrace();
- }
- }
下面详细介绍下代理
代理这种现象在生活中是非常常见的,比如你要买火车票,可以让你的朋友帮你买,也可以托代售点帮你买。你把钱给了他们,他们可能会做任何事情。也许你朋友忘记;或者代售点把你黑了。当然,程序是你的,你可以控制他们的行为。
代理有普通代理和动态代理。上面说的你的朋友,可以看成是普通代理,代售点可以看成是动态代理。区别在于,你的朋友并不会帮每个人都买票,仅仅帮他认识少部分人买,而代售点是来者不拒的,具有通用性。
使用代理模式,需要区分真实对象和代理对象。代理对象中含有真实对象的引用,可以对真实对象进行操作,调用真实对象的方法。可以通过调用代理对象的方法,间接的去调用真实对象的方法。
下面还是举例来说说这两种代理吧。
以下是这两种代理所需的类:
行为PersonAct.java
- package proxy;
- public interface PersonAct {
- void buyTicket();//买票
- void checkProperty();//查看财产
- }
抽象类Person.java
- import java.util.Map.Entry;
- public abstract class Person implements PersonAct{
- //全部家当放在这个HashMap里
- HashMap<String, Object> property = new HashMap<String, Object>();
- String name;
- public Person(String name, int money){
- this.name= name;
- this.property.put("money", money);
- }
- //打印目前家当
- public void checkProperty(){
- System.out.println(this.name + "的家当如下:");
- Iterator<Entry<String, Object>> iter = property.entrySet().iterator();
- while(iter.hasNext()){
- Entry<String, Object> entry = (Entry<String, Object>)iter.next();
- System.out.println(entry.getKey() + "---" + entry.getValue());
- }
- }
- }
真实类Boy.java
- package proxy;
- public class Boy extends Person{
- public Boy(String name, int money){
- super(name, money);
- }
- //买票
- @Override
- public void buyTicket() {
- int money = (Integer)property.get("money");
- property.put("money", money - 38);//扣钱
- property.put("ticket", "北京到上海机票");//拿票
- }
- }
真实类Girl.java
- package proxy;
- public class Girl extends Person{
- public Girl(String name, int money){
- super(name, money);
- }
- @Override
- public void buyTicket() {
- int money = (Integer)property.get("money");
- property.put("money", money - 46);//扣钱
- property.put("ticket", "北京到深圳机票");//拿票
- }
- }
上面的类Boy和Girl代表现实生活中的两个人,Boy自己去买票的过程如下。
- Boy boy = new Boy("西门庆", 100);
- boy.checkProperty();
- boy.buyTicket();
- boy.checkProperty();
结果将会打印出boy购票前和投票后的家当。
1,普通代理
类FatherOfBoy.java
- package proxy;
- public class FatherOfBoy {
- private Boy boy = new Boy("西门庆", 100);
- public void buyTicket(){
- boy.buyTicket();
- }
- public void checkProperty(){
- boy.checkProperty();
- }
- }
概念很好理解,普通代理买票可以这样测试。
- FatherOfBoy father = new FatherOfBoy();
- father.checkProperty();
- father.buyTicket();
- father.checkProperty();
2, 动态代理
动态代理主要通过继承InvocationHandler接口来实现。
ProxyAgency.java如下:
- package proxy;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- public class ProxyAgency implements InvocationHandler{
- private Person target;//真实对象的引用
- public void setTarget(Person target) {
- this.target = target;
- }
- public ProxyAgency(){
- }
- public ProxyAgency(Person obj){
- this.setTarget(obj);
- }
- //obj,这个是代理对象
- //method,具体调用的方法
- //args,调用方法的时候传进来的参数,一般都直接传给真实对象即可
- @Override
- public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
- //target,真实对象,收取手续费10元
- if(method.getName().equalsIgnoreCase("buyTicket")){
- int money = Integer.parseInt(target.property.get("money").toString());
- target.property.put("money", money - 10);
- }
- //target,真实对象,下面一句属于java反射机制的东西,用反射机制获取真实对象的方法
- return method.invoke(target, args);//调用真实对象的方法
- }
- }
调用实例:
- //代理对象构建器
- ProxyAgency proxyAgency = new ProxyAgency();
- //真实对象
- Boy b = new Boy("西门庆", 100);
- Girl g = new Girl("潘金莲", 200);
- //代理对象,获取方法1
- PersonAct p1 = (PersonAct) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{ PersonAct.class }, proxyAgency);
- //真实对象注入构建器,也可以在构建器的构造函数中注入
- proxyAgency.setTarget(b);
- //代理对象获取方法2
- // Class<?> cls = g.getClass();
- // PersonAct p1 = (PersonAct) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), proxyAgency);
- p1.checkProperty();
- p1.buyTicket();
- p1.checkProperty();
- proxyAgency.setTarget(g);
- p1.checkProperty();
- p1.buyTicket();
- p1.checkProperty();
输出如下:
西门庆的家当如下:
money---100
西门庆的家当如下:
ticket---北京到上海机票
money---52
潘金莲的家当如下:
money---200
潘金莲的家当如下:
ticket---北京到深圳机票
money---144
------------------------------------------------------------------------------------