概念

代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持。这种模式在RMI和EJB中都得到了广泛的使用。传统的代理模式的实现,需要在源代码中添加一些附加的类。这些类一般是手写或是通过工具来自动生成。

JDK 5引入的动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。在运行时刻,可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接 口的实现。当使用者调用了代理对象所代理的接口中的方法的时候,这个调用的信息会被传递给InvocationHandlerinvoke方法。在 invoke方法的参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数。invoke方法的返回值被返回给使用者。这种做法实际上相 当于对方法调用进行了拦截。熟悉AOP的人对这种使用模式应该不陌生。但是这种方式不需要依赖AspectJ等AOP框架。

原理

在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class)

InvocationHandler

每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。我们来看看InvocationHandler这个接口的唯一一个方法 invoke 方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable

参数的含义:

proxy:   指代我们所代理的那个真实对象

method:  指代的是我们所要调用真实对象的某个方法的Method对象

args:   指代的是调用真实对象某个方法时接受的参数

Proxy

Proxy这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

参数的含义:

loader:   一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载

interfaces:  一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了

h: 一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

简单的例子

首先定义一个Hello接口

public interface Hello {
public void helloCat();
public void helloDog(String dog);
}

实现了该接口的实现类HelloImpl,也就是我们的真实对象

public class HelloImpl implements Hello {
@Override
public void helloCat() {
System.out.println("hello Cat !");
}
@Override
public void helloDog(String dog) {
System.out.println("hello " + dog + "!");
}
}

定义动态代理类,实现 InvocationHandler 这个接口。

public class DynamicProxy implements InvocationHandler {
private Hello hello;
public DynamicProxy(Hello hello){
this.hello = hello;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//before
System.out.println("before say hello");
//当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
Object object = method.invoke(hello, args);
//after
System.out.println("after say hello");
return object;
}
}

用代理实现功能

public class Client {
public static void main(String[] args) {
//代理对象
Hello helloImpl = new HelloImpl();
//将需要代理的对象传进去,最后是需要该对象调用其方法的
InvocationHandler handler = new DynamicProxy(helloImpl);
/*
* 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数
* 第一个参数 handler.getClass().getClassLoader() ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象
* 第二个参数realSubject.getClass().getInterfaces(),我们这里为代理对象提供的接口是真实对象所实行的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了
* 第三个参数handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上
*/
Hello helloProxy = (Hello)Proxy.newProxyInstance(handler.getClass().getClassLoader(), helloImpl
.getClass().getInterfaces(), handler);
//看看这个代理对象的真实面目
System.out.println(helloProxy.getClass().getName());
helloProxy.helloCat();
helloProxy.helloDog("小白");
}
}

控制台输出

com.sun.proxy.$Proxy0
before say hello
hello Cat !
after say hello
before say hello
hello 小白!
after say hello

代理的类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。

应用

1.方法性能监测

获取执行方法前后的系统时间,算出其执行时间。

long startTime = System.currentTimeMillis();
Object obj = method.invoke(proxied, args);
long endTime = System.currentTimeMillis();
System.out.println("Method " + method.getName() + " execution time: " + (endTime - startTime) * 1.0 / 1000 + "s");

2.日志管理

获取日志需要的方法名和时间,并利用代理写入。

public String beforeMethod(Method method) {
return getFormatedTime() + " Method:" + method.getName() + " start running\r\n";
} public String afterMethod(Method method) {
return getFormatedTime() + " Method:" + method.getName() + " end running\r\n";
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
write(path, beforeMethod(method));
Object object = method.invoke(proxied, args);
write(path, afterMethod(method));
return object;
} public String getFormatedTime() {
DateFormat formater = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return formater.format(System.currentTimeMillis());
} public void write(String path, String content) {
FileWriter writer = null;
try {
writer = new FileWriter(new File(path), true);
writer.write(content);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(null != writer) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

3.使用动态代理对注解进行处理

在实例中如何使用和处理注解。

假定有一个公司的雇员信息系统,从访问控制的角度出发,对雇员的工资的更新只能由具有特定角色的用户才能完成。考虑到访问控制需求的普遍性,可以定义一个注解来让开发人员方便的在代码中声明访问控制权限。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredRoles {
String[] value();
}

下一步则是如何对注解进行处理,这里使用的Java的反射API并结合动态代理。下面是动态代理中的InvocationHandler接口的实现。

public class AccessInvocationHandler<T> implements InvocationHandler {
final T accessObj;
public AccessInvocationHandler(T accessObj) {
this.accessObj = accessObj;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RequiredRoles annotation = method.getAnnotation(RequiredRoles.class); //通过反射API获取注解
if (annotation != null) {
String[] roles = annotation.value();
String role = AccessControl.getCurrentRole();
if (!Arrays.asList(roles).contains(role)) {
throw new AccessControlException("The user is not allowed to invoke this method.");
}
}
return method.invoke(accessObj, args);
}
}

在具体使用的时候,首先要通过Proxy.newProxyInstance方法创建一个EmployeeGateway的接口的代理类,使用该代理类来完成实际的操作。

相当于使用一个AOP切面对所有实现了这个接口(也就是追加了注解的类)实现切面控制。

总结

Java动态代理美中不足的是仅支持 interface 代理。因为那些动态生成的代理类都有一个共同的父类叫Proxy。Java 的继承机制注定了这些动态代理类们无法实现对 class 的动态代理,原因是多继承在 Java 中本质上就行不通。

虽然有缺点,但是Proxy设计的非常优美是毋庸置疑的,有时间一定要去看看源码。

Java动态代理机制分析及扩展这篇文章写得相当好,里面有些东西还没有完全理解,标记下来,多读几遍。

最新文章

  1. CAS实现SSO单点登录原理
  2. 关于TCP/IP的三次握手和四次挥手解释
  3. 20145225《Java程序设计》 第10周学习总结
  4. window删除损坏无法打开的文件
  5. java验证码-汉字验证码
  6. Android中的普通对话框、单选对话框、多选对话框、带Icon的对话框、以及自定义Adapter和自定义View对话框详解
  7. CSU 1160 把十进制整数转换为十六进制,格式为0x开头,10~15由大写字母A~F表示
  8. Ext JS学习第十天 Ext基础之 扩展原生的javascript对象(二)
  9. Can&#39;t call commit when autocommit=true(转)
  10. 使用Q进行同步的Promises操作
  11. C++ 配置文件类的封装
  12. webgl学习
  13. 10个最佳Node.js企业应用案例:从Uber到LinkedIn
  14. Hibrenate之事务的理解以及代码编写
  15. 全渠道java b2b b2c o2o平台
  16. unigui结合JS方法记录
  17. merge into用法小结
  18. ViewPager的addOnPageChangeListener和setOnPageChangeListener的区别,ViewPager改变数据后IndexOutOfBoundsException
  19. Intellij idea 配置热部署
  20. Ucloud的自主研发的检测主机是否被入侵的agent

热门文章

  1. Hibernate原生SQL查询数据转换为HQL查询数据方法
  2. http://blog.sina.com.cn/s/blog_4dd787e40102uysg.html
  3. select()/poll() 的内核实现
  4. Mac Finder 显示路径和复制路径
  5. QSignalMapper类的使用
  6. Linux 倒引号、单引号、双引号
  7. 虚拟机快照,克隆,静态ip地址的设置(centos 6.5)
  8. PHP的抽象类、接口的区别和选择[转载]
  9. Fedora 28 设置yum代理
  10. Android app启动耗时分析