从生活场景、业务场景剖析代理模式(JDK、CGLIB、spring)

本文详细介绍了代理模式的概念、应用场景及分类,包括静态代理和动态代理,并通过明星与经纪人关系的类比,深入浅出地讲解了JDK动态代理与CGLib动态代理的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代理模式应用场景

代理模式就是一层代理,日常生活中的中介、经纪人,代码中的事务代理、非侵入式日志等都是代理。
代理模式就是代理对象持有被代理对象的引用,在代理类中访问代理对象的时候,在方法前后做一些处理逻辑。
比如经纪人持有明星的访问权,记者要采访明星,经纪人会对记者的访问做一些筛选、档期判断等操作,之后符合条件的话才会进入到访问明星的阶段。
代理模式属于结构型模式。

代理模式与之后的装饰模式结构有相似的地方,处理逻辑的重心不同,不必太拘泥于两者的区别

代理模式的分类

代理模式总体上分为两种,静态代理和动态代理,可以理解为静态代理是一对一的代理,动态代理是利用反射等技术实现的一对多的通用代理,一个是手写的代理类,一个是动态生成的代理类

静态代理

静态代理理解起来比较简单,用两个例子来说明下

明星-经纪人之静态代理

这是个比较通俗的例子

package com.luhui.pattern.proxy;

/**
 * 通用接口,以备后患
 */
public interface Person {
}
package com.luhui.pattern.proxy.staticproxy;
import com.luhui.pattern.proxy.Person;

/**
 * 明星1号
 */
public class Star1 implements Person{

    public void interviewed(){
        System.out.println("明星1号接受记者采访");
    }

}
package com.luhui.pattern.proxy.staticproxy;
import com.luhui.pattern.proxy.Person;

/**
 * 经纪人类
 */
public class Agent implements Person {
    private Star1 star1;

    public Agent(Star1 star1){
        this.star1 = star1;
    }

    public void interviewed(){
        before();
        this.star1.interviewed();
        after();
    }

    private void before(){
        System.out.println("查看明星档期");
    }

    private void after(){
        System.out.println("与记者沟通采访结果");
    }
}
package com.luhui.pattern.proxy.staticproxy;

/**
 * 测试类
 */
public class AgentProxyTest {
    public static void main(String[] args) {
        Agent agent = new Agent(new Star1());
        agent.interviewed();
    }

这里的经纪人就是明星1号的一个代理类,因为这个代理类是我们手写的,所以是静态代理。

分库分表中的业务问题

这个比较接近生产中的场景了,比如我们面对与日俱增的订单量,需要对产生的数据根据年份进行分库分表,不同年份的订单放到不同的数据库中。这个时候就需要有个代理类来对订单进行判断,然后再路由到对应年份的数据据库中进行操作(与此类似的还有日常微服务开发中的开发者模式等)。

package com.luhui.pattern.proxy.dbroute;

import lombok.Getter;
import lombok.Setter;

/**
 * 订单类
 */
@Getter
@Setter
public class Order {

    /**
     * 订单ID
     */
    private String orderId;
    
    /**
     * 订单信息
     */
    private Object orderInfo;

    /**
     * 创建时间(一般而言都是必备字段)
     */
    private Long createTime;
    
}
package com.luhui.pattern.proxy.dbroute;

/**
 * 模拟订单操作的持久层
 */
public class OrderDao {
    public int insert(Order order){
        System.out.println("OrderDao创建Order成功!");
        return 1;
    }
}

package com.luhui.pattern.proxy.dbroute;

public interface IOrderService {
    int createOrder(Order order);
}

package com.luhui.pattern.proxy.dbroute;

public class OrderService implements IOrderService {
    private OrderDao orderDao;

    public OrderService(){
        orderDao = new OrderDao();
    }

    public int createOrder(Order order) {
        System.out.println("OrderService调用orderDao创建订单");
        return orderDao.insert(order);
    }
}

如上所述,一个实体类-持久层-业务接口-业务层的简单架子,这个时候再加上分库分表的代理逻辑

package com.luhui.pattern.proxy.dbroute.proxy;

import com.luhui.pattern.proxy.dbroute.IOrderService;
import com.luhui.pattern.proxy.dbroute.Order;
import com.luhui.pattern.proxy.dbroute.db.DynamicDataSourceEntity;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 分库分表的静态代理
 */
public class OrderServiceStaticProxy implements IOrderService {
    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");

    /**
     * 代理类持有被代理类引用
     */
    private IOrderService orderService;
    public OrderServiceStaticProxy(IOrderService orderService) {
        this.orderService = orderService;
    }

    /**
     * 代理前置工作
     */
    private void before(Order order){
        Long time = order.getCreateTime();
        Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
        System.out.println("静态代理类自动分配到【DB_" +  dbRouter + "】数据源处理数据" );
        // 模拟设置数据源
        DynamicDataSourceEntity.set(dbRouter);
    }

    /**
     * 代理后置工作
     */
    private void after(){
        // 模拟重置数据源
        DynamicDataSourceEntity.restore();
    }

    public int createOrder(Order order) {
        before(order);
        this.orderService.createOrder(order);
        after();
        return 0;
    }
}

以下是个利用ThreadLocal模拟数据源的实体类,如果只是单纯的了解代理模式,不想涉及的太多,可以忽略这个类。如果想再多了解的,可以看我之前的另一个博客
链接: 关于ThreadLocal的使用心得.

package com.luhui.pattern.proxy.dbroute.db;

/**
 * 这是一个模拟数据源设置的实体类
 */
public class DynamicDataSourceEntity {

    public final static String DEFAULE_SOURCE = null;

    private final static ThreadLocal<String> local = new ThreadLocal<String>();

    private DynamicDataSourceEntity(){}


    public static String get(){return  local.get();}

    public static void restore(){
         local.set(DEFAULE_SOURCE);
    }

    //DB_2019
    //DB_2020
    public static void set(String source){local.set(source);}

    public static void set(int year){local.set("DB_" + year);}

}

如上所述就是一个贴近日常业务场景的一个代理模式的例子。
综合两个例子来看,静态代理就是我们手写一个类作为代理类,代理类持有被代理类的引用,在我们使用被代理类的时候,先访问代理类(访问的时候初始化一个被代理类实例),通过代理类的访问控制逻辑后(比如经纪人的档期判断,订单创建时间的判断),再来访问代理类的逻辑(访问明星,创建订单),之后可以根据实际情况选择是否增加对应的后置逻辑(比如流的关闭,ThreadLocal的remove等一些资源的释放)

动态代理

我们已经知道了两者的区别,那么话题紧接上面的静态代理例子。

明星-经纪人之静态代理缺点

众所周知一名强大的经纪人往往是服务于多个明星的,如果是面对这样的场景,我们要按照上面静态代理的思路来实现的话,就需要写很多个经纪人代理类了。

/**
 * 经纪人类-服务于明星1号
 */
public class Agent implements Person {
    private Star1 star1;

    public Agent(Star1 star1){
        this.star1 = star1;
    }

    public void interviewed(){
        before();
        this.star1.interviewed();
        after();
    }

    private void before(){
        System.out.println("查看明星1档期");
    }

    private void after(){
        System.out.println("与记者沟通采访结果");
    }
}
/**
 * 经纪人类-服务于明星2号
 */
public class Agent implements Person {
    private Star2 star2;

    public Agent(Star2 star2){
        this.star2 = star2;
    }

    public void interviewed(){
        before();
        this.star2.interviewed();
        after();
    }

    private void before(){
        System.out.println("查看明星1档期");
    }

    private void after(){
        System.out.println("与记者沟通采访结果");
    }
}
/**
 * 经纪人类-服务于明星3号
 */
public class Agent implements Person {
    private Star2 star2;
    ......

诸如此类,如果数量繁多的话,我们要写很多代理类,写很多重复代码,显然不符合我们使用设计模式的初衷。这时候就需要动态代理

明星-经纪人之JDK动态代理

package com.luhui.pattern.proxy.dynamicproxy.jdkproxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 使用JDK动态代理实现的经纪人类(注意,这个不是真正的代理类)
 */
public class JDKAgent implements InvocationHandler {

    private Object target;

    /**
     * 获取一个代理类
     *
     * @param target 为了实现动态,这里肯定不能传具体的代理类,所以是Object
     * @author ngls.luhui 2020-12-22 15:03
     * @return java.lang.Object
     */
    public Object getInstance(Object target) throws Exception{
        // 1.0 获得被代理类的实例对象并存储
        this.target = target;
        // 2.0 获得被代理类的字节码对象
        Class<?> clazz = target.getClass();
        // 3.0 根据被代理类的字节码对象生成并返回代理类
        return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }

    /**
     * 实现InvocationHandler接口所必须实现的方法,用来写代理的逻辑
     *
     * @author ngls.luhui 2020-12-22 15:06
     * @return java.lang.Object
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object obj = method.invoke(this.target,args);
        after();
        return obj;
    }
    
    private void before(){
        System.out.println("查看明星档期");
    }

    private void after(){
        System.out.println("与记者沟通采访结果");
    }
}
package com.luhui.pattern.proxy.dynamicproxy.jdkproxy;

import com.luhui.pattern.proxy.Person;
import sun.misc.ProxyGenerator;

import java.io.FileOutputStream;
import java.lang.reflect.Method;

public class JDKProxyTest {

    public static void main(String[] args) {
        try {
            // 访问代理类被代理的方法
            Object obj = new JDKAgent().getInstance(new Star1());
            Method method = obj.getClass().getMethod("interviewed",null);
            method.invoke(obj);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

这个时候会发现getMethod这一步报错了,不急。
我们先debug看一下
在这里插入图片描述
可以发现,我们获得的代理类对象是一个很奇怪的$Proxy0类型,这个其实是一个动态生成的代理类。
我们可以把这个class从内存中输出到磁盘里看一看,加上如下代码

public class JDKProxyTest {
    public static void main(String[] args) {
        try {

            Person obj = (Person)new JDKAgent().getInstance(new Star1());
            // Method method = obj.getClass().getMethod("interviewed",null);
            // method.invoke(obj);

            //用于打印出 $Proxy0的class文件
            byte [] bytes = ProxyGenerator.generateProxyClass("$Proxy0",new Class[]{Person.class});
            FileOutputStream os = new FileOutputStream("E://$Proxy0.class");
            os.write(bytes);
            os.close();

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

执行以后找到如下文件,进行反编译(反编译的手段多样,可使用jad,也可使用idea自带的反编译插件)

在这里插入图片描述
反编译打开后,发现生成的代理类中并没有interviewed方法,所以执行以下这行代码必然会出错

Method method = obj.getClass().getMethod("interviewed",null);

在这里插入图片描述
原因其实是,jdk动态代理是根据代理类和被代理类的统一接口而实现的,只有接口中有的方法,才会生成在代理类中。所以在Person接口中增加interviewed()方法后,执行成功。
在编写JDKAgent的时候其实也有体现
在这里插入图片描述

在这里插入图片描述
再次反编译动态生成的代理类,interviewed方法已经有了
在这里插入图片描述
所以通过这个bug,也能体会到jdk动态代理的最大特点了。

明星-经纪人之CGLib动态代理

上述例子中,为了解决jdk代理带来的bug,我们给People接口增加了interviewed方法。
但是,从逻辑上讲,是有点牵强的,这样的话所有继承People接口的类都要实现interviewed方法,但不是每个人都要有被采访的方法的。
所以,我们来看另一种代理方法-cglib的动态代理

package com.luhui.pattern.proxy.dynamicproxy.cglibproxy;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGlibAgent implements MethodInterceptor {


    public Object getInstance(Class<?> clazz) throws Exception{
        //相当于Proxy,代理的工具类
        Enhancer enhancer = new Enhancer();
        //要把哪个设置为即将生成的新类父类
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

	/**
     * 我们调用的所有的原始方法都会被替代为调用该方法
     * 我们可以在该方法中使用反射的形式来调用原始方法(比较费时)
     * 或者使用MethodProxy,使用MethodProxy性能更好
     * @param o: 代理类对象
     * @param method : 原始方法对象
     * @param objects : 方法参数
     * @param proxy : MethodProxy,该参数涉及到Cglib的FastClass机制
     */
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object obj = methodProxy.invokeSuper(o,objects);
        after();
        return obj;
    }

    private void before(){
        System.out.println("查看明星档期");
    }

    private void after(){
        System.out.println("与记者沟通采访结果");
    }
}
package com.luhui.pattern.proxy.dynamicproxy.cglibproxy;

/**
 * 明星1号,不需要继承接口了
 */
public class Star1{

    public void interviewed(){
        System.out.println("明星1号接受记者采访");
    }

}
package com.luhui.pattern.proxy.dynamicproxy.cglibproxy;

import net.sf.cglib.core.DebuggingClassWriter;

public class CglibTest {
    public static void main(String[] args) {

        try {
        	// 用来打印生成的代理类信息
            //System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E://cglib_proxy_classes");

            Star1 star1 = (Star1) new CGlibAgent().getInstance(Star1.class);
            star1.interviewed();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果如下
在这里插入图片描述
可以看到,这次的Star1并没有实现任何接口,所以可以看出cglib的代理是不依赖接口的。而且从下面的setSuperclass可以猜测一下,cglib的实现原理是生成一个被代理类的子类来进行代理的。

在这里插入图片描述
接下来反编译一下生成的代理类,通过调试或者打印类信息我们可以得到,这个就是生成的代理类信息

在这里插入图片描述
首先第一个关键点,确实是通过继承来实现的,而且生成的代理类,继承了被代理类Star1的全部方法。
在这里插入图片描述
我们看下interviewed方法
在这里插入图片描述
可以看下,其实其他的equals和toString等方法也是一样的,所以我们在CGlibAgent中重写的intercept方法相当于给所有方法加了个截面
在这里插入图片描述
如果要仅对interviewed方法生效,可以通过method参数对before和after的调用进行控制。
关于cglib的原理,其实使用了fastClass的机制,上面三个class文件中

在这里插入图片描述
这里的FastCalss简单来说就是:为代理类和被代理类各生成一个 Class,这个 Class 会为代 理类或被代理类的方法分配一个 index(int 类型)。这个 index 当做一个入参,FastClass 就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比 JDK 动态代理通过反射调用高。
在这里插入图片描述

CGLib 和 JDK 动态代理对比

  1. JDK 动态代理是实现了被代理对象的接口,CGLib 是继承了被代理对象。
  2. JDK 和 CGLib 都是在运行期生成字节码,JDK 是直接写 Class 字节码,CGLib 使用 ASM 框架写 Class 字节码
  3. JDK Proxy生成代理的逻辑简单,执行效率相对要低,每次都要反射动态调用,CGLib 生成代理逻辑更复杂,调用效率更高,生成一个包含了所有的逻辑的FastClass,不再需要反射调用。

代理模式与Spring

  1. 当 Bean 有实现接口时,Spring 就会用 JDK 的动态代理
  2. 当 Bean 没有实现接口时,Spring 选择 CGLib
  3. Spring 可以通过配置强制使用 CGLib,只需在 Spring 的配置文件中加入如下代码
<aop:aspectj-autoproxy proxy-target-class="true"/>

静态代理和动态的本质区别

  1. 静态代理只能通过手动完成代理操作,如果被代理类增加新的方法,代理类需要同步新增,违背开闭原则。
  2. 动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。
  3. 若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成, 无需修改代理类的代码。

代理模式的优缺点

优点

  1. 代理模式能将代理对象与真实被调用的目标对象分离。
  2. 一定程度上降低了系统的耦合度,扩展性好。
  3. 可以起到保护目标对象的作用。
  4. 可以对目标对象的功能增强。

缺点

  1. 代理模式会造成系统设计中类的数量增加。
  2. 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。
  3. 增加了系统的复杂度。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

unhappy404

感谢老板打赏!!!爱您

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值