# Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

# 聊一下 SpringBoot 中使用的 cglib 作为动态代理中的一个注意点

Posted on 2021-09-19 In Java , SpringBoot Views: Views: Disqus:

这个话题是由一次组内同学分享引出来的，首先在 springboot 2.x 开始默认使用了 cglib 作为 aop 的实现，这里也稍微讲一下，在一个 1.x 的老项目里，可以看到AopAutoConfiguration 是这样的

    @Configuration
    @ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
    @ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    public class AopAutoConfiguration {

    	@Configuration
    	@EnableAspectJAutoProxy(proxyTargetClass = false)
    	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
    	public static class JdkDynamicAutoProxyConfiguration {
    	}

    	@Configuration
    	@EnableAspectJAutoProxy(proxyTargetClass = true)
    	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
    	public static class CglibAutoProxyConfiguration {
    	}

    }

而在 2.x 中变成了这样

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
    public class AopAutoConfiguration {

    	@Configuration(proxyBeanMethods = false)
    	@ConditionalOnClass(Advice.class)
    	static class AspectJAutoProxyingConfiguration {

    		@Configuration(proxyBeanMethods = false)
    		@EnableAspectJAutoProxy(proxyTargetClass = false)
    		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
    		static class JdkDynamicAutoProxyConfiguration {

    		}

    		@Configuration(proxyBeanMethods = false)
    		@EnableAspectJAutoProxy(proxyTargetClass = true)
    		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
    				matchIfMissing = true)
    		static class CglibAutoProxyConfiguration {

    		}

    	}

为何会加载 AopAutoConfiguration 在前面的文章聊聊 SpringBoot 自动装配里已经介绍过，有兴趣的可以看下，可以发现 springboot 在 2.x 版本开始使用 cglib 作为默认的动态代理实现。

然后就是出现的问题了，代码是这样的，一个简单的基于 springboot 的带有数据库的插入，对插入代码加了事务注解，

    @Mapper
    public interface StudentMapper {
    		// 就是插入一条数据
        @Insert("insert into student(name, age)" + "values ('nick', '18')")
        public Long insert();
    }

    @Component
    public class StudentManager {

        @Resource
        private StudentMapper studentMapper;

        public Long createStudent() {
            return studentMapper.insert();
        }
    }

    @Component
    public class StudentServiceImpl implements StudentService {

        @Resource
        private StudentManager studentManager;

        // 自己引用
        @Resource
        private StudentServiceImpl studentService;

        @Override
        @Transactional
        public Long createStudent() {
            Long id = studentManager.createStudent();
            Long id2 = studentService.createStudent2();
            return 1L;
        }

        @Transactional
        private Long createStudent2() {
    //        Integer t = Integer.valueOf("aaa");
            return studentManager.createStudent();
        }
    }

第一个公有方法 createStudent 首先调用了 manager 层的创建方法，然后再通过引入的 studentService 调用了createStudent2，我们先跑一下看看会出现啥情况，果不其然报错了，正是这个报错让我纠结了很久

报了个空指针，而且是在 createStudent2 已经被调用到了，在它的内部，报的 studentManager 是 null，首先 cglib 作为动态代理它是通过继承的方式来实现的，相当于是会在调用目标对象的代理方法时调用 cglib 生成的子类，具体的代理切面逻辑在子类实现，然后在调用目标对象的目标方法，但是继承的方式对于 final 和私有方法其实是没法进行代理的，因为没法继承，所以我最开始的想法是应该通过 studentService 调用 createStudent2 的时候就报错了，也就是不会进入这个方法内部，后面才发现犯了个特别二的错误，继承的方式去调用父类的私有方法，对于 Java 来说是可以调用到的，父类的私有方法并不由子类的InstanceKlass维护，只能通过子类的InstanceKlass找到Java类对应的_super，这样间接地访问。也就是说子类其实是可以访问的，那为啥访问了会报空指针呢，这里报的是studentManager 是空的，可以往依赖注入方面去想，如果忽略依赖注入，我这个studentManager 的确是 null，那是不是就没有被依赖注入呢，但是为啥前面那个可以呢

这个问题着实查了很久，不废话来看代码

    @Override
    		protected Object invokeJoinpoint() throws Throwable {
    			if (this.methodProxy != null) {
            // 这里的 target 就是被代理的 bean
    				return this.methodProxy.invoke(this.target, this.arguments);
    			}
    			else {
    				return super.invokeJoinpoint();
    			}
    		}

这个是`org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation`的代码，其实它在这里不是直接调用 super 也就是父类的方法，而是通过 methodProxy 调用 target 目标对象的方法，也就是原始的 studentService bean 的方法，这样子 spring 管理的已经做好依赖注入的 bean 就能正常起作用，否则就会出现上面的问题，因为 cglib 其实是通过继承来实现，通过将调用转移到子类上加入代理逻辑，我们在简单使用的时候会直接 invokeSuper() 调用父类的方法，但是在这里 spring 的场景里需要去支持 spring 的功能逻辑，所以上面的问题就可以开始来解释了，因为 createStudent 是公共方法，cglib 可以对其进行继承代理，但是在执行逻辑的时候其实是通过调用目标对象，也就是 spring 管理的被代理的目标对象的 bean 调用的 createStudent，而对于下面的 createStudent2 方法因为是私有方法，不会走代理逻辑，也就不会有调用回目标对象的逻辑，只是通过继承关系，在子类中没有这个方法，所以会通过子类的InstanceKlass找到这个类对应的_super，然后调用父类的这个私有方法，这里要搞清楚一个点，从这个代理类直接找到其父类然后调用这个私有方法，这个类是由 cglib 生成的，不是被 spring 管理起来经过依赖注入的 bean，所以是没有 studentManager 这个依赖的，也就出现了前面的问题

而在前面提到的cglib通过methodProxy调用到目标对象，目标对象是在什么时候设置的呢，其实是在bean的生命周期中，org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization这个接口的在bean的初始化过程中，会调用实现了这个接口的方法，

    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    	if (bean != null) {
    		Object cacheKey = getCacheKey(bean.getClass(), beanName);
    		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
    			return wrapIfNecessary(bean, beanName, cacheKey);
    		}
    	}
    	return bean;
    }

具体的逻辑在 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary这个方法里

    protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
    		if (StringUtils.hasLength(beanName)) {
    			return (FactoryBean.class.isAssignableFrom(beanClass) ?
    					BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
    		}
    		else {
    			return beanClass;
    		}
    	}

    	/**
    	 * Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
    	 * @param bean the raw bean instance
    	 * @param beanName the name of the bean
    	 * @param cacheKey the cache key for metadata access
    	 * @return a proxy wrapping the bean, or the raw bean instance as-is
    	 */
    	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
    			return bean;
    		}
    		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
    			return bean;
    		}
    		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
    			this.advisedBeans.put(cacheKey, Boolean.FALSE);
    			return bean;
    		}

    		// Create proxy if we have advice.
    		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    		if (specificInterceptors != DO_NOT_PROXY) {
    			this.advisedBeans.put(cacheKey, Boolean.TRUE);
    			Object proxy = createProxy(
    					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
    			this.proxyTypes.put(cacheKey, proxy.getClass());
    			return proxy;
    		}

    		this.advisedBeans.put(cacheKey, Boolean.FALSE);
    		return bean;
    	}

然后在 `org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#createProxy` 中创建了代理类

Related Posts

- 聊聊 SpringBoot 自动装配
- Filter, Interceptor, Aop, 啥, 啥, 啥? 这些都是啥?
- 聊聊 Java 中绕不开的 Synchronized 关键字-二
- Maven实用小技巧
- mybatis 的 $ 和 # 是有啥区别