面试官提问:什么是动态代理?

fatherlaw
发布于 2022-9-7 15:16
浏览
0收藏

一、介绍
何谓代理?

代理这个词最早出现在代理商这个行业,所谓代理商,简而言之,其实就是帮助企业或者老板打理生意,自己本身不做生产任何商品。

举个例子,我们去火车站买票的时候,人少老板一个人还忙的过来,但是人一多的话,就会非常拥挤,于是就有了各种代售点,我们可以从代售点买车票,从而加快老板的卖票速度。

代售点的出现,可以说,很直观的帮助老板提升了用户购票体验。

站在软件设计的角度,其实效果也是一样的,采用代理模式的编程,能显著的增强原有的功能和简化方法调用方式。

面试官提问:什么是动态代理?-鸿蒙开发者社区
在介绍动态代理之前,我们先来聊解静态代理。

二、静态代理
下面,我们以两数相加为例,实现过程如下!

•  接口类

public interface Calculator {

    /**
     * 计算两个数之和
     * @param num1
     * @param num2
     * @return
     */
    Integer add(Integer num1, Integer num2);
}

•  目标对象

public class CalculatorImpl implements Calculator {


    @Override
    public Integer add(Integer num1, Integer num2) {
        Integer result = num1 + num2;
        return result;
    }
}

•  代理对象

public class CalculatorProxyImpl implements Calculator {


    private Calculator calculator;


    @Override
    public Integer add(Integer num1, Integer num2) {
        //方法调用前,可以添加其他功能....
        Integer result = calculator.add(num1, num2);
        //方法调用后,可以添加其他功能....
        return result;
    }


    public CalculatorProxyImpl(Calculator calculator) {
        this.calculator = calculator;
    }
}

•  测试类

public class CalculatorProxyClient {

    public static void main(String[] args) {
        //目标对象
        Calculator target = new CalculatorImpl();
        //代理对象
        Calculator proxy = new CalculatorProxyImpl(target);
        Integer result = proxy.add(1,2);
        System.out.println("相加结果:" + result);
    }
}

•  输出结果

相加结果:3

通过这种代理方式,最大的优点就是:可以在不修改目标对象的前提下,扩展目标对象的功能。

但也有缺点:需要代理对象和目标对象实现一样的接口,因此,当目标对象扩展新的功能时,代理对象也要跟着一起扩展,不易维护!

三、动态代理
动态代理,其实本质也是为了解决上面当目标对象扩展新功能时,代理对象也需要跟着一起扩展的痛点问题而生。

那它是怎么解决的呢?

以 JDK 为例,当需要给某个目标对象添加代理处理的时候,JDK 会在内存中动态的构建代理对象,从而实现对目标对象的代理功能。

下面,我们还是以两数相加为例,介绍具体的玩法!

3.1、JDK 中生成代理对象的玩法
•  创建接口

public interface JdkCalculator {

    /**
     * 计算两个数之和
     * @param num1
     * @param num2
     * @return
     */
    Integer add(Integer num1, Integer num2);
}

•  目标对象

public class JdkCalculatorImpl implements JdkCalculator {

    @Override
    public Integer add(Integer num1, Integer num2) {
        Integer result = num1 + num2;
        return result;
    }
}

•  动态代理对象

public class JdkProxyFactory {

    /**
     * 维护一个目标对象
     */
    private Object target;

    public JdkProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance(){
        Object proxyClassObj = Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler(){

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("方法调用前,可以添加其他功能....");

                        // 执行目标对象方法
                        Object returnValue = method.invoke(target, args);
                        System.out.println("方法调用后,可以添加其他功能....");
                        return returnValue;
                    }
                });
        return proxyClassObj;
    }
}

•  测试类

public class TestJdkProxy {

    public static void main(String[] args) {
        //目标对象
        JdkCalculator target = new JdkCalculatorImpl();
        System.out.println(target.getClass());
        //代理对象
        JdkCalculator proxyClassObj = (JdkCalculator) new JdkProxyFactory(target).getProxyInstance();
        System.out.println(proxyClassObj.getClass());
        //执行代理方法
        Integer result = proxyClassObj.add(1,2);
        System.out.println("相加结果:" + result);
    }
}

•  输出结果

class com.example.java.proxy.jdk1.JdkCalculatorImpl
class com.sun.proxy.$Proxy0
方法调用前,可以添加其他功能....
方法调用后,可以添加其他功能....
相加结果:3

采用 JDK 技术动态创建interface实例的步骤如下:

1. 首先定义一个 InvocationHandler 实例,它负责实现接口的方法调用
2. 通过 Proxy.newProxyInstance() 创建 interface 实例,它需要 3 个参数:
   (1)使用的 ClassLoader,通常就是接口类的 ClassLoader
   (2)需要实现的接口数组,至少需要传入一个接口进去;
   (3)用来处理接口方法调用的 InvocationHandler 实例。
3. 将返回的 Object 强制转型为接口

动态代理实际上是 JVM 在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法技术,把上面的动态代理改写为静态实现类大概长这样:

public class JdkCalculatorDynamicProxy implements JdkCalculator {

    private InvocationHandler handler;

    public JdkCalculatorDynamicProxy(InvocationHandler handler) {
        this.handler = handler;
    }

    public void add(Integer num1, Integer num2) {
        handler.invoke(
           this,
           JdkCalculator.class.getMethod("add", Integer.class, Integer.class),
           new Object[] { num1, num2 });
    }
}

本质就是 JVM 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码)。

3.2、cglib 生成代理对象的玩法
除了 jdk 能实现动态的创建代理对象以外,还有一个非常有名的第三方框架:cglib,它也可以做到运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

cglib 特点如下:

•  cglib 不仅可以代理接口还可以代理类,而 JDK 的动态代理只能代理接口
•  cglib 是一个强大的高性能的代码生成包,它广泛的被许多 AOP 的框架使用,例如我们所熟知的 Spring AOP,cglib 为他们提供方法的 interception(拦截)。
•  CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类,速度非常快。
在使用 cglib 之前,我们需要添加依赖包,如果你已经有spring-core的jar包,则无需引入,因为spring中包含了cglib。

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

下面,我们还是以两数相加为例,介绍具体的玩法!

•  创建接口

public interface CglibCalculator {

    /**
     * 计算两个数之和
     * @param num1
     * @param num2
     * @return
     */
    Integer add(Integer num1, Integer num2);
}

•  目标对象

public class CglibCalculatorImpl implements CglibCalculator {


    @Override
    public Integer add(Integer num1, Integer num2) {
        Integer result = num1 + num2;
        return result;
    }
}


•  动态代理对象

public class CglibProxyFactory implements MethodInterceptor {

    /**
     * 维护一个目标对象
     */
    private Object target;

    public CglibProxyFactory(Object target) {
        this.target = target;
    }

    /**
     * 为目标对象生成代理对象
     * @return
     */
    public Object getProxyInstance() {
        //工具类
        Enhancer en = new Enhancer();
        //设置父类
        en.setSuperclass(target.getClass());
        //设置回调函数
        en.setCallback(this);
        //创建子类对象代理
        return en.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("方法调用前,可以添加其他功能....");

        // 执行目标对象方法
        Object returnValue = method.invoke(target, args);
        System.out.println("方法调用后,可以添加其他功能....");
        return returnValue;
    }
}

•  测试类

public class TestCglibProxy {

    public static void main(String[] args) {
        //目标对象
        CglibCalculator target = new CglibCalculatorImpl();
        System.out.println(target.getClass());
        //代理对象
        CglibCalculator proxyClassObj = (CglibCalculator) new CglibProxyFactory(target).getProxyInstance();
        System.out.println(proxyClassObj.getClass());
        //执行代理方法
        Integer result = proxyClassObj.add(1,2);
        System.out.println("相加结果:" + result);
    }
}

•  输出结果

class com.example.java.proxy.cglib1.CglibCalculatorImpl
class com.example.java.proxy.cglib1.CglibCalculatorImpl$$EnhancerByCGLIB$$3ceadfe4
方法调用前,可以添加其他功能....
方法调用后,可以添加其他功能....
相加结果:3

将 cglib 生成的代理类改写为静态实现类大概长这样:

public class CglibCalculatorImplByCGLIB extends CglibCalculatorImpl implements Factory {
    

    private static final MethodInterceptor methodInterceptor;

    private static final Method method;
        

    public final Integer add(Integer var1, Integer var2) {
        return methodInterceptor.intercept(this, method, new Object[]{var1, var2}, methodProxy);
    }

    //....
}

其中,拦截思路与 JDK 类似,都是通过一个接口方法进行拦截处理!

在上文中咱们还介绍到了,cglib 不仅可以代理接口还可以代理类,下面我们试试代理类。

•  创建新的目标对象

public class CglibCalculatorClass {

    /**
     * 计算两个数之和
     * @param num1
     * @param num2
     * @return
     */
    public Integer add(Integer num1, Integer num2) {
        Integer result = num1 + num2;
        return result;
    }
}

•  测试类

public class TestCglibProxyClass {

    public static void main(String[] args) {
        //目标对象
        CglibCalculatorClass target = new CglibCalculatorClass();
        System.out.println(target.getClass());
        //代理对象
        CglibCalculatorClass proxyClassObj = (CglibCalculatorClass) new CglibProxyFactory(target).getProxyInstance();
        System.out.println(proxyClassObj.getClass());
        //执行代理方法
        Integer result = proxyClassObj.add(1,2);
        System.out.println("相加结果:" + result);
    }
}

•  输出结果

class com.example.java.proxy.cglib1.CglibCalculatorClass
class com.example.java.proxy.cglib1.CglibCalculatorClass$$EnhancerByCGLIB$$e68ff36c
方法调用前,可以添加其他功能....
方法调用后,可以添加其他功能....
相加结果:3

四、静态织入
在上文中,我们介绍的代理方案都是在代码运行时动态的生成class文件达到动态代理的目的。

回到问题的本质,其实动态代理的技术目的,主要为了解决静态代理模式中当目标接口发生了扩展,代理类也要跟着一遍变动的问题,避免造成了工作伤的繁琐和复杂。

在 Java 生态里面,还有一个非常有名的第三方代理框架,那就是AspectJ,AspectJ通过特定的编译器可以将目标类编译成class字节码的时候,在方法周围加上业务逻辑,从而达到静态代理的效果。

采用AspectJ进行方法植入,主要有四种:

•  方法调用前拦截
•  方法调用后拦截
•  调用方法结束拦截
•  抛出异常拦截
使用起来也非常简单,首先是在项目中添加AspectJ编译器插件。

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.5</version>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <source>1.6</source>
        <target>1.6</target>
        <encoding>UTF-8</encoding>
        <complianceLevel>1.6</complianceLevel>
        <verbose>true</verbose>
        <showWeaveInfo>true</showWeaveInfo>
    </configuration>
</plugin>

然后,编写一个方法,准备进行代理。

@RequestMapping({"/hello"})
public String hello(String name) {
    String result = "Hello World";
    System.out.println(result);
    return result;
}

编写代理配置类

@Aspect
public class ControllerAspect {

    /***
     * 定义切入点
     */
    @Pointcut("execution(* com.example.demo.web..*.*(..))")
    public void methodAspect(){}

    /**
     * 方法调用前拦截
     */
    @Before("methodAspect()")
    public void before(){
        System.out.println("代理 -> 调用方法执行之前......");
    }

    /**
     * 方法调用后拦截
     */
    @After("methodAspect()")
    public void after(){
        System.out.println("代理 -> 调用方法执行之后......");
    }

    /**
     * 调用方法结束拦截
     */
    @AfterReturning("methodAspect()")
    public void afterReturning(){
        System.out.println("代理 -> 调用方法结束之后......");
    }

    /**
     * 抛出异常拦截
     */
    @AfterThrowing("methodAspect()")
    public void afterThrowing() {
        System.out.println("代理 -> 调用方法异常......");
    }
}

编译后,hello方法会变成这样。

@RequestMapping({"/hello"})
public String hello(Integer name) throws SQLException {
    JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this, name);

    Object var7;
    try {
        Object var5;
        try {
            //调用before
            Aspectj.aspectOf().doBeforeTask2(var2);
            String result = "Hello World";
            System.out.println(result);
            var5 = result;
        } catch (Throwable var8) {
            Aspectj.aspectOf().after(var2);
            throw var8;
        }
        //调用after
        Aspectj.aspectOf().after(var2);
        var7 = var5;
    } catch (Throwable var9) {
        //调用抛出异常
        Aspectj.aspectOf().afterthrowing(var2);
        throw var9;
    }
    //调用return
    Aspectj.aspectOf().afterRutuen(var2);
    return (String)var7;
}

很显然,代码被AspectJ编译器修改了,AspectJ并不是动态的在运行时生成代理类,而是在编译的时候就植入代码到class文件。

由于是静态织入的,所以性能相对来说比较好!

五、小结
看到上面的介绍静态织入方案,跟我们现在使用Spring AOP的方法极其相似,可能有的同学会发出疑问,我们现在使用的Spring AOP动态代理,到底是动态生成的还是静态织入的呢?

实际上,Spring AOP代理是对JDK代理和CGLIB代理做了一层封装,同时引入了AspectJ中的一些注解@pointCut、@after,@before等等,本质是使用的动态代理技术。

总结起来就三点:

•  如果目标是接口的话,默认使用 JDK 的动态代理技术;
•  如果目标是类的话,使用 cglib 的动态代理技术;
•  引入了AspectJ中的一些注解@pointCut、@after,@before,主要是为了简化使用,跟AspectJ的关系并不大;
那为什么Spring AOP不使用AspectJ这种静态织入方案呢?

虽然AspectJ编译器非常强,性能非常高,但是只要目标类发生了修改就需要重新编译,主要原因可能还是AspectJ的编译器太过于复杂,还不如动态代理来的省心!

六、参考
1、Java三种代理模式:静态代理、动态代理和cglib代理

2、Java 动态代理作用是什么?

 

本文转载自公共号Java极客技术。

分类
标签
已于2022-9-7 15:16:32修改
收藏
回复
举报
回复
    相关推荐