Java动态代理解析

2018/04/14 Java

前言

代理模式是一个高频使用的设计模式,其原因在于我们可以在不改变被代理类的情况下,提供一个和被代理类一样的代理类,并负责预处理被代理类的方法,过滤或执行被代理类的方法以及后续操作。这里所说的一样是指代理类对象可以替代被代理类对象,实现这种效果有两种,一种是代理类和被代理类实现同一个接口,另一种是代理类直接继承自被代理类,这两种方式在接下来的jdk动态代理和cglib中均有体现。在深入了解动态代理之前,我们先了解一下Java的反射机制和静态代理,其中反射机制是动态代理实现的基础。

Java的反射机制

通过反射实例化对象

Java反射实例化对象主要依赖于java.lang.Class对象,其中一个重要的方法是

//通过全限名称获取Class对象
public static Class<?> forName(String className)

通过全限名称获取Class对象,完成类加载的过程,如果需要调用无参的构造方法用Class对象的newInstance()即可完成实例化过程,一个具体的例子如下

result=(ReflectService) Class.forName(serviceFullName).newInstance();

如果需要调用有参数的构造方法,则还需要调用public Constructor<T> getConstructor(Class<?>... parameterTypes)方法指明构造方法的参数类型列表,然后执行public T newInstance(Object ... initargs)指明构造方法的参数值的表。具体的例子如下

result=(ReflectService) Class.forName(serviceName).getConstructor(String.class).newInstance(param);

执行反射方法

执行反射方法需要先获取Class对象,获取Class对象除了使用Class.forName方法之外,还可以通过对象的getClass()方法,接下来执行

  1. public Method getMethod(String name, Class<?>… parameterTypes),通过Class的getMethod方法获取method对象,name为方法名,parameterTypes为参数类型表。
  2. public Object invoke(Object obj, Object… args),执行method对象的invoke方法,obj为执行方法的对象,args为方法参数值

如果方法没有参数,可以不写parameterTypes和args,一个调用的具体例子如下

//有参数
Method method=service2.getClass().getMethod("sayToTwo", String.class,String.class);
method.invoke(service2, "person1","person2");
//无参数
Method method2=service2.getClass().getMethod("sayHello");
method2.invoke(service2);

静态代理

假设我们有一个UserManager接口,其中定义了三个方法,用来增加用户,删除用户,修改用户信息。

public interface UserManager {
	public void addUser(String username,String password);
	public void delUser(int userId);
	public void modifyUser(int userId,String username,String password);
}

并且我们有一个实现类UserManager实现了这个接口

public class UserManagerImpl implements UserManager {
	@Override
	public void addUser(String username, String password) {
		System.out.println("---UserManagerImpl:addUser---");
	}
	@Override
	public void delUser(int userId) {
		System.out.println("---UserManagerImpl:delUser---");
	}
	@Override
	public void modifyUser(int userId, String username, String password) {
		System.out.println("---UserManagerImpl:modifyUser---");
	}
	public void addBatch(List<String>users) {
		System.out.println("---UserManagerImpl:addBatch---");
	}
}

假设我们想要在每次操作之前都想加一个日志操作,那么最简单的方式是直接在UserManagerImpl中增加一个logging()方法,然后加到方法实际调用之前,但是这样的坏处是日志记录和业务代码混在了一起,这不是我们想要的,另一种情况是这个类在第三方的jar包中,我们不能直接修改这个类代码。静态代理可以解决这个问题,我们不必要修改原有的代码也可以把想要的日志操作加进去,它的类图是这样设计的。

image

其中,被代理对象是UserManagerImpl,代理对象UserManagerProxy,它们都实现了UserManager接口,而UserManagerImplProxy持有UserManagerImpl对象,在addUser方法之前插入logging方法。这样UserManagerImpl和UserManagerProxy对于使用了UserManager接口的对象的地方是等同的,我们实现了在方法之前插入方法。 虽然我们实现了在不修改原有代码的情况下,在原有的业务逻辑之前加入日志记录的操作,将日志操作与原有的业务逻辑代码解耦,但是在代理类中,日志操作仍然和业务逻辑耦合在一起,我们将这种耦合关系转移到了代理类中,代理类中的耦合关系是不可避免的。对于每个业务逻辑如果都有不同的日志操作过程,也就是代理的粒度细粒度到这种操作,静态代理是适合的。但是如果插入的逻辑,比如日志操作,是同样的,并且需要插入的方法数是很多的,比如有1000个方法之前需要插入业务逻辑,那么静态代理就显得不那么合适了,因为我们需要在代理类中插入1000次logging的调用。

JDK动态代理

相比静态代理,动态代理的动态在于方法,我们可以拦截所有接口定义的方法,并且在实际方法调用的过程中获取到实际调用的方法,然后进行一些逻辑处理。JDK动态代理实现的过程如下:

  1. 首先,继承InvocationHandler,重写public Object invoke(Object proxy, Method method, Object[] args)方法,其中proxy为自动生成的代理对象,method为被调用的方法,args为方法参数
  2. 使用Proxy的静态方法public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)生成对应的代理实例
  3. 将代理实例上转型到对应的接口,执行接口中的方法

例如我们上面的例子,我们首先实现一个InvocationHandler的实现类,然后用createProxy生成了代理对象。

public class LogHandler implements InvocationHandler {
	
	public Object target;
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		logging();
		Object result=method.invoke(target, args);
		return result;
	}
	
	private void logging() {
		System.out.println("start logging now!");
	}
	public Object createProxy(Object target) {
		this.target=target;
		return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
	}
}

调用的过程

//动态代理调用
LogHandler logHandler=new LogHandler();
UserManager dynamicProxy=(UserManager) logHandler.createProxy(userManager);
dynamicProxy.addUser("hello", "hello");
dynamicProxy.delUser(1);
dynamicProxy.modifyUser(1, "hello", "hello");

从调用过程来看,我们就很明白自动生成的代理类实现了UserManager接口,实际上代理类还继承了Proxy类。另外一个问题,如果我们想要执行被代理类原有的方法,必须给InvocationHandler传入一个被代理类的对象target,在invoke方法中执行method.invoke放在中执行执行的类。为什么我们不能用invoke方法中的proxy,它不是也是实现了同样的接口吗?回答这个问题很简单,因为proxy类的对应方法增强了,如果用proxy的话,它会递归的调用该方法,直至虚拟机栈溢出。

从原理上来看,动态代理的关键在于可以动态的确定是哪个方法被调用,并且可以统一路由到InvocationHandler的invoke方法上,使得所有方法之前插入同样的逻辑操作得以归结到一个方法只内,不可以不说设计之精妙,至于jdk是如何路由到invoke方法上的,有待继续探究。

另外有趣的一点是,如果抛开代理的本意,我可以仅在提供接口的情况下而不提供具体实现类(被代理类)的情况下使用这种分发技术,执行对应的逻辑,实际上,这里的接口就仅仅充当了一组规范而已,而这一组规范路由到同一个处理方法,在注解方式提供参数信息的帮助下,这种方式也大有用武之处。

在上面的UserManagerImpl类中,我不仅实现了接口方法,而且提供了一个子类特有的方法addBatch(Listusers),加了这个方法目的是为了说明JDK动态代理是基于接口的,从上面的实现过程也可以看出我们需要将生成的代理类强转换成接口,而接口子类对象的特有的方法显然不能被代理,但是如果我想代理具体类中所有的方法呢,那么只能使用cglib来实现了

cglib动态代理

不同于JDK动态代理代理类要和被代理类实现同样的接口,cglib动态代理使用asm字节码增强生成代理对象直接继承自被代理对象,从而实现动态代理的过程。其实现过程如下

  1. 实现MethodInterceptor接口,实现其中的public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)方法
  2. 配置增强类Enhancer的父类,MethodInterceptor处理对象
  3. Enhanceer对象强转型被代理类,执行对应的方法

其基本过程和JDK动态代理类似,唯一的不同是我们不用传入被代理类,而只是被代理类的Class对象,重要的实现代码如下

/**
 * @param proxy 代理对象,当前对象
 * @param method 方法
 * @param args 方法参数
 * @param methodProxy代理方法,用于执行原来的代理方法
 * @return 代理逻辑返回值
 */
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
	logging();
	System.out.println(proxy.getClass().getSuperclass());
	//这里只能拿到代理对象proxy,所以只能通过找代理对象的父方法执行原来的方法
	//如果能够拿到原始的被代理对象(比如通过构造函数传入),则可以通过method来进行执行
	Object result=methodProxy.invokeSuper(proxy, args);
	return result;
}

intercept每个参数代表的含义不同,我们如果需要调用被代理类的方法,需要通过MethodProxy的invokeSuper方法调用。 设置增强类的过程

public Object getProxy(Class<?> cls) {
		//增强类
		Enhancer enhancer=new Enhancer();
		//设置增强的对象
		enhancer.setSuperclass(cls);
		//设置代理逻辑
		enhancer.setCallback(this);
		return enhancer.create();
	}

调用过程

//cglib动态代理
CglibProxy cglibProxy=new CglibProxy();
UserManagerImpl cglibUserManager=(UserManagerImpl)cglibProxy.getProxy(UserManagerImpl.class);
cglibUserManager.addBatch(new ArrayList<>());
cglibUserManager.delUser(1);

参考原文:

  • http://suo.im/5at3wJ
Show Disqus Comments

Search

    Post Directory