全网最全解析!Spring与非Spring环境下获取动态代理对象的原始目标对象

本文详细解释了SpringBoot2.x中默认使用Cglib动态代理的原因,以及如何在SpringAOP和非Spring环境下获取动态代理对象的原始目标对象,包括使用工具类、Advised接口和Spring内置工具。同时介绍了JDK动态代理和Cglib的区别以及获取实例中的具体实现方法。

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


在这里插入图片描述

前言

在日常开发工作过程中,不知道是否有细心的小伙伴们注意到:

  • 为什么这个userSerivce代理对象里面又是一个CGLIB$CALLBACK_0变量???
  • 为什么这个userMapper代理对象里面是一个h变量???

image-20240401204016994

关于这个问题的话,我在之前的文章中其实已经讲到了,这里面涉及到了两种动态代理的原理,可以看我之前文章

动态代理系列文章:
深入探索JDK动态代理:从入门到原理的全面解析
探索Cglib:解析动态代理的神奇之处
全网最全解析!Spring与非Spring环境下获取动态代理对象的原始目标对象

今天我们不讨论这个问题,我们讨论另外一个问题,如何获取这两种动态代理对象里面的原始目标对象???而再细分的话,又可以分为在spring环境和spring环境,接下来我们一步步进行深度解析

在Spring AOP中获取动态代理对象的目标对象

前置知识—SpringBoot默认是JDK动态代理还是Cglib动态代理?

SpringBoot 2.x 版本分析

下面SpringBoot 版本为2.1.7

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

在 SpringBoot 中,通过AopAutoConfiguration来自动装配 AOP。搜索这个类,查看源码:

matchIfMissing=true属性:在配置文件中没有找到spring.aop.proxy-target-class这个属性,CglibAutoProxyConfiguration成了默认生效的配置

image-20240331230312832

默认情况下,是没有spring.aop.proxy-target-class这个配置项的。而此时,在 SpringBoot 2.x 中会默认使用 Cglib 来实现。如果需要修改 AOP 的实现,需要通过spring.aop.proxy-target-class这个配置项来修改。

#在application.properties文件中通过spring.aop.proxy-target-class来配置
spring.aop.proxy-target-class=false

这里提一下META-INF/spring-configuration-metadata.json文件的作用:在使用application.properties或application.yml文件时,IDEA 就是通过读取这些文件信息来提供代码提示的,SpringBoot 框架自己是不会来读取这个配置文件的。

Spring5 版本分析

SpringBoot 2.x内嵌Spring版本为Spring5.x:

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.1.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.1.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>

但是Spring5中默认还是使用jdk,查看官网,地址为:

https://docs.spring.io/spring/docs/5.2.4.RELEASE/spring-framework-reference/core.html#spring-core:
**翻译:Spring AOP 默认使用 JDK 动态代理,*如果对象没有实现接口,则使用 CGLIB 代理。*当然,也可以强制使用 CGLIB 代理。

在这里插入图片描述

SpringBoot 1.x 版本分析

下面SpringBoot 版本为1.5.16

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.16.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

搜索AopAutoConfiguration:在 SpringBoot 1.5.x 版本中,默认还是使用 JDK 动态代理的

matchIfMissing=true属性:在配置文件中没有找到spring.aop.proxy-target-class这个属性,JdkDynamicAutoProxyConfiguration成了默认生效的配置

image-20240331225832625

SpringBoot 2.x 为何默认使用 Cglib

假设,我们有一个UserServiceImpl和UserService类,此时需要在UserContoller中使用UserService。在 Spring 中通常都习惯这样写代码:

@Autowired
UserService userService;

在这种情况下,无论是使用 JDK 动态代理,还是 CGLIB 都不会出现问题。但是,如果代码是这样的:

@Autowired
UserServiceImpl userService;

这个时候,如果使用 JDK 动态代理,启动时就会报错,因为 JDK 动态代理是基于接口的,代理生成的对象只能赋值给接口变量。

而 CGLIB 就不存在这个问题。因为 CGLIB 是通过生成子类来实现的,代理对象无论是赋值给接口还是实现类,这两者都是代理对象的父类。

SpringBoot 正是出于这种考虑,于是在 2.x 版本中,将 AOP 默认实现改为了 CGLIB。

前置准备–工程准备

我这里就是一个常见的springboot工程,springboot是基于上面说的2.1.7版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

UserService和对应实现类UserServiceImpl,里面引入的UserMapper就是我们常见的mybatis Mapper类,就不多说了,这里主要说明我们的UserService是基于接口的

public interface UserService {
	User getUserById();
}

@Service
public class UserServiceImpl implements UserService {

	@Autowired
	UserMapper userMapper;

	@Transactional
	@Override
	public User getUserById(){
		return userMapper.getUserById("7");
    }

}

1、自己写工具类获取–利用反射

可能有很多小伙伴会疑问下面这里为什么JDK获取是getDeclaredField("h"),而Cglib动态代理为什么getDeclaredField("CGLIB$CALLBACK_0")
,这个可以看我之前的文章!

深入探索JDK动态代理:从入门到原理的全面解析!!!

探索Cglib:解析动态代理的神奇之处!!!

public class AopTargetUtils {

    /**
     * 获取 目标对象
     * @param proxy 代理对象
     * @return
     * @throws Exception
     */
    public static Object getTarget(Object proxy) throws Exception {
		//不是spring aop代理对象
        if(!AopUtils.isAopProxy(proxy)) {
            return proxy;
        }

        if(AopUtils.isJdkDynamicProxy(proxy)) {
            return getJdkDynamicProxyTargetObject(proxy);
        } else { //cglib
            return getCglibProxyTargetObject(proxy);
        }

    }


    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
        h.setAccessible(true);
        Object dynamicAdvisedInterceptor = h.get(proxy);

        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
        advised.setAccessible(true);

        Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();

        return target;
    }


    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
        h.setAccessible(true);
        AopProxy aopProxy = (AopProxy) h.get(proxy);

        Field advised = aopProxy.getClass().getDeclaredField("target");
        advised.setAccessible(true);

        Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();

        return target;
    }

}

我们这里userService是基于接口的,但这里是SpringBoot2.x,按照上面的分析,所以默认注入的是Cglib代理对象,下面采用AopTargetUtils取到了原始目标对象

image-20240331232141364

将spring.aop.proxy-target-class设置为false,就可以采用JDK动态代理,下面注入的UserSerivce就是JDK动态代理对象

#在application.properties文件中通过spring.aop.proxy-target-class来配置
spring.aop.proxy-target-class=false

image-20240331232746426

2、使用 Advised 接口

Spring 中的代理对象不管是JDK动态动代理,还是Cglib代理,都实现了 org.springframework.aop.framework.Advised 接口,该接口提供了一种获取原始目标对象的方法。示例如下:

image-20240331233242456

我们发现采用advised获取得到的对象和刚刚AopTargetUtils获取到的对象完全相等!

3、采用Spring自带的工具类获取—AopProxyUtils.getSingletonTarget

AopProxyUtils.getSingletonTarget获取原始目标对象的原理其实跟上面Advised 基本是一样的,看下面源代码就明白了!!!

public abstract class AopProxyUtils {
    public AopProxyUtils() {
    }

    @Nullable
    public static Object getSingletonTarget(Object candidate) {
    	//也是利用了Advised接口!
        if (candidate instanceof Advised) {
            TargetSource targetSource = ((Advised)candidate).getTargetSource();
            if (targetSource instanceof SingletonTargetSource) {
                return ((SingletonTargetSource)targetSource).getTarget();
            }
        }

        return null;
    }

我们发现AopProxyUtils获取得到的对象也是一样的

image-20240331233821372

非Spring AOP 中获取动态代理对象的原始目标对象

如果我们的代理对象不是Spring的AOP进行代理的,此时应该怎么获取呢,这里分两种JDK和Cglib

JDK动态代理

对于使用 JDK 动态代理创建的代理对象,可以通过 Proxy 类的 getInvocationHandler() 方法获取代理对象的 InvocationHandler,然后从 InvocationHandler 中获取原始目标对象。

代码准备

这里一个是接口IUserDao及其实现类UserDao,接下来我们要给这个UserDao进行JDK动态代理

public interface IUserDao {
    String save();
}

public class UserDao implements IUserDao {
    public String save() {
        System.out.println("--------jdk代理----已经保存数据!----");
        return "save";
    }
}

编写对应的InvocationHandler进行代理

public class MyInvocationHandler implements InvocationHandler {

	//原始目标对象
    private Object origBean;

    public MyInvocationHandler(Object origBean) {
        this.origBean = origBean;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("执行代理之前");
        Object res = method.invoke(origBean, args);
		System.out.println("执行代理之后");
		return res;
    }
}

测试验证

测试验证获取原始目标对象:

@SneakyThrows
	@Test
	public void testProxy() {
		IUserDao userDao=new UserDao();
		MyInvocationHandler myInvocationHandler = new MyInvocationHandler(userDao);
		//利用Proxy.newProxyInstance生成对应的代理对象
		IUserDao userDaoProxy = (IUserDao) Proxy.newProxyInstance(userDao.getClass()
				.getClassLoader(), userDao.getClass()
				.getInterfaces(), myInvocationHandler);

		//利用Proxy.getInvocationHandler获取InvocationHandler
		MyInvocationHandler proxyHandler = (MyInvocationHandler) Proxy.getInvocationHandler(userDaoProxy);

		//利用反射从MyInvocationHandler获取原始目标对象
		Field declaredField = MyInvocationHandler.class.getDeclaredField("origBean");
		declaredField.setAccessible(true);
		UserDao origBean = (UserDao) declaredField.get(proxyHandler);
		
		//代理对象进行调用
		userDaoProxy.save();
		//原始目标对象进行调用
		origBean.save();
	}

1、可以发现代理对象进行调用userDaoProxy.save(),打印出

执行代理之前
--------静态代理/jdk代理----已经保存数据!----
执行代理之后

2、原始目标对象进行调用origBean.save(),打印出,这里并不会进行代理拦截,说明我们确实取到了原始目标对象

--------静态代理/jdk代理----已经保存数据!----

举一反三-获取UserMapper里面的SqlSession

上面的例子中,有的小伙伴可能会说,这里为什么要那么麻烦,通过Proxy.getInvocationHandler获取InvocationHandler,上面不是已经有了嘛???
这个例子里面其实没问题,是可以直接用上面的,不过那是因为这个代理完全是我们自己定义的,但是如果是源码里面的呢??

MyInvocationHandler proxyHandler = (MyInvocationHandler) Proxy.getInvocationHandler(userDaoProxy);

比如我们需要获取下面这个controller里面注入的userMapper这个代理对象(小伙伴如果不清楚这个userMapper为什么是JDK动态代理对象的可以看我之前的文章!)的sqlSession变量???

image-20240401200644284

这个是里面是源码了,我们就不可能直接得到对应的InvocationHandler,这里方法跟上面差不多,我们举一反三,也是通过反射获取:
1、Proxy.getInvocationHandler获取MapperProxy,这个也是一个实现了InvocationHandler接口的类,里面就有我们的sqlSession
2、反射获取Field.get 获取到 MapperProxy 类中的 sqlSession 成员变量

public SqlSession getSqlSessionFromMapper(Object mapperProxy) {
        try {
            // 获取代理对象的InvocationHandler 实例
            MapperProxy<?> mapperProxyInstance = (MapperProxy<?>) Proxy.getInvocationHandler(mapperProxy);

            // 获取到 MapperProxy 类中的 sqlSession 成员变量
            Field sqlSessionField = MapperProxy.class.getDeclaredField("sqlSession");

			sqlSessionField.setAccessible(true);
            // 获取 sqlSession 成员变量的值
            SqlSession sqlSession = (SqlSession) sqlSessionField.get(mapperProxyInstance);

            return sqlSession; // 返回 SqlSession 对象

        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException("获取SqlSession失败", e);
        }
    }

image-20240401201201593

Cglib动态代理

代码准备

/**
 * 目标对象,没有实现任何接口
 */
public class UserDaoForCglib {

    public void save() {
        System.out.println("----cglib代理----已经保存数据!----");
    }

}

编写MethodInterceptor实现代理

/**
 * Cglib子类代理工厂
 * 对UserDao在内存中动态构建一个子类对象
 */
public class MyMethodInterceptor implements MethodInterceptor {
    //维护目标对象
    private Object origBean;

    public MyMethodInterceptor(Object origBean) {
        this.origBean = origBean;
    }

    //给目标对象创建一个代理对象
    public Object getProxyInstance(){
		System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\code");

		//1.工具类
        Enhancer en = new Enhancer();
        //2.设置父类
        en.setSuperclass(origBean.getClass());
        //3.设置回调函数
        en.setCallback(this);
        //4.创建子类(代理对象)
        return en.create();

    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		System.out.println("执行代理之前");
        //执行目标对象的方法
        Object returnValue = method.invoke(origBean, args);
		System.out.println("执行代理之后");
        return returnValue;
    }

}

测试验证

测试验证获取原始目标对象:

    @SneakyThrows
	@Test
    public void testCglib() {
        //目标对象
        UserDaoForCglib target = new UserDaoForCglib();

        //代理对象,他是继承UserDaoForCglib类的,所以可以直接用UserDaoForCglib接收
        UserDaoForCglib proxy = (UserDaoForCglib) new MyMethodInterceptor(target).getProxyInstance();

		//反射获取原始目标对象
		Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
		h.setAccessible(true);
		MyMethodInterceptor myMethodInterceptor = (MyMethodInterceptor) h.get(proxy);
		Field advised = myMethodInterceptor.getClass().getDeclaredField("origBean");
		advised.setAccessible(true);
		UserDaoForCglib origBean = (UserDaoForCglib) advised.get(myMethodInterceptor);

		//执行代理对象的方法
		proxy.save();
		//执行原始对象的方法
		origBean.save();

    }

}

1、可以发现代理对象进行调用proxy.save(),打印出

执行代理之前
----cglib代理----已经保存数据!----
执行代理之后

2、原始目标对象进行调用origBean.save(),打印出,这里并不会进行代理拦截,说明我们确实取到了原始目标对象

----cglib代理----已经保存数据!----

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值