聊一聊Spring中的代理

代理的概念本文就不细说了,相信网上的文章很多,动态代理,静态代理,JDKProxy和CGLIB,他们的使用区别等。我们今天就重点聊一聊CGLIB,找一个具体的场景聊聊

最近项目使用了SpringBoot的缓存,玩着玩着我就觉得这个@Cacheable真是牛逼,一个注解,就能向redis发送存取缓存指令,完成了我们要写千万次的get/set代码。

比如如下的代码:

@EnableCaching
@SpringBootApplication
public class MinimalApplication {

    @Autowired private CacheService cacheService;

    public static void main(String[] args)  {
        ApplicationContext context = SpringApplication.run(MinimalApplication.class, args);
        MinimalApplication app = context.getBean(MinimalApplication.class);

        String cache = app.cacheService.getStr("hi");
        System.out.println(cache);
    }
}
@Service
public class CacheService {
    @Cacheable(cacheNames="XA", key="#hi")
    public String getStr(String hi) {
        return "Aloha, 大猪小猪在菜盘!";
    }
}

这里做了太多的事情,寻找缓存Map, 建立redis连接,读写操作 and so on...

其实这个场景下就用了CGLIB代理,代理加强的代码帮我们做了太多的事情。我们首先来调试下这段代码,来看看调用栈的信息:


debug-stack.png

信息量非常巨大,至少你能够看到的getStr()方法跟真正执行getStr()方法中间垮了12层调用栈。。。
你也能看到一些很奇怪的信息,比如第二行就是这样的:

invoke:-1, CacheService$$FastClassBySpringCGLIB$$e3b31d3f 

我擦了,这是什么鬼畜代码?我咋不记得我有写过这火星文玩意儿的类?
其实这这个类就是CGLIB帮我们生成的类,这个类在我们的编译器中是看不到的,我们需要做一些特殊的处理,来查看这生成的类,在main方法中添加这一段代码,再次运行debug:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"~/Desktop/cglib");

完毕之后去这个目录下查看生成的文件,我们发现就有这么一个火星文明明的class:

CacheService$$EnhancerBySpringCGLIB$$4872f987.class

打开这个class瞅瞅,代码已经完全不可读了:

public class CacheService$$EnhancerBySpringCGLIB$$4872f987 extends CacheService implements SpringProxy, Advised, Factory {
    private boolean CGLIB$BOUND;
    public static Object CGLIB$FACTORY_DATA;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private MethodInterceptor CGLIB$CALLBACK_1;
    private NoOp CGLIB$CALLBACK_2;
    private Dispatcher CGLIB$CALLBACK_3;
    private Dispatcher CGLIB$CALLBACK_4;
    private MethodInterceptor CGLIB$CALLBACK_5;
    private MethodInterceptor CGLIB$CALLBACK_6;
    private static Object CGLIB$CALLBACK_FILTER;
    private static final Method CGLIB$getStr$0$Method;
    private static final MethodProxy CGLIB$getStr$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$equals$1$Method;
    private static final MethodProxy CGLIB$equals$1$Proxy;
    private static final Method CGLIB$toString$2$Method;
    private static final MethodProxy CGLIB$toString$2$Proxy;
    private static final Method CGLIB$hashCode$3$Method;
    private static final MethodProxy CGLIB$hashCode$3$Proxy;
    private static final Method CGLIB$clone$4$Method;
    private static final MethodProxy CGLIB$clone$4$Proxy;

    static void CGLIB$STATICHOOK103() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class var0 = Class.forName("zsh.demos.simple.minimal.serv.CacheService$$EnhancerBySpringCGLIB$$4872f987");
        Class var1;
        CGLIB$getStr$0$Method = ReflectUtils.findMethods(new String[]{"getStr", "(Ljava/lang/String;)Ljava/lang/String;"}, (var1 = Class.forName("zsh.demos.simple.minimal.serv.CacheService")).getDeclaredMethods())[0];
        CGLIB$getStr$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)Ljava/lang/String;", "getStr", "CGLIB$getStr$0");
        Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
        CGLIB$equals$1$Method = var10000[0];
        CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
        CGLIB$toString$2$Method = var10000[1];
        CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
        CGLIB$hashCode$3$Method = var10000[2];
        CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
        CGLIB$clone$4$Method = var10000[3];
        CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
    }
...

这个class其实就是CGLIB生成的代理增强类,这是整个也是Spring AOP的基础,我们终于见到了庐山真面目了。

光看了这个class文件也看不出什么东西,我们还是要关心这个getStr()方法加了@Cacheable之后到底Spring做了哪些事情,还是再次回到我们调用栈来:

在ideallij开发IDE中,点击CGLIB增强的class的栈都直接会回到原类本身,所以可以暂时忽略这一层,第8层是lambda表达式解释层也可以忽略,我们就看中间的AOP层做了些什么事情:

11. invoke:204, MethodProxy (org.springframework.cglib.proxy)
10. invokeJoinpoint:746, CglibAopProxy$CglibMethodInvocation (org.springframework.aop.framework)
9.  proceed:163, ReflectiveMethodInvocation (org.springframework.aop.framework)
8.  lambda$invoke$0:53, CacheInterceptor (org.springframework.cache.interceptor)
7.  invoke:-1, 718057154 (org.springframework.cache.interceptor.CacheInterceptor$$Lambda$464)
6.  invokeOperation:337, CacheAspectSupport (org.springframework.cache.interceptor)
5.  execute:392, CacheAspectSupport (org.springframework.cache.interceptor)
4.  execute:317, CacheAspectSupport (org.springframework.cache.interceptor)
3.  invoke:61, CacheInterceptor (org.springframework.cache.interceptor)
2.  proceed:185, ReflectiveMethodInvocation (org.springframework.aop.framework)
1.  intercept:688, CglibAopProxy$DynamicAdvisedInterceptor (org.springframework.aop.framework)

第一层,最难理解的可能就是第一层了,什么?我getStr()方法为什么会跳到这个DynamicAdvisedInterceptor类里面的intercept()方法里执行?其实这一步我们需要拿出CGLIB之前生成的class来看,我们找到getStr()方法:

public final String getStr(String var1) {
    try {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (this.CGLIB$CALLBACK_0 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }
        return var10000 != null ? (String)var10000.intercept(this, CGLIB$getStr$0$Method, new Object[]{var1}, CGLIB$getStr$0$Proxy) : super.getStr(var1);
    } catch (Error | RuntimeException var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

真正被实例化的CacheService对象其实是由这个CGLIB生成的class生成的。如果明白这一点,那么我们便很好理解,上面代码中的var10000其实就是MethodInterceptor的实现类DynamicAdvisedInterceptor

第2层到第11层暂略

那么我们再看一下,为什么我们的getStr()方法会被增强代理?这里先做个测试,如果将getStr()方法的@Cacheable注解去掉,我们看看调用栈的变化:

@Service
public class CacheService {
//    @Cacheable(cacheNames="XA", key="#hi")
    public String getStr(String hi) {
        return "Aloha, 大猪小猪在菜盘!";
    }
}

调用栈如下,没有任何的CGLIB代理类出现


debug-stack-noproxy.png

这一点至少能说明,引入了@Cacheable注解会让SpringBoot对他进行代理增强,处理变得复杂。程序员是有理想,有文化,有道德的,不继续深入怎么行?我们就看看@Cacheable注解到底是怎么告诉SpringBoot去做代理增强的

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
    boolean proxyTargetClass() default false;
    AdviceMode mode() default AdviceMode.PROXY;
    int order() default Ordered.LOWEST_PRECEDENCE;
}

这个注解其实包含了很丰富的信息,我们其实从注解的attribute中已经能够看出一些端倪,这里面有proxy,有AdviceMode,如果你熟悉了AOP的概念,你就会知道Advice(通知)的具体作用。同时这个注解中有@Import(CachingConfigurationSelector.class)我们进入这个导入类查看

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

    private final Log logger = LogFactory.getLog(getClass());

    /**
     * Register, escalate, and configure the standard auto proxy creator (APC) against the
     * given registry. Works by finding the nearest annotation declared on the importing
     * {@code @Configuration} class that has both {@code mode} and {@code proxyTargetClass}
     * attributes. If {@code mode} is set to {@code PROXY}, the APC is registered; if
     * {@code proxyTargetClass} is set to {@code true}, then the APC is forced to use
     * subclass (CGLIB) proxying.
     * <p>Several {@code @Enable*} annotations expose both {@code mode} and
     * {@code proxyTargetClass} attributes. It is important to note that most of these
     * capabilities end up sharing a {@linkplain AopConfigUtils#AUTO_PROXY_CREATOR_BEAN_NAME
     * single APC}. For this reason, this implementation doesn't "care" exactly which
     * annotation it finds -- as long as it exposes the right {@code mode} and
     * {@code proxyTargetClass} attributes, the APC can be registered and configured all
     * the same.
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        boolean candidateFound = false;
        Set<String> annoTypes = importingClassMetadata.getAnnotationTypes();
        for (String annoType : annoTypes) {
            AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
            if (candidate == null) {
                continue;
            }
            Object mode = candidate.get("mode");
            Object proxyTargetClass = candidate.get("proxyTargetClass");
            if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&
                    Boolean.class == proxyTargetClass.getClass()) {
                candidateFound = true;
                if (mode == AdviceMode.PROXY) {
                    AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                    if ((Boolean) proxyTargetClass) {
                        AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                        return;
                    }
                }
            }
        }
        if (!candidateFound) {
            String name = getClass().getSimpleName();
            logger.warn(String.format("%s was imported but no annotations were found " +
                    "having both 'mode' and 'proxyTargetClass' attributes of type " +
                    "AdviceMode and boolean respectively. This means that auto proxy " +
                    "creator registration and configuration may not have occurred as " +
                    "intended, and components may not be proxied as expected. Check to " +
                    "ensure that %s has been @Import'ed on the same class where these " +
                    "annotations are declared; otherwise remove the import of %s " +
                    "altogether.", name, name, name));
        }
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249