最全代理模式

        代理是一种模式,提供了对目标对象的间接访问方式,即通过代理对象访问目标对象.如此便于在目标实现的基础上增加额外的功能操作,以满足自身的业务需求.

常用的代理方式可以粗分为:静态代理和动态代理。

1. 静态代理

静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。

/**
 * 目标对象实现的接口
 * @author suke
 */
public interface BussinessInterface {
 
    String execute();
}
 
/**
 * 目标对象实现类
 * @author suke
 */
public class Bussiness implements BussinessInterface{
 
    @Override
    public String execute() {
        System.out.println("执行业务逻辑...");
        return "完成";
    }
}
 
/**
 * 代理类,通过实现与目标对象相同的接口
 * 并维护一个代理对象,通过构造器传入实际目标对象并赋值
 * 执行代理对象实现的接口方法,实现对目标对象实现的干预
 * @author suke
 */
public class BussinessProxy implements BussinessInterface{
     
    private BussinessInterface bussinessImpl;
     
    public BussinessProxy(BussinessInterface bussinessImpl) {
        this.bussinessImpl = bussinessImpl;
    }
     
    @Override
    public String execute() {
        System.out.println("前拦截...");
        String rs = bussinessImpl.execute();
        System.out.println("后拦截...");
        return rs;
    }
}

静态代理的总结

  优点:可以做到不对目标对象进行修改的前提下,对目标对象进行功能的扩展和拦截。

  缺点:

  1. 因为代理对象,需要实现与目标对象一样的接口,会导致代理类十分繁多,不易维护,

  2. 同时一旦接口增加方法,则目标对象和代理类都需要维护。

  3. 存在重复代码

  4. 硬编码在代理类中,不利于后期维护

2. 动态代理

上面案例中,代理类是我们事先编写的,而且要和目标对象类实现相同接口。由于Bussiness(目标对象)需要日志功能,我们即编写了BussinessProxy(代理对象),并通过构造器传入BussinessInterface(目标对象),调用目标对象同名方法的同时添加增强代码。

但是这里有个问题!代理对象构造器的参数类型是BussinessInterface,这意味着它只能接受BussinessInterface的实现类对象,亦即我们写的代理类BussinessProxy只能给BussinessInterface做代理,它们绑定死了!

如果现在我们系统需要全面改造,给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类...

自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!那么,能否让JVM根据接口自动生成代理对象呢?

比如,有没有一个方法,我传入接口,它就给我自动返回代理对象呢?

那是必须的,其实我们JDK已经提供了java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy类这两个类为了实现动态代理

2.1 动态代理的实现推理

接下来我们需要思考,怎么传递一个接口,就可以通过这个接口动态生成对应的代理实现类.

我们回顾一下,之前我们学习对象的创建过程:

虽然知道源代码经过javac命令编译后会在磁盘中得到字节码文件(.class文件),也知道java命令会启动JVM将字节码文件加载进内存,但也仅仅止步于此了。至于从字节码文件加载进内存到堆中产生对象,期间具体发生了什么,他们并不清楚。

所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。它被加载进内存后,JVM为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。

所以借此机会,我们不妨换种方式看待类和对象:

也就是说,要得到一个类的实例,关键是先得到该类的Class对象!只不过new这个关键字实在太方便,为我们隐藏了底层很多细节.

我们都知道接口Class没有构造器,无法new.那怎么办呢?

但是,仔细想想,接口之所以不能new,是因为它缺少构造器,它本身是具备完善的类结构信息的。就像一个武艺高强的大内太监(接口),他空有一身绝世神功(类结构信息),却后继无人。如果江湖上有一位妙手圣医,能克隆他的一身武艺,那么克隆人不就武艺高强的同时,还能生儿育女了吗? ​ 所以我们就想,JDK有没有提供这么一个方法,比如getXxxClass(),我们传进一个接口Class对象,它帮我们克隆一个具有相同类结构信息,又具备构造器的新的Class对象呢?

其实在JDK的java.lang.reflect.Proxy类就提供了这个方法:

Proxy.getProxyClass():返回代理类的Class对象。终于找到妙手圣医。

也就说,只要传入目标类实现的接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。

利用Proxy类的静态方法getProxyClass()方法,给它传一个接口Class对象,它能返回一个加强版Class对象。也就是说getProxyClass()的本质是:用Class,造Class。

要谢谢Proxy类和JVM,让我们不写代理类却直接得到代理Class对象,进而得到代理对象。

静态代理

动态代理

既然Proxy提供了getProxyClass()方法得到代理类的Class对象,那我们再通过反射得到代理类对象:

    @Test
    public void test2() throws IllegalAccessException, InstantiationException {
        //通过接口得到代理类Class对象
        Class<?> proxyClass = Proxy.getProxyClass(BussinessInterface.class.getClassLoader(), BussinessInterface.class);
        //通过反射创建代理类对象
        Object proxy = proxyClass.newInstance();
    }

但是我们在运行的时候,抛出如下异常:

我们发现,newInstance()创建对象失败。因为Class的newInstance()方法底层会走无参构造器。我们查看Proxy类的源码,发现它的无参构造器是私有的,但是它提供了一个带参的构造器,如下:

所以我们只能使用protected Proxy(InvocationHandler h)这个方法

 @Test
    public void test2() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //通过接口得到代理类Class对象
        Class<?> proxyClass = Proxy.getProxyClass(BussinessInterface.class.getClassLoader(), BussinessInterface.class);
        //通过反射创建代理类对象
        Constructor<?> constructor = proxyClass.getDeclaredConstructor(InvocationHandler.class);
        BussinessInterface proxy = (BussinessInterface) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });
          //调用方法
        System.out.println(proxy.execute());
    }

上面的代码能正常运行,为什么是一个null呢?我们发现我们new的匿名内部类的invoke()方法返回值是null,那是不是这个方法返回的原因呢?我们试一下,如果我们访问一个"1",再运行代码一下:

巧合吗?应该不是。我猜:每次调用代理对象的方法都会调用invoke(),且invoke()的返回值就是代理方法的返回值。如果真是如此,null就可以解释了。

那么动态代理的大致设计思路就是

为什么这么设计?

为了解耦,也为了通用性。

如果JVM生成代理对象的同时生成了特定逻辑的方法体,那这个代理对象后期就没有扩展的余地,只能有一种玩法。而引入InvocationHandler的好处是:

  • JVM创建代理对象时不必考虑方法实现,只要造一个空壳的代理对象

  • 后期代理对象想要什么样的方法实现,我写在invocationHandler对象的invoke()方法里送进来便是

所以,invocationHandler的作用,倒像是把“方法”和“方法体”分离。JVM只造一个空的代理对象给你,后面想怎么玩,由你自己组装。反正代理对象中有个成员变量invocationHandler,每一个方法里只有一句话:handler.invoke()。所以调任何一个代理方法,最终都会跑去调用invoke()方法。

invoke()方法是代理对象和目标对象的桥梁。

但是我们真正想要的结果是:调用代理对象的方法时,去调用目标对象的方法。

所以,接下来努力的方向就是:设法在invoke()方法得到目标对象,并调用目标对象的同名方法。

2.2 代理对象调用目标对象方法

那么,如何在invoke()方法内部得到目标对象呢?我们来看看能不能从invoke()方法的形参上获取点线索:

  • Object proxy:很遗憾,是代理对象本身,而不是目标对象(不要调用,会无限递归)

  • Method method:本次被调用的代理对象的方法

  • Obeject[] args:本次被调用的代理对象的方法参数

很可惜,proxy不是代理对象。其实想想也知道,创建代理对象的过程中自始至终没有目标对象参与所以也就无法产生关联。而且一个接口可以同时被多个类实现,所以JVM也无法判断当前代理对象要代理哪个目标对象。但好在我们已经知道本次调的方法(Method)和参数(args)。我们接下来要做就是得到目标对象并调用同名方法,然后把参数给它。

如何得到目标对象呢?没办法,为今之计只能new了...哈哈哈哈。我靠,饶了一大圈,又是动态代理,又是invoke()的,结果还是要手动new?别急,先玩玩。后面会改进的:

 @Test
    public void test2() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //通过接口得到代理类Class对象
        Class<?> proxyClass = Proxy.getProxyClass(BussinessInterface.class.getClassLoader(), BussinessInterface.class);
        //通过反射创建代理类对象
        Constructor<?> constructor = proxyClass.getDeclaredConstructor(InvocationHandler.class);
        BussinessInterface proxy = (BussinessInterface) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //new 一个目标类对象
                Bussiness target = new Bussiness();
                Object rs = method.invoke(target, args);
                return rs;
            }
        });
        //调用方法
        proxy.execute();
    }

但是这样的写法显然是倒退30年,一夜回到解放前。我们需要改进一下,封装Proxy.getProxyClass(),使得目标对象可以作为参数传入:

 private static Object getProxy(final Object target) throws Exception {
        //参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
        Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "方法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "方法执行结束...");
                return result;
            }
        });
        return proxy;
    }

   @Test
    public void test2() throws Exception {
        BussinessInterface proxy = (BussinessInterface)getProxy(new Bussiness());
        //调用方法
        proxy.execute();
    }

测试:

但是还是太麻烦了。有没有更简单的方式获取代理对象?有!

  private static Object getProxy(final Object target) throws Exception {
        //直接调用newProxyInstance()方法得到代理对象
        Object proxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(),/*类加载器*/
                target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
                new InvocationHandler(){/*代理对象的方法最终都会被JVM导向它的invoke方法*/
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println(method.getName() + "方法开始执行...");
                        Object result = method.invoke(target, args);
                        System.out.println(result);
                        System.out.println(method.getName() + "方法执行结束...");
                        return result;
                    }
                }
        );
        return proxy;
    }

到此我们已经使用动态代理完成不改变目标类,而增强目标方法,但是动态代理还有有如下问题:

使用动态代理,让我们避免手写代理类,只要给getProxy()方法传入target就可以生成对应的代理对象。但是日志打印仍是硬编码在invoke()方法中。虽然修改时只要改一处,但是别忘了“开闭原则”。所以最好是能把日志打印单独拆出来,像目标对象一样作为参数传入。

日志打印其实就是AOP里的通知概念。我打算定义一个Advice接口,并且写一个MyLogger实现该接口。

通知接口

public interface Advice {
    void beforeMethod(Method method);
    void afterMethod(Method method);
}

日志打印通知类:

public class MyLogger implements Advice {

	public void beforeMethod(Method method) {
		System.out.println(method.getName() + "方法执行开始...");
	}

	public void afterMethod(Method method) {
		System.out.println(method.getName() + "方法执行结束...");
	}
}

测试类

private static Object getProxy(final Object target, Advice logger) throws Exception {
		/*代理对象的方法最终都会被JVM导向它的invoke方法*/
		Object proxy = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),/*类加载器*/
				target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
				(proxy1, method, args) -> {
					logger.beforeMethod(method);
					Object result = method.invoke(target, args);
					System.out.println(result);
					logger.afterMethod(method);
					return result;
				}
		);
		return proxy;
	}

2.3 Java 中实现动态代理方式

Java 中,实现动态代理有两种方式:

1、JDK 动态代理:java.lang.reflect 包中的 Proxy 类和 InvocationHandler 接口提供了生成动态代理类的能力。

2、Cglib 动态代理:Cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

JDK原生动态代理是Java原生支持的,不需要外部依赖,但是它只能基于接口进行代理(需要动态代理的对象必须实现与某个接口)

CGLIB通过继承的方式进行代理,要导入cglib第三方库,使用的类是Enhancer的create静态方法创建,要求被代理类不能是最终类,即不能用final修饰,如String类。

使用cglib实现动态代理:

 <dependency>
     <groupId>cglib</groupId>
     <artifactId>cglib</artifactId>
     <version>3.3.0</version>
</dependency>

   @Test
    public void test3(){
        // 创建一个被代理对象,这里要求必须是final
        final Bussiness target = new Bussiness();
        Bussiness proxy =(Bussiness) Enhancer.create(target.getClass(), new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object returnValue = null;
                System.out.println(method.getName() + "方法开始执行...");
                returnValue = method.invoke(target, objects);
                System.out.println(method.getName() + "方法执行结束...");
                return returnValue;
            }
        });
        proxy.execute();
    }
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值