在开发中,经常遇到代理问题,尤其是动态代理,在这里,本人对Java中的动态代理做一个小结。
在工作中,我们发现,当对所有业务类都需要打日志时,我们有两种方案:
-
在每个类中加入日志代码(每个类都写一次,累不累?!);
-
实现动态代理,只需要写一次日志代码就搞定了(对于我这种懒人来说,当然是这种了!);
有的人会说,那直接使用Spring的AOP不就行了么?
答案当时是:可以的!但是,你知道AOP是怎么实现的么?
AOP实际上就是动态代理,而且可选的动态代理还有两种,一种是JDK自带的代理,另一种是Cglib(多以Cglib为主)。
那么在将动态代理之前,先来一个小例子帮助大家理解,不多说,直接上代码:
业务接口类:
public interface OrderService {
String getOrder();
long getOrderId();
}
业务实现类:
public class OrderServiceImpl implements OrderService {
@Override
public String getOrder() {
System.out.println("------getOrder-----");
return "order_123456";
}
@Override
public long getOrderId() {
System.out.println("------getOrderId-----");
return 123456;
}
}
对于这个业务,我们需要进行代理,在每次执行getOrder方法的时候,我们需要进行其他一些操作,那么我试着去代理一下:
public class OrderServiceStaticProxy {
private OrderService orderService;
OrderServiceStaticProxy(OrderService orderService) {
this.orderService = orderService;
}
public String getOrder() {
System.out.println("------before getOrder-----");
String result = orderService.getOrder();
System.out.println("------after getOrder-----");
return result;
}
public long getOrderId() {
return orderService.getOrderId();
}
}
测试类:
public class StaticProxyTest {
public static void main(String[] args) {
OrderService orderService = new OrderServiceImpl();
OrderServiceStaticProxy orderServiceProxy
= new OrderServiceStaticProxy(orderService);
System.out.println(orderServiceProxy.getOrder());
System.out.println(orderServiceProxy.getOrderId());
}
}
输出:
------before getOrder-----
------getOrder-----
------after getOrder-----
order_123456
------getOrderId-----
123456
好了,我们已经成功为业务实现了一个代理了!
这是一个静态代理,OrderServiceStaticProxy类通过持有OrderService对象,来实现对OrderService所有方法的代理,比如控制在执行方法前后打印日志等。但是这种方法的缺点也很明显,就是我每写一个类,再增加一个代理类,有100个,我就写100次,有1000个,我就写1000次!!!程序员当然是要以最小的代价来完成工作!所以,我如何能只写一遍代理,然后就直接使用就可以了呢?
好了,正片开始了
JDK动态代理
废话不多说,直接上代码,代码之下,无所遁形:
public class OrderServiceProxy implements InvocationHandler {
private Object target;
OrderServiceProxy() {
super();
}
OrderServiceProxy(Object target) {
super();
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;
if (Objects.equals("getOrder", method.getName())) {
System.out.println("-------before-------");
result = method.invoke(target, args);
System.out.println("-------after-------");
} else {
result = method.invoke(target, args);
}
return result;
}
}
public class ProxyTest {
public static void main(String[] args) {
OrderServiceImpl orderService = new OrderServiceImpl();
InvocationHandler invocationHandler = new OrderServiceProxy(orderService);
OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(
orderService.getClass().getClassLoader(),
orderService.getClass().getInterfaces(),
invocationHandler);
System.out.println(orderServiceProxy.getOrder());
System.out.println(orderServiceProxy.getOrderId());
}
}
输出:
-------before-------
------getOrder-----
-------after-------
order_123456
------getOrderId-----
123456
在讲解这段代码之前,我先简单讲一个JVM中的加载机制。
总得来说,就是我们写的java代码,机器是不认识的,所有需要编译,转成二进制文件,就是编译完后的.class文件。那么,这些二进制文件是我们一启动JVM就加载进去了么?答案是:否。这些二进制文件只有在被调用的时候才会由相应的ClassLoader加载进JVM中,这时候,才能真正有了对象供程序调用。好,加载机制先了解到这里(详细的过程,可查阅JVM相关的资料,这里不细说)
先说OrderServiceProxy,它实现了InvocationHandler,InvocationHandler中有一个方法invoke(),invoke()中三个参数,分别是:需要代理的对象proxy,代理对象的方法method,以及需要的参数args。什么意思呢?就是说,假如我现在知道了我需要代理对象、方法、参数了,我不就可以根据判断来就执行我需要额外执行业务了吗?然后通过method.invoke(target, args)来调用代理对象的方法了,至于method.invoke(target, args)这里面的实现逻辑,我们是不用关心的,因为里头其实就是写如何找到相关的类,执行相关方法的流程,JDK都已经封装好了。
然后,再通过Proxy.newProxyInstance()来生成一个代理类,这个代理类,实际上是调用invoke()方法来执行target(即代理对象)的方法。
public class Proxy implements java.io.Serializable {
...
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* 1.通过ClassLoader和接口来查找接口类
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* 2.通过接口来获取构造对象
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
/*
* 3.通过构造对象和InvocationHandler来构造实例,并返回
*/
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
}
具体来讲就三个步骤:
- 根据ClassLoader和Interface来获取接口类(前面已经讲了,类是由ClassLoader加载到JVM的,所以通过ClassLoader和Interface可以找到接口类)
2.获取构造对象;
3.通过构造对象和InvocationHandler生成实例,并返回,就是我们要的代理类。
Java动态代理优缺点:
优点:
1.Java本身支持,不用担心依赖问题,随着版本稳定升级;
2.代码实现简单;
缺点:
1.目标类必须实现某个接口,换言之,没有实现接口的类是不能生成代理对象的;
2.代理的方法必须都声明在接口中,否则,无法代理;
3.执行速度性能相对cglib较低;
进阶篇
常见错误1:
这就是常见的栈溢出,在这里也就是递归调用InvocationHandler的invoke方法问题。产生这个问题的原因,从源码可以读取出来:
比如你写了一段
常见错误2:
提示实例不是对象的声明类
导致这个错误的原因是当我们执行method.invoke(参数1,…)方法时候,如果method这个方法,不是invoke参数1的实例方法,就会报这个,如有一个类A和B都是M的接口实现类,用A的method去调用B实例
还有一种特殊情况比如A类的method调用A类的代理类,也会出现这个问题。这个时候,要用A类的接口类方法去调用A类的代理类,就可以避免出现这个错误。
有兴趣的可以看看码云这个GVP项目:
项目地址:https://gitee.com/dromara/koalas-rpc.git
看下面截图:
public String invoke(GenericRequest request) throws TException {
//"getRpc"
String methodName = request.getMethodName ();
//["java.lang.String","java.lang.Long"]
List<String> classTypes = request.getClassType ();
//["4","5"]
List<String> requestObjs = request.getRequestObj ();
Class<?> targetImpl = AopUtils.getTargetClass ( realImpl );
this.realClassName = targetImpl.getName ();
checkparam(request);
List<Class> realClassTypes = null;
if(classTypes!=null && classTypes.size ()>0){
realClassTypes = new ArrayList<> ( );
for(String _classTypes:classTypes){
try {
realClassTypes.add (getClassType(_classTypes));
} catch (ClassNotFoundException e) {
throw new TException ("class:" + realClassName + ",classType:"+_classTypes+ " not found in the server side!" );
}
}
}
Method method=null;
List<Object> realRequest = null;
if(realClassTypes==null || realClassTypes.size ()==0){
try {
method = targetImpl.getMethod ( methodName );
} catch (NoSuchMethodException e) {
throw new TException ( "class:" + realClassName + ",method:"+methodName+ " not found !" );
}
} else{
try {
method = targetImpl.getMethod ( methodName ,realClassTypes.toArray ( new Class[0] ));
} catch (NoSuchMethodException e) {
throw new TException ( "class:" + realClassName + ",method:"+methodName+ " not found !" );
}
realRequest = new ArrayList<> ( );
for(int i =0;i<requestObjs.size ();i++){
try {
Object o= JSONObject.parseObject (requestObjs.get ( i ),realClassTypes.get ( i ));
realRequest.add ( o );
} catch (Exception e) {
throw new TException ( "class:" + realClassName + ",method:"+methodName+ " ,JSONObject.parseObject error, ! text:"+ requestObjs.get ( i ) + ",classType:" + realClassTypes.get ( i ) );
}
}
}
Object ojb =null;
if(realClassTypes==null || realClassTypes.size ()==0){
try {
ojb = method.invoke ( realImpl );
} catch (Exception e) {
Throwable cause = e.getCause ()!=null?e.getCause ():e;
throw new TException (cause.getMessage ());
}
}else{
try {
ojb = method.invoke ( realImpl,realRequest.toArray ( new Object[0] ) );
} catch (Exception e) {
Throwable cause = e.getCause ()!=null?e.getCause ():e;
throw new TException (cause);
}
}
if(ojb==null){
return "VOID";
} else{
return JSON.toJSONString ( ojb );
}
}
ojb = method.invoke ( realImpl,realRequest.toArray ( new Object[0] ) );
看这行代码,实例中realImpl是spring aop生成的一个代理类,method是WmCreateAccountServiceThriftImpl的方法,它的接口是WmCreateAccountService.Iface
源码中就是用WmCreateAccountServiceThriftImpl调用了realImpl,就会报上面的错误
最后我联系了源码作者修复了该问题,修改的方法也很简单,
method = ifaceClass.getMethod ( methodName );
换用接口的方法去调就可以了