JAVA代理那些事儿

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/l00149133/article/details/78753729

对于java里的代理,一直是一知半解,这次打算一次性解决这个问题。

我不打算按部就班的解释什么叫代理,而是从一个需求入手,来一步步的引出代理的概念

首先我们有一个项目,完成了一些功能。
在这个项目里面,我们有个UserManager的类,里面有个方法,叫addUser(User):

class UserManager{
    public void addUser(User user){
        //add a user
    }
}

当我们开发完成后,我们突然发现,应该在addUser执行前添加一些东西(比如加个log,或者我们想把user存到缓存里面),而且我们也希望在addUser执行后再做一些事(比如加个log)

一个简单的办法就是我们直接修改方法:

public class UserManager{
    public void addUser(User user){
        //do something before
        //add a user
        //do something after
    }
}

但是这时候会存在这么几种情况:
1. 需要加前置和后缀的方法很多
2. UserManager不是我们写的类,我不知道它里面的细节,也不想去改动它的代码,以免产生别的什么bug
3. 我压根儿没有UserManager的代码,我们拿到的就是一个jar包,无法修改类的代码

这时候,代理就出现了。
所谓的代理,是为其他对象提供一种代理以控制对这个对象的访问。
就是我们不直接调用我们想调用的类和方法,而是通过代理来调用。
比如说,如果你要买台电脑,你肯定不会直接去dell买,而且会选择去中关村,这里中关村就是一个代理。

还是上面的例子,如果我们使用代理的话,代码是这样的:

//接口
public interface Manager{
    public void addUse(User user);
}
//UserManager,我们稍微修改了一下,让它实现了Manager接口,而且我们也没有在UserManager里面去加入前置和后缀逻辑
public class UserManager implements Manager{
    public void addUser(User user){
        //add a user
    }
}
//代理类,在代理类的addUser方法中,我们调用了UserManager的addUser方法,同时我们增加了前置和后缀逻辑
public class UserManagerProxy implements Manager{
    private UserManager manager = new UserManager();
    public void addUser(User user){
        //do something before
        manager.addUser(user);
        //do something after
    }
}
//测试代码,调用了代理类的addUser方法
public class TestProxy 
{
   public static void main(String args[])
   {
       Manager manager = new UserManagerProxy();
       manager.addUser(user);
   }
}

代码结束。
可以看到,我们多写了不少代码,这段代码帮我们解决了一些问题,但是也带来了另外一些问题
解决的问题:
1. 我们成功的加入了前置和后缀,而且没有搞乱UserManager这个类的任何代码。你可以试想一下,如果我们要添加的代码,跟业务本身并没有什么关系,比如UserManager看名字是一个管理用户的类,而我们要添加的代码是几行log信息,或者是为了让addUser是一个事务(这个我们后面会讲到)。这个时候,我们把这些业务无关的代码掺杂到UserManager显然是不好的

另外一些问题:
1. 有多少个需要被代理的类(术语叫委托类,后面我们会用这个术语),我们就需要写多少个代理类。如果需要被代理的类太多,对程序员来说也是一个不小的负担
2. UserManager居然必须要去实现一个接口!这样我还是动了UserManager的代码。而且如果UserManager只是一个jar包里面的类,那我该怎么办?

我们先来解决第一个问题:有多少个委托类,我们就需要写多少个代理类
上面的代理的实现,用一个术语叫做“静态代理”。从字面意思上我们也可以看出来,它跟委托类的关系是静态的,所谓的静态,指的是在代码编译期间,代理类和委托类的关系就确定了—-UserManagerProxy就是用来代理UserManager。在代码运行之前,两者就已经确认了关系,你无法来改变。

扫描二维码关注公众号,回复: 3878979 查看本文章

要解决第一个问题,可以用下面的代码来实现:

//接口我们还是跟静态代理保持一致
public interface Manager{
    public void addUse(User user);
}
//UserManager,我们也还是不需要修改
public class UserManager implements Manager{
    public void addUser(User user){
        //add a user
    }
}
/*但是这里我们不再使用静态代理类UserManagerProxy,而是换成了另外一个UserManagerHandler,并且实现了接口InvocationHandler,它是java.lang.reflect包里的一个接口,现在你不需要知道太多它的细节,只需要知道我们通常使用它来实现动态代理,而且我们要实现它里面的invoke方法*/
public class ManagerHandler implements InvocationHandler {
    private Object target;

    //绑定委托对象,并返回代理类
    public Object bind(Object target)
    {
        this.target = target;
        //绑定该类实现的所有接口,取得代理类 
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                                      target.getClass().getInterfaces(),
                                      this);
    }

    //这里的几个参数说明一下:proxy是代理类的实例,method是被代理类的方法,在这里也就是UserManager的addUer方法,args是被代理类方法的参数,在这里就是addUer方法的参数User
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {        
        //do something before
        //method.invoke通过反射,调用了target类的method方法,一会我们会把target设置成UserManager的实例
        Object value = method.invoke(target, args);
        //do something after
        return value;
    }
}
//测试类,可以看到,当我们bind方法传入了UserManager的实例后,调用了代理类的addUser方法,这样实际上执行的是UserManager类的addUser方法
public class TestProxy {
    public void addUser(User user) {
        ManagerHandler managerHandler = new UserManagerHandler();
        (Manager) proxy = (Manager)managerHandler.bind(new UserManager());
        proxy.addUser(user);
    }
}

代码结束。
可以看到,通过上面的代理,我们解决了问题:有多少个委托类,我们就需要写多少个代理类。
如果我们现在还有一个类,叫VIPManager(假设它也实现了接口Manager),那我们依旧可以通过代理类来代理它:

//只需要把UserManager的实例换成VIPManager的实例,方法可能不叫addUser而是addVIP了
public class TestProxy {
    public void addUser(User user) {
        ManagerHandler managerHandler = new UserManagerHandler();
        (Manager) proxy = (Manager)managerHandler.bind(new VIPManager());
        proxy.addVIP(user);
    }
}

而且,即便VIPManager没有实现Manager接口,而是实现了另外一个接口,比如ImportPerson接口,那我们还是可以代理它:

//把Manager接口换成ImportPerson
public class TestProxy {
    public void addUser(User user) {
        ManagerHandler managerHandler = new UserManagerHandler();
        (ImportPerson) proxy = (ImportPerson)managerHandler.bind(new VIPManager());
        proxy.addVIP(user);
    }
}

当然,你需要确保的是,VIPManager和UserManager所需要添加的前置和后缀业务是一样的。
也就是说,在这里,区分是否需要创建多个代理类的依据,由“有多少个委托类”,变成了“我需要额外添加的业务逻辑有多少种”

上面的这种代理模式,我们就解决了“有多少个委托类,我们就需要写多少个代理类”。而这种代理模式,我们叫做动态代理,区别于静态代理,我们可以看到,委托类跟代理类并没有绑死,我们可以用一个代理类来完成多个委托类的代理。
而且,我们还可以在代码运行期间来改变我们代理类所代理的委托类!
看到这里你可能会有疑问了,你的代码里面都写死了VIPManager或UserManager,类一旦装载到方法去,还怎么在代码运行期间来改变委托类?
这里就要提到另外一个知识点:声明式编程。同样这里你也无需了解太多,你只需要知道,java可以通过读取xml的声明,从而达到运行时修改代码(其实关于声明式编程你天天都在用,spring就是个典型的例子)

这样问题就迎刃而解了!我们通过XML配置,来声明我们是要传入一个VIPManager还是UserManager的实例,这样动态代理就完成了运行时才确认究竟代理哪个委托类(实际状况是,运行时才会创建一个代理类,而不是编译时就生成了ManagerProxy.class)!

问题一解决了,现在还剩下问题二:如果要使用代理,我们必须要实现接口
这里再引入一个知识点:CGLIB
cglib是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。
你可以从概念上看出来,这家伙就是为了在运行时改变代码而生

关于cglib我们不在这里展开讨论,但是你需要指导cglib有一个接口MethodInterceptor,我们可以用它来实现动态代理,而无需委托类实现接口。

//UserManager,现在它不再实现任何接口了,就是一个简单的类
public class UserManager{
    public void addUser(User user){
        //add a user
    }
}
public class CglibProxy implements MethodInterceptor{ 
 //这是一个增强类(Enhancer的意思也是增强),我们需要用它来创建一个“增强的”UserManager
 private Enhancer enhancer = new Enhancer();  

 //这个方法类似我们在上面动态代理里面的bind方法,用来创建一个代理类的实例,同时绑定委托类,所以这里的参数clazz我们一会会传入UserManager.class
 public Object getProxy(Class clazz){  
  //设置需要创建子类的类  
  enhancer.setSuperclass(clazz);  
  enhancer.setCallback(this);  
  //通过字节码技术动态创建子类实例  
  return enhancer.create();  
 }  

 //intercept是MethodInterceptor接口要实现的方法,类似于上面动态代理里面的invoke方法
 public Object intercept(Object obj, Method method, Object[] args,  
   MethodProxy proxy) throws Throwable {  
  System.out.println("这里写入前置的逻辑");  
  //通过代理类调用父类中的方法  
  Object result = proxy.invokeSuper(obj, args);  
  System.out.println("这里写入后置的逻辑");  
  return result;  
 }  
}  
//只需要把UserManager的实例换成VIPManager的实例,方法可能不叫addUser而是addVIP了
public class TestProxy {
    public void addUser(User user) {
        CglibProxy proxy = new CglibProxy();  
        //通过生成子类的方式创建代理类,这里传入了UserManager.class,说明委托类是UserManager  
        UserManager proxyImp = (UserManager)proxy.getProxy(UserManager.class);  
        proxyImp.addUser(user); 
    }
}

代码结束。
可以看到,UserManager没有实现任何接口,但是我们还是完成了动态代理。

后记:
我们讲了3种模式:
静态代理,动态代理以及cglib动态代理
显然cglib动态代理更加实用。

那在实际应用中,动态代理到底能干什么呢?总不会只是加个log吧?
当然不是。
首先,大家有没有发现,动态代理非常非常类似我们的一个概念AOP(面向切面编程)。
而实际上,动态代理一个很大的作用就是用来实现AOP的,spring也是通过动态代理来实现的AOP。
而AOP可以干什么呢?
举几个例子:
1. 当然我们可以加log
2. 对于一些数据库的操作,我们做事务控制。对于所有以DAO结尾的类,所有的方法执行之前都要调用TransactionManager.begin(),执行之后都要调用TransactionManager.commit(), 如果抛出异常的话调用TransactionManager.rollback()。
3. 添加缓存。比如对于一些我们希望缓存起来是数据,调用redis的缓存方法,并且在获取的时候先检查缓存中是否存在,如果存在则直接返回缓存中的数据

举个添加缓存的例子吧:

//我们还是用UserManager,但是现在我们要处理的方法是getUser
public class UserManager{
    public User getUser(String uid){
        //访问数据库获取User数据,代码略
        return User;
    }
}
public class CglibProxy implements MethodInterceptor{ 
 private Enhancer enhancer = new Enhancer();  
 //这是我们自己写的一个缓存类,用来缓存数据
 private Cached cached;

 public Object getProxy(Class clazz){  
  enhancer.setSuperclass(clazz);  
  enhancer.setCallback(this);  
  return enhancer.create();  
 }  

 //我们在intercept来实现写入缓存的业务
 public Object intercept(Object obj, Method method, Object[] args,  
   MethodProxy proxy) throws Throwable {  
      //获取我们要调用的方法的参数(这里我们一会要调用的是UserManager的getUser方法)
      Type[] types = method.getParameterTypes();

      //如果方法是以get开头,而且只有一个参数,且参数类型是String
      if (method.getName().matches("get.+") && (types.length == 1) &&
                (types[0] == String.class)) {
            //获取参数,args是参数列表,还记得吧?
            String key = (String) args[0];

            //根据uid,看看我们的缓存里面有没有存过这个用户
            Object value = cached.get(key);

            //如果没有存,那我们就调用UserManager的getUser方法,并且把获取到的用户传入到我们的cached里面,如果存了,我们就不调用getUser方法,而直接返回缓存里面的User!而这个操作对于调用者来说是不知道的,他还会以为自己调用了getUser方法!
            if (value == null) {
                value = method.invoke(target, args);
                cached.put(key, value);
            }
            return value;
        }


  //走到这里说明method没有满足我们的条件,也就是说不是getUser方法。当然你也可以通过筛选method,来让更多的method匹配我们的条件,比如getVIPUser(String)其实也会匹配我们的条件
  return method.invoke(target, args); 
 }  
}  

代码结束。
怎么样,这样看来非常实用吧:
1. 没有修改UserManager的任何代码,对于UserManager来说,它甚至不知道代理类的存在
2. 在没有侵入UserManager的同时,我们给user做了缓存!!
3. 而且我们还不止给getUser做了缓存,我们还可以给getVIPUser,甚至addUser做一些更好的操作!!
4. 我们甚至在有缓存的情况下,没有调用getUser方法,减少了数据库的访问,而直接从缓存中返回了数据!!!

想象一下实际场景吧!
你完成了一个项目,在这个项目里,你从数据库读取数据展示给用户。
在项目上线后,随着用户的增多,你发现数据库读取扛不住了!
经过一系列分析和操作(比如读写分离)后,你发现给某些业务加个缓存可能是个不错的办法
当然,你也不是非常确定缓存一定能解决所有问题

这个时候,如果没有动态代理,你需要修改所有要加缓存的方法!!如果是个大项目,那代码里不会少了
更让人蛋疼的是,你不确定缓存能解决问题,所以可能修改了后,你还要改回来,或者改成别的!!

基本上要疯啊
而这个时候,如果你使用动态代理,那就是从地狱到天堂了?
你可以通过method来判断给哪些方法来加缓存,你可以通过args[]参数的个数和类型,来判断给哪些方法来加缓存
最重要的是,如果缓存解决不了问题,你还可以快速的换其他方案!!!
还有什么比这个更爽的=。=
OVER.

猜你喜欢

转载自blog.csdn.net/l00149133/article/details/78753729