Java 代理

背景

小明是一个客户,它让小红帮忙购买香水,小红就成了一个代理对象,而香水提供商是一个真实的对象,可以售卖香水,小明通过代理商小红,购买到法国的香水,这就是一个代购的例子
代理可以分为静态代理和动态代理两大类:
在这里插入图片描述

静态代理

  • 优点:代码结构简单,较容易实现
  • 缺点:无法适配所有代理场景,如果有新的需求,需要修改代理类,不符合软件工程的开闭原则

动态代理

  • 优点:能够动态适配特定的代理场景,扩展性较好,符合软件工程的开闭原则
  • 缺点:动态代理需要利用到反射机制和动态生成字节码,导致其性能会比静态代理稍差一些,但是相比于优点,这些劣势几乎可以忽略不计

代理模式

(1)我们定义好一个售卖香水的接口,定义好售卖香水的方法并传入该香水的价格。

public interface SellPerfume {
    void sellPerfume(double price);
}

(2)定义香奈儿(Chanel)香水提供商,实现接口

public class ChanelFactory implements SellPerfume {
    @Override
    public void sellPerfume(double price) {
        System.out.println("成功购买香奈儿品牌的香水,价格是:" + price + "元");
    }
}

(3)定义小红代理类,她需要代购去售卖香奈儿香水,所以她是香奈儿香水提供商的代理对象,同样实现接口,并在内部保存对目标对象(香奈儿提供商)的引用,控制其它对象对目标对象的访问

public class XiaoHongSellProxy implements SellPerfume {
    private SellPerfume sellPerfumeFactory;

    public XiaoHongSellProxy(SellPerfume sellPerfumeFactory) {
        this.sellPerfumeFactory = sellPerfumeFactory;
    }

    @Override
    public void sellPerfume(double price) {
        doSomethingBeforeSell(); // 前置增强
        sellPerfumeFactory.sellPerfume(price);
        doSomethingAfterSell(); // 后置增强
    }

    private void doSomethingBeforeSell() {
        System.out.println("小红代理购买香水前的额外操作...");
    }

    private void doSomethingAfterSell() {
        System.out.println("小红代理购买香水后的额外操作...");
    }
}

(4)小明是一个需求者,他需要去购买香水,只能通过小红去购买

public class XiaoMing {
    public static void main(String[] args) {
        ChanelFactory factory = new ChanelFactory();
        XiaoHongSellProxy proxy = new XiaoHongSellProxy(factory);
        proxy.sellPerfume(1999.99);
    }
}

在这里插入图片描述
实现代理模式,需要走以下几个步骤:

  • 定义真实对象和代理对象的公共接口(售卖香水接口)
  • 代理对象内部保存对真实目标对象的引用(小红引用提供商)
  • 访问者仅能通过代理对象访问真实目标对象,不可直接访问目标对象(小明只能通过小红去购买香水,不能直接到香奈儿提供商购买)

代理模式的目的:

  • 通过代理对象的隔离,可以在对目标对象访问前后增加额外的业务逻辑,实现功能增强。
  • 通过代理对象访问目标对象,可以防止系统大量地直接对目标对象进行不正确地访问,出现不可预测的后果

动态代理在静态代理的基础上做了改进,极大地提高了程序的可维护性和可扩展性

  • 动态代理产生代理对象的时机是运行时动态生成,它没有 Java 源文件,直接生成字节码文件实例化代理对象;而静态代理的代理对象,在程序编译时已经写好 Java 文件了,直接 new 一个代理对象即可。
  • 动态代理比静态代理更加稳健,对程序的可维护性和可扩展性更加友好

动态代理解决的问题是面对新的需求时,不需要修改代理对象的代码,只需要新增接口和真实对象,在客户端调用即可完成新的代理。

JDK Proxy

JDK Proxy 是 JDK 提供的一个动态代理机制,它涉及到两个核心类,分别是Proxy和InvocationHandler

以小红代理卖香水的故事为例,香奈儿香水提供商依旧是真实对象,实现了SellPerfume接口,这里不再重新写了,重点是小红代理,这里的代理对象不再是小红一个人,而是一个代理工厂,里面会有许多的代理对象

在这里插入图片描述

代理对象是在程序运行过程中,由代理工厂动态生成,代理对象本身不存在 Java 源文件。

首先,代理工厂需要实现InvocationHanlder接口并实现其invoke()方法

public class SellProxyFactory implements InvocationHandler {
    /**
     * 代理的真实对象
     */
    private Object realObject;

    public SellProxyFactory(Object realObject) {
        this.realObject = realObject;
    }

    /**
     * invoke() 方法是一个代理方法,
     * 也就是说最后客户端请求代理时,执行的就是该方法
     *
     * @param proxy  代理对象
     * @param method 真正执行的方法
     * @param args   调用第二个参数 method 时传入的参数列表值
     *
     * @return
     *
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        doSomethingBefore();
        Object obj = method.invoke(realObject, args);
        doSomethingAfter();
        return obj;
    }

    private void doSomethingAfter() {
        System.out.println("执行代理后的额外操作...");
    }

    private void doSomethingBefore() {
        System.out.println("执行代理前的额外操作...");
    }

如何通过代理工厂动态生成代理对象
生成代理对象需要用到Proxy类,它可以帮助我们生成任意一个代理对象,里面提供一个静态方法newProxyInstance。

 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

客户端请求代理时

    public static void main(String[] args) {
        //设置系统属性,生成$Proxy0的class文件,在项目根目录下增加com/sun/proxy目录
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        ChanelFactory chanelFactory = new ChanelFactory();
        SellProxyFactory sellProxyFactory = new SellProxyFactory(chanelFactory);
        SellPerfume sellPerfume =
                //生成代理对象需要用到Proxy类,它可以帮助我们生成任意一个代理对象
                (SellPerfume) Proxy.newProxyInstance(
                        //加载动态代理类的类加载器
                        chanelFactory.getClass().getClassLoader(),
                        //代理类实现的接口,可以传入多个接口
                        chanelFactory.getClass().getInterfaces(),
                        //指定代理类的调用处理程序,即调用接口中的方法时,会找到该代理工厂h,执行invoke()方法
                        sellProxyFactory
                );
        sellPerfume.sellPerfume(1999.99);
    }

在这里插入图片描述

新增红酒代理功能时,需要2个步骤:

  • 创建新的红酒提供商SellWineFactory和售卖红酒接口SellWine
  • 在客户端实例化一个代理对象,然后向该代理对象购买红酒

开闭原则:面向扩展开放,面向修改关闭。动态代理正是满足了这一重要原则,在面对功能需求扩展时,只需要关注扩展的部分,不需要修改系统中原有的代码。

Proxy.newProxyInstance() 是生成动态代理对象的关键

private static final Class<?>[] constructorParams ={ InvocationHandler.class };
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {
    // 获取代理类的 Class 对象
    Class<?> cl = getProxyClass0(loader, intfs);
    // 获取代理对象的显示构造器,参数类型是 InvocationHandler
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    // 反射,通过构造器实例化动态代理对象
    return cons.newInstance(new Object[]{h});
}

动态代理对象,那么是如何生成的呢

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    // 去代理类对象缓存中获取代理类的 Class 对象
    return proxyClassCache.get(loader, interfaces);
}

发现里面用到一个缓存 proxyClassCache,从结构来看类似于是一个 map 结构,根据类加载器loader和真实对象实现的接口interfaces查找是否有对应的 Class 对象

 public V get(K key, P parameter) {
     // 先从缓存中查询是否能根据 key 和 parameter 查询到 Class 对象
     // ...
     // 生成一个代理类
     Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
 }

在 get() 方法中,如果没有从缓存中获取到 Class 对象,则需要利用 subKeyFactory 去实例化一个动态代理对象,而在 Proxy 类中包含一个 ProxyClassFactory 内部类,由它来创建一个动态代理类,所以我们接着去看 ProxyClassFactory 中的 apply() 方法

private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
    // 非常重要,这就是我们看到的动态代理的对象名前缀!
	private static final String proxyClassNamePrefix = "$Proxy";

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        // 一些状态校验
		
        // 计数器,该计数器记录了当前已经实例化多少个代理对象
        long num = nextUniqueNumber.getAndIncrement();
        // 动态代理对象名拼接!包名 + "$Proxy" + 数字
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        // 生成字节码文件,返回一个字节数组
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            // 利用字节码文件创建该字节码的 Class 类对象
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            throw new IllegalArgumentException(e.toString());
        }
    }
}

apply() 方法中注意有两个非常重要的方法:

  • ProxyGenerator.generateProxyClass():它是生成字节码文件的方法,它返回了一个字节数组,字节码文件本质上就是一个字节数组,所以 proxyClassFile数组就是一个字节码文件
  • defineClass0():生成字节码文件的 Class 对象,它是一个 native 本地方法,调用操作系统底层的方法创建类对象

CGLIB

CGLIB(Code generation Library) 不是 JDK 自带的动态代理,它需要导入第三方依赖,它是一个字节码生成类库,能够在运行时动态生成代理类对 Java类 和 Java接口 扩展

  1. 依赖

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib-nodep</artifactId>
        <version>3.3.0</version>
        <scope>test</scope>
    </dependency>
    
  2. CGLIB 代理中有两个核心的类:MethodInterceptor接口 和 Enhancer类,前者是实现一个代理工厂的根接口,后者是创建动态代理对象的类

    /**
     * 定义代理工厂SellProxyFactory
     */
    public class SellProxyFactory implements MethodInterceptor {
    
        // 关联真实对象,控制对真实对象的访问
        private Object realObject;
    
        /**
         * 从代理工厂中获取一个代理对象实例,等价于创建小红代理
         */
        public Object getProxyInstance(Object realObject) {
            this.realObject = realObject;
    
            Enhancer enhancer = new Enhancer();
            // 设置需要增强类的类加载器
            enhancer.setClassLoader(realObject.getClass().getClassLoader());
            // 设置被代理类,真实对象
            enhancer.setSuperclass(realObject.getClass());
            // 设置方法拦截器,代理工厂
            enhancer.setCallback(this);
            // 创建代理类
            return enhancer.create();
        }
    
        /**
         * @param o           被代理对象
         * @param method      被拦截的方法
         * @param objects     被拦截方法的所有入参值
         * @param methodProxy 方法代理,用于调用原始的方法
         *
         * @return
         *
         * @throws Throwable
         */
        @Override
        public Object intercept(Object o,
                                Method method,
                                Object[] objects,
                                MethodProxy methodProxy) throws Throwable {
            doSomethingBefore(); // 前置增强
            Object object = methodProxy.invokeSuper(o, objects);
            doSomethingAfter(); // 后置增强
            return object;
        }
    
        private void doSomethingBefore() {
            System.out.println("执行方法前额外的操作...");
        }
    
        private void doSomethingAfter() {
            System.out.println("执行方法后额外的操作...");
        }
    
    }
    
  3. 调用

       public static void main(String[] args) {
        //生动态代理 class 文件位置
        String path = XiaoMing.class.getResource("").getPath();
        System.out.println("生动态代理 class 文件位置" + path);
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, path);
    
    
        SellProxyFactory sellProxyFactory = new SellProxyFactory();
        // 获取一个代理实例
        SellPerfumeFactory proxyInstance =
                (SellPerfumeFactory) sellProxyFactory.getProxyInstance(new SellPerfumeFactory());
        // 创建代理类
        proxyInstance.sellPerfume(1999.99);
    
    
    }
    

CGLIB 的使用方法:

  • 代理工厂需要实现 MethodInterceptor 接口,并重写方法,内部关联真实对象,控制第三者对真实对象的访问;代理工厂内部暴露 getInstance(Object realObject) 方法,用于从代理工厂中获取一个代理对象实例
  • Enhancer 类用于从代理工厂中实例化一个代理对象,给调用者提供代理服务。

JDK Proxy 和 CGLIB 的对比

在这里插入图片描述

参考

Java 代理

Cglib源码分析 invoke和invokeSuper的差别

cglib动态代理中invokeSuper和invoke的区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java代理模式是一种结构型设计模式,其目的是为其他对象提供一种代理以控制对这个对象的访问。代理对象可以在客户端和目标对象之间充当中介,以便于客户端访问目标对象时,可以在不改变目标对象的情况下添加一些额外的功能,比如安全性、远程访问、缓存等。 在Java中,代理模式可以通过两种方式实现:静态代理和动态代理。静态代理需要手动编写代理,而动态代理可以在运行时通过反射机制动态生成代理,更加灵活。 举个例子,假设我们有一个接口`Subject`,其中定义了一些方法。我们希望在调用这些方法时,增加一些额外的日志记录功能。我们可以编写一个代理`SubjectProxy`,在代理中实现接口方法并调用目标对象的方法,同时在方法前后添加日志记录的代码。客户端则通过代理访问目标对象。 静态代理示例代码如下: ```java public interface Subject { void doSomething(); } public class RealSubject implements Subject { @Override public void doSomething() { System.out.println("RealSubject do something."); } } public class SubjectProxy implements Subject { private Subject realSubject; public SubjectProxy(Subject realSubject) { this.realSubject = realSubject; } @Override public void doSomething() { System.out.println("Before do something."); realSubject.doSomething(); System.out.println("After do something."); } } public class Client { public static void main(String[] args) { Subject realSubject = new RealSubject(); Subject subjectProxy = new SubjectProxy(realSubject); subjectProxy.doSomething(); } } ``` 动态代理示例代码如下: ```java public class SubjectHandler implements InvocationHandler { private Object target; public SubjectHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before " + method.getName()); Object result = method.invoke(target, args); System.out.println("After " + method.getName()); return result; } } public class Client { public static void main(String[] args) { Subject realSubject = new RealSubject(); InvocationHandler handler = new SubjectHandler(realSubject); Subject subjectProxy = (Subject) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler); subjectProxy.doSomething(); } } ``` 无论是静态代理还是动态代理代理模式都可以在不改变目标对象的情况下,为其添加额外的功能,提高代码的可复用性和灵活性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值