FunTester Java 代理模式初探

FunTester · 2020年12月29日 · 681 次阅读

代理模式是一种常见的软件设计模式。所谓的代理者是指一个类别可以作为其它对象的接口。代理者可以作任何东西的接口:网络连接、存储器中的大对象、文件或其它重要或无法复制的资源。

有时候我们需要需要一个功能(通常是一个API)。该功能已经被某个类A实现了,代理类B实现相同的接口,并将操作交给A去处理,在这个过程中可以添加自己的功能。

之前只是听过一些说法,并未对Java代理进行学习,前些日子抽空学习了一点点,下面分享我在Java使用代理模式的一些Demo,主要分三类:静态代理JDK 动态代理cglib 动态代理

创建一个简单的代理

我们从一个接口IUserProvider开始:

package com.fun.ztest.proxytest;

public interface IUserProvider {

    User getUser(int i);

}

此接口由UserProviderImpl实现:

package com.fun.ztest.proxytest;

import com.fun.frame.SourceCode;
import com.fun.utils.RString;

public class UserProviderImpl extends SourceCode implements IUserProvider {

    @Override
    public User getUser(int i) {
        output("我是原配实现类!");
        return new User(RString.getString(i), getRandomInt(i), null);
    }


}

现在让我们为IUserProvider添加一个代理对象,该对象执行一些简单的日志记录:

package com.fun.ztest.proxytest;

import com.fun.frame.SourceCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogProxy extends SourceCode implements IUserProvider {

    private static Logger logger = LoggerFactory.getLogger(LogProxy.class);

    private IUserProvider userProvider;

    public LogProxy(IUserProvider userProvider) {
        this.userProvider = userProvider;
    }

    @Override
    public User getUser(int i) {
        logger.info("获取用户{}", i + EMPTY);
        return userProvider.getUser(i);
    }


}

我们要为IUserProvider创建代理,因此我们的代理需要实现IUserProvider。在构造函数中,AutoLogProxy类需要传入一个实际的IUserProvider实现类UserProviderImpl对象。然后在getUser方法中添加了一行日志,最终的操作还是交给IUserProvider实现类UserProviderImpl对象去完成。

要使用我们的代理,我们必须更新初始化代码:

public static void main(String[] args) {
        IUserProvider userProvider = new UserProviderImpl();
        LogProxy logProxy = new LogProxy(userProvider);
        logProxy.getUser(12);
    }

控制台输出

INFO-> 当前用户:fv,IP:10.60.131.54,工作目录:/Users/fv/Documents/workspace/fun/,系统编码格式:UTF-8,系统Mac OS X版本:10.16
INFO-> 获取用户12
INFO-> 我是原配实现类!

Process finished with exit code 0

手动代理创建的问题

这个解决方案有一个主要缺点:代理实现绑定到IUserProvider接口,因此很难重用。

代理逻辑通常很通用。代理的典型用例包括缓存、远程对象或延迟加载。

但是,代理需要实现特定的接口(及其方法),这与可重用性矛盾。

DK 动态代理

JDK提供了针对此问题的标准解决方案,称为Dynamic Proxies。使用动态代理,我们可以在运行时为特定接口创建实现。在此生成的代理上的方法调用被委派给InvocationHandler

下面是代理类的代码:

package com.fun.ztest.proxytest;

import com.fun.frame.SourceCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;

public class AutoLogProxy extends SourceCode implements InvocationHandler {

    private static Logger logger = LoggerFactory.getLogger(AutoLogProxy.class);

    private final Object drive;

    public AutoLogProxy(Object invocationTarget) {
        this.drive = invocationTarget;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        logger.warn("调用方法:{},参数:{}",method.getName(), Arrays.toString(args));
        return method.invoke(drive, args);
    }


}

下面是使用方法:

public static void main(String[] args) {
    IUserProvider userProvider = new UserProviderImpl();
    IUserProvider provider = (IUserProvider) Proxy.newProxyInstance(IUserProvider.class.getClassLoader(), new Class[]{IUserProvider.class}, new AutoLogProxy(userProvider));
    provider.getUser(10);
}

控制台输出

INFO-> 当前用户:fv,IP:10.60.131.54,工作目录:/Users/fv/Documents/workspace/fun/,系统编码格式:UTF-8,系统Mac OS X版本:10.16
WARN-> 调用方法:getUser,参数:[10]
INFO-> 我是原配实现类!

Process finished with exit code 0


使用Proxy.newProxyInstance,我们创建一个新的代理对象。此方法采用三个参数:

  • 应该使用的类加载器
  • 代理应实现的接口列表
  • 一个InvocationHandler接口实现

invoke方法有三个参数:

  • 调用了方法的代理对象
  • 被调用的方法
  • 已传递给调用方法的参数列表

代理创建(和接口实现)和代理逻辑(通过InvocationHandler)的分离支持可重用性。注意,我们在InvocationHandler实现中对IUserProvider接口没有任何依赖。在构造函数中,我们接受通用对象。这为我们提供了将InvocationHandler实现重用于不同接口的功能。

动态代理的局限性

动态代理始终需要一个接口。我们不能基于抽象类创建代理。

如果这确实是个大问题,那么可以查看字节码操作库cglibcglib能够通过子类化创建代理,因此能够在不需要接口的情况下为类创建代理。

cglib 实现动态代理

这里我创建了一个新的User对象,写了两个方法:

package com.fun.ztest.proxytest;

import com.fun.frame.SourceCode;

public class CUser extends SourceCode {

    public void fun() {
        output(DEFAULT_STRING);
    }

    public void tester() {
        output(DEFAULT_CHARSET.name());
    }

}

下面是代理的实现类:

package com.fun.ztest.proxytest;

import com.fun.frame.SourceCode;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.Arrays;

public class CUserProxy extends SourceCode implements MethodInterceptor {

    private static Logger logger = LoggerFactory.getLogger(CUserProxy.class);

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        logger.info("调用方法:{},参数:{}", method.getName(), Arrays.toString(args));
        proxy.invokeSuper(obj, args);
        return obj;
    }


}

下面是使用Demo

public static void main(String[] args) {
    CUserProxy proxy = new CUserProxy();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(CUser.class);
    enhancer.setCallback(proxy);
    CUser user = (CUser) enhancer.create();
    user.fun();
}

控制台输出


INFO-> 当前用户:fv,IP:10.60.131.54,工作目录:/Users/fv/Documents/workspace/fun/,系统编码格式:UTF-8,系统Mac OS X版本:10.16
INFO-> 调用方法:fun,参数:[]
INFO-> FunTester

Process finished with exit code 0

结论

代理模式可能非常强大,我们可以添加功能而无需修改实际的代码。代理通常用于向现有类中添加一些通用功能。使用动态代理,我们可以将代理创建与代理实现分开,具备重用性和灵活性多种优点。


FunTester,非著名测试开发,文章记录学习和感悟,欢迎关注,交流成长。

FunTester 热文精选

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册