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

文章目录

  • 前言
  • 在Spring AOP中获取动态代理对象的目标对象
    • 前置知识---SpringBoot默认是JDK动态代理还是Cglib动态代理?
      • SpringBoot 2.x 版本分析
      • Spring5 版本分析
      • SpringBoot 1.x 版本分析
      • SpringBoot 2.x 为何默认使用 Cglib
    • 前置准备--工程准备
    • 1、自己写工具类获取--利用反射
    • 2、使用 Advised 接口
    • 3、采用Spring自带的工具类获取---AopProxyUtils.getSingletonTarget
  • 非Spring AOP 中获取动态代理对象的原始目标对象
    • JDK动态代理
      • 代码准备
      • 测试验证
      • 举一反三-获取UserMapper里面的SqlSession
    • Cglib动态代理
      • 代码准备
      • 测试验证

在这里插入图片描述

前言

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

  • 为什么这个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动态代理

代码准备

这里一个是接口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代理----已经保存数据!----

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/507570.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

中国力量:NeurIPS报告折射中国人工智能研究的惊人崛起

会议之眼 快讯 近年来&#xff0c;人工智能技术的发展和应用在全球范围内引起了广泛关注。2024年3月27日&#xff0c;美国保尔森基金会旗下的麦克罗波洛智库&#xff08;MacroPolo&#xff09;发布的《全球人工智能人才追踪调查报告 2.0》为我们揭示了这一领域的一个重要趋势&…

大模型 智能体 智能玩具 智能音箱 构建教程 wukong-robot

视频演示 10:27 一、背景 继上文《ChatGPT+小爱音响能擦出什么火花?》可以看出大伙对AI+硬件的结合十分感兴趣,但上文是针对市场智能音响的AI植入,底层是通过轮询拦截,算是hack兼容,虽然官方有提供开发者接口,也免不了有许多局限性(比如得通过特定指令唤醒),不利于我…

vite vue3 import.meta.glob动态路由

在Vite中使用Vue 3&#xff0c;你可以使用import.meta.glob来导入目录下的多个Vue组件&#xff0c;并自动生成路由。以下是一个简单的例子&#xff1a; router/index.js // router/index.js import { createRouter, createWebHistory } from vue-router;// 自动导入views目录下…

【算法】01背包问题(代码+详解+练习题)

题目&#xff1a; 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。 输出最大价值。 输入格式 第一行两个整…

举个栗子!Tableau 技巧(268):折叠文本表的数据列

之前&#xff0c;我们分享过 &#x1f330; &#xff1a;灵活折叠文本表的多级数据行。陆续收到很多数据粉的反馈&#xff0c;想学习如何折叠文本表的数据列。 如下示例&#xff0c;假如将所有月份字段全部呈现出来&#xff0c;表格过长不利于查看数据。那么&#xff0c;我们就…

物联网网关和飞鸟物联平台如何助力其实现智能化升级,提升生产效率

随着工业4.0时代的到来&#xff0c;物联网技术逐渐成为推动工业转型升级的关键力量。物联网网关作为连接工业设备与网络的核心枢纽&#xff0c;在工业自动化、数据收集与分析等方面发挥着越来越重要的作用。本案例将围绕一家知名制造企业&#xff0c;展示物联网网关和飞鸟物联平…

职场人该如何学习使用AI大模型

【写在开篇&#xff1a;这是一篇针对非技术背景的职场人&#xff0c;学习和使用AI大模型的完全攻略。】 【今日份AI绘画&#xff1a;终身学习的职人】 非技术背景的职场人想要学习和使用AI大模型&#xff0c;可以遵循以下步骤&#xff1a; 1. 基础学习&#xff1a;首先&#…

Clickhouse-表引擎探索之MergeTree

引言 前文曾说过&#xff0c;Clickhouse是一个强大的数据库Clickhouse-一个潜力无限的大数据分析数据库系统 其中一个强大的点就在于支持各类表引擎以用于不同的业务场景。 MergeTree MergeTree系列的引擎被设计用于插入极大量的数据到一张表当中。数据可以以数据片段的形式一…

如何在Ubuntu系统部署Z-blog博客结合cpolar实现无公网IP访问本地网站

文章目录 1. 前言2. Z-blog网站搭建2.1 XAMPP环境设置2.2 Z-blog安装2.3 Z-blog网页测试2.4 Cpolar安装和注册 3. 本地网页发布3.1. Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 想要成为一个合格的技术宅或程序员&#xff0c;自己搭建网站制作网页是绕…

Huber Robust loss

Huber Loss&#xff08;Huber损失&#xff09;是一种用于回归任务的损失函数&#xff0c;它结合了均方误差&#xff08;MSE&#xff09;和绝对误差&#xff08;MAE&#xff09;的优点&#xff0c;在一定程度上抵抗了异常值的影响。Huber Loss 的数学表达式如下所示&#xff1a;…

【上海大学计算机组成原理实验报告】二、数据传送实验

一、实验目的 了解在模型机中算术、逻辑运算单元的控制方法。学习机器语言程序的运行过程。通过人工译码&#xff0c;加深对译码器基本工作原理的理解。 二、实验原理 根据实验指导书的相关内容&#xff0c;本次实验所要用的CP226实验仪在手动方式下&#xff0c;运算功能通过…

Matlab|配电网三相不平衡潮流计算【隐式Zbus高斯法】【可设定变压器数量、位置、绕组方式】

目录 主要内容 部分代码 结果一览 1.以33节点为例 2.以12节点系统为例 下载链接 主要内容 该模型基于隐式Zbus高斯法实现对配电网的三相不平衡潮流计算&#xff0c;通过选项可实现【不含变压器】和【含变压器】两种方式下的潮流计算&#xff0c;并且通过参数设置…

NLP技术大解析:人工智能应用从分词到情感分析的全面指南

自然语言处理&#xff0c;简称NLP&#xff0c;是人工智能领域中的一个重要分支&#xff0c;致力于让计算机理解和生成人类使用的自然语言。随着科技的飞速发展&#xff0c;NLP已经渗透到我们生活的方方面面&#xff0c;从智能语音助手到在线翻译工具&#xff0c;再到社交媒体的…

TikTok零播放?可能是海外代理IP的问题

在当今社交媒体的蓬勃发展中&#xff0c;TIKTOK作为一款备受欢迎的短视频平台&#xff0c;其直播功能也逐渐受到用户的青睐。然而&#xff0c;有时候跨境电商商家在进行直播时却面临着一个令人头疼的问题&#xff1a;没有观众。这时候&#xff0c;海外代理IP可能是一个潜在的原…

前端-深入探讨网络面试题

第一关 请求-文件、数据、连接 文件类的请求&#xff1a;加载HTMl、CSS 数据&#xff1a; ajax请求&#xff08;基于HTTP&#xff0c;HTTP基于TCP&#xff09;&#xff0c;如何建立连接的&#xff08;三次握手&#xff0c;为什么不是两次或者四次&#xff09;&#xff0c;sock…

C++ | Leetcode C++题解之第2题两数相加

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {map<int,int> a;//提供一对一的hashvector<int> b(2,-1);//用来承载结果&#xff0c;初始化一个大小为2&#xff0c;值为-1的容…

Qt 实现的万能采集库( 屏幕/相机/扬声器/麦克风采集)

【写在前面】 之前应公司需要&#xff0c;给公司写过一整套直播的库( 推拉流&#xff0c;编解码)&#xff0c;类似于 libobs。 结果后来因为没有相关项目&#xff0c;便停止开发&维护了。 不过里面很多有用的组件&#xff0c;然后也挺好用的&#xff0c;遂开源出来一部分。…

总结HTTPS加密流程

前言 本篇博客将介绍HTTPS加密的具体流程&#xff0c;坐好板凳发车啦~~ 一.HTTPS是什么 HTTPS也是一个应用层协议&#xff0c;是在HTTP协议的基础上引入了一个加密层 HTTP协议内容都是按照文本的方式明文传输的&#xff0c;这就导致在传输的过程中可能有一些内容被篡改。 …

leetcode 热题 100(部分)C/C++

leetcode 热题 100 双指针 盛最多水的容器 【mid】【双指针】 思路&#xff1a; 好久没写代码sb了&#xff0c;加上之前写的双指针并不多&#xff0c;以及有点思维定势了。我对双指针比较刻板的印象一直是两层for循环i&#xff0c;j&#xff0c;初始时i,j都位于左界附近&…