文章目录
- 前言
- 在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变量???
关于这个问题的话,我在之前的文章中其实已经讲到了,这里面涉及到了两种动态代理的原理,可以看我之前文章
动态代理系列文章: |
---|
深入探索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
成了默认生效的配置
默认情况下,是没有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
成了默认生效的配置
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取到了原始目标对象
将spring.aop.proxy-target-class设置为false,就可以采用JDK动态代理,下面注入的UserSerivce就是JDK动态代理对象
#在application.properties文件中通过spring.aop.proxy-target-class来配置
spring.aop.proxy-target-class=false
2、使用 Advised 接口
Spring 中的代理对象不管是JDK动态动代理,还是Cglib代理,都实现了 org.springframework.aop.framework.Advised
接口,该接口提供了一种获取原始目标对象的方法。示例如下:
我们发现采用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获取得到的对象也是一样的
非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变量???
这个是里面是源码了,我们就不可能直接得到对应的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);
}
}
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代理----已经保存数据!----