Java 基础 - 动态代理

本贴最后更新于 1405 天前,其中的信息可能已经东海扬尘

Java 基础-动态代理

在切面编程(AOP)中,可以方便的对某些业务进行增强,比如添加日志。

在声明式事务中,可以方便的通过 @Transactional 注解开启事务支持。

在 Mybatis 中,我们没有创建 Mapper 接口的实现类,却可以直接注入 Mapper 并进行 CRUD。

而这些底层的实现,就是动态代理。

本篇博客包括以下内容:

  • 静态代理
  • jdk 动态代理
  • cglib 动态代理

一、静态代理

静态代理意味着我们需要在程序运行前创建代理类。

首先,创建 UserService 接口

public interface UserService {
    void addUser();
    void selectUser(String name);
}

然后创建 UserService 接口的实现类 UserServiceImpl

public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("添加用户...");
    }
    @Override
    public void selectUser(String name) {
        System.out.println("查询:" + name);
    }
}

假如静态代理需要给 UserService 接口的中的方法添加调用前后的日志记录,那么可以创建 UserServiceProxy

public class UserServiceProxy implements UserService {
    private UserService target;

    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    @Override
    public void addUser() {
        System.out.println("日志记录前...");
        target.addUser();
        System.out.println("日志记录后...");
    }

    @Override
    public void selectUser(String name) {
        System.out.println("日志记录前...");
        target.selectUser(name);
        System.out.println("日志记录后...");
    }
}

简单使用静态代理:

UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser();
proxy.selectUser("ccran");

控制台输出如下:

日志记录前...
添加用户...
日志记录后...
日志记录前...
查询:ccran
日志记录后...

至此,分析一下静态代理的缺点:

  1. 日志记录的代码存在大量冗余
  2. 如果需要代理多个接口,需要修改原代理类或创建新的代理类,难以维护和拓展。

二、jdk 动态代理

与静态代理通过 javac 生成.class 字节码文件,然后通过 ClassLoader 加载到 JVM 不同。

动态代理在程序运行时生成字节码文件,通过 Proxy 的 newProxyInstance 方法可以实现 jdk 动态代理。

首先,需要创建 InvocationHandler 接口的实现类 LogInvocationHandler,这个类规定了代理的业务实现,目前是日志记录。

public class LogInvocationHandler implements InvocationHandler {
    private UserService target;

    public LogInvocationHandler(UserService target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("日志记录前...");
        Object res = method.invoke(target, args);
        System.out.println("日志记录后...");
        return res;
    }
}

通过 Proxy 类的 newProxyInstance 方法可以创建动态代理对象

第一个参数需要传入类加载器,一般传入加载代理类的类加载器

第二个参数需要传入需要代理接口的 class 对象

第三个参数就是之前定义的 InvocationHandler 实现类

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)

创建与使用代码如下,输出结果与静态代理的使用相同。

    // jdk动态代理
    public static void dynamicProxyJDK() {
        UserService target = new UserServiceImpl();
        InvocationHandler invocationHandler = new LogInvocationHandler(target);
        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                invocationHandler);
        proxy.addUser();
        proxy.selectUser("ccran");
    }

通过 ProxyGenerator 的 generateProxyClass 方法将代理类的字节码保存到文件后反编译打开,动态代理的本质就很清晰了。

public final class DynamicUserServiceProxy extends Proxy implements UserService {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m0;
    private static Method m3;

    public DynamicUserServiceProxy(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void selectUser(String var1) throws  {
        try {
            super.h.invoke(this, m4, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void addUser() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.ccran.proxy.UserService").getMethod("selectUser", Class.forName("java.lang.String"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("com.ccran.proxy.UserService").getMethod("addUser");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

jdk 动态代理解读:

  1. 继承 Proxy 类(因此只能代理接口),并实现了 newProxyInstance 方法中第二个参数规定的代理接口。
  2. 通过 final 保证行为不可更改。
  3. 在静态代码块中通过反射拿到了代理接口的所有方法。
  4. 所有方法都是调用 super.h.invoke 方法,在 Proxy 父类中可以发现,super.h 正是 newProxyInstance 方法中第三个参数传入的 LogInvocationHandler 对象(代理的业务实现:日志记录)。
  5. 调用链:查询用户---DynamicUserServiceProxy(selectUser)---LogInvocationHandler(invoke)---(日志前)被代理对象的 selectUser(日志后)

优点:

解决静态代理的问题。

java 原生代理,无需依赖。

缺点:

只能代理接口。

三、cglib 动态代理

jdk 动态代理无法代理非接口方法,通过 cglib 动态代理可以解决,首先引入 cglib 依赖

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

移除 UserServiceImpl 实现的 UserService 接口,实现 cglib 动态代理

    // 动态代理
    public static void dynamicProxyCglib() {
        UserServiceImpl target = new UserServiceImpl();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserServiceImpl.class);
        // 设置enhancer的回调对象
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects,
                                    MethodProxy methodProxy) throws Throwable {
                System.out.println("日志记录前...");
                Object res = methodProxy.invokeSuper(o, objects);
                System.out.println("日志记录后...");
                return res;
            }
        });
        // 创建代理对象
        UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
        // 通过代理对象调用目标方法
        proxy.addUser();
        proxy.selectUser("ccran");
    }

我们猜测,cglib 可能创建出来的代理类如下:

public final class CglibProxy extends UserServiceImpl{
    private MethodInterceptor m;
    public final void addUser(){
        if(m!=null){
            m.intercept(xxx)
        }else{
            super.addUser();
        }
    }
}

四、实现

4.1 AOP 实现

AOP 的使用很简单,通过切面表达式定义切点(需要代理的方法,如 addUser),然后配合相关的注解定义增强的方法(代理的业务:如日志记录)即可。

故猜测实现如下(以 jdk 动态代理为例):

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object res = null;
        // 符合切面表达式,是需要代理的方法
        if(jointExp.match(method)){
            // 封装切点参数 
            JointPoint jointPoint = getJointPoint(proxy,method,args,xxx);
            // 前置通知
            beforeMethod.invoke(jointPoint);
            res = method.invoke(target, args);
        }else{
            res = method.invoke(target, args);
        }
        return res;
    }

4.2 声明式事务

对于某个添加了 @Transactional 注解的方法,如

@Transactional
public void insertInfo(){
    userService.insertUser();
    otherService.insertOther();
}

最终的代理对象方法可能实现如下:

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		conn.setAutoCommit(false);
        Object res = null;
        try{
            res = method.invoke(target, args);
        }catch (SQLException e) {
            // 回滚事务:
            conn.rollback();
        } finally {
            conn.setAutoCommit(true);
            conn.close();
        }
        return res;
    }

4.3 Mapper

在 Mybatis 中,对用户 CRUD 的 Mapper 可能如下:

public interface UserMapper {
    @Select("SELECT id,name,age FROM user where id= #{id}")
    User selectById(int id);
}

最终注入到 UserService 中的 UserMapper 实现可能如下

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object res = null;
		if(method.isAnnotationPresent(Select.class)){
           // 获取sql
           Select selectAnnotation = method.getAnnotation(Select.class);
           String query = generateQuery(selectAnnotation.value());
            // 执行sql
            ResultSet resultSet = conn.excuteQuery(sql);
            // 封装成对象返回
            res = generate(resultSet);
        }else if(method.isAnnotationPresent(Update.class)){
            xxx
        }
        xxx
        return res;
    }

五、总结

  1. 静态代理比较简单直观,但是会使编写的代码冗余,并且代理多个类时,难以维护和拓展。
  2. jdk 动态代理本质是实现了代理类实现的接口,因此无法代理非接口方法,但是原生无依赖。
  3. cglib 动态代理可以代理非接口方法,代码相比于 jdk 动态代理可读性更强。
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3165 引用 • 8206 回帖 • 1 关注
  • 动态代理
    6 引用 • 10 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...