Spring Aop之Cglib實現原理詳解
Spring Aop實現對目標物件的代理,AOP的兩種實現方式:Jdk代理和Cglib代理。這兩種代理的區別在於,Jdk代理與目標類都會實現同一個介面,並且在代理類中會呼叫目標類中被代理的方法,呼叫者實際呼叫的則是代理類的方法,通過這種方式我們就可以在代理類中織入切面邏輯;Jdk代理存在的問題在於目標類被代理的方法必須實現某個介面,Cglib代理則是為了解決這個問題而存在的,其實現代理的方式是通過為目標類動態生成一個子類,通過在子類中織入相應邏輯來達到織入代理邏輯的目的。
關於Jdk代理和Cglib代理,其優缺點主要在於:
- Jdk代理生成的代理類只有一個,因而其編譯速度是非常快的;而由於被代理的目標類是動態傳入代理類中的,Jdk代理的執行效率相對來說低一點,這也是Jdk代理被稱為動態代理的原因;
- Cglib代理需要為每個目標類生成相應的子類,因而在實際執行過程中,其可能會生成非常多的子類,過多的子類始終不是太好的,因為這影響了虛擬機器編譯類的效率;但由於在呼叫過程中,代理類的方法是已經靜態編譯生成了的,因而Cglib代理的執行效率相對來說高一些。
本文主要講解Spring Aop是如何通過Cglib代理實現將切面邏輯織入目標類的。
1. AopProxy織入物件生成
前面我們講過,Spring Aop織入切面邏輯的入口方法是AbstractAutoProxyCreator.createProxy()
方法,如下是該方法的原始碼:
protected Object createProxy(Class<?> beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { // 如果當前beanFactory實現了ConfigurableListableBeanFactory介面,則將需要被代理的 // 物件暴露出來 if (this.beanFactory instanceof ConfigurableListableBeanFactory) { AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); } // 建立代理工廠 ProxyFactory proxyFactory = new ProxyFactory(); // 複製proxyTargetClass,exposeProxy等屬性 proxyFactory.copyFrom(this); // 如果當前設定了不使用Cglib代理目標類,則判斷目標類是否設定了preserveTargetClass屬性, // 如果設定了,則還是強制使用Cglib代理目標類;如果沒有設定,則判斷目標類是否實現了相關介面, // 沒有設定,則還是使用Cglib代理。需要注意的是Spring預設使用的是Jdk代理來織入切面邏輯。 if (!proxyFactory.isProxyTargetClass()) { // 判斷目標類是否設定了preserveTargetClass屬性 if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { // 判斷目標類是否實現了相關介面 evaluateProxyInterfaces(beanClass, proxyFactory); } } // 將需要織入的切面邏輯都轉換為Advisor物件 Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); // 提供的hook方法,供子類實現以實現對代理工廠的定製 customizeProxyFactory(proxyFactory); proxyFactory.setFrozen(this.freezeProxy); // 當前判斷邏輯預設返回false,子類可進行重寫,對於AnnotationAwareAspectJAutoProxyCreator, // 其重寫了該方法返回true,因為其已經對獲取到的Advisor進行了過濾,後面不需要在對目標類進行重新 // 匹配了 if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } // 生成代理類 return proxyFactory.getProxy(getProxyClassLoader()); }
可以看到,在生成代理類之前,主要做了兩件事:①判斷使用Jdk代理還是Cglib代理;②設定相關的屬性。這裡我們繼續看最後的ProxyFactory.getProxy()
方法:
public Object getProxy(@Nullable ClassLoader classLoader) { // 首先獲取AopProxy物件,其主要有兩個實現:JdkDynamicAopProxy和ObjenesisCglibAopProxy, // 分別用於Jdk和Cglib代理類的生成,其getProxy()方法則用於獲取具體的代理物件 return createAopProxy().getProxy(classLoader); }
上面的createAopProxy()
方法可以理解為一個工廠方法,返回值是一個AopProxy型別的物件,其內部根據具體的條件生成相應的子類物件,即JdkDynamicAopProxy和ObjenesisCglibAopProxy。後面則通過呼叫AopProxy.getProxy()
方法獲取代理過的物件。如下是createAopProxy()
方法的實現邏輯:
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 判斷當前類是否需要進行執行時優化,或者是指定了使用Cglib代理的方式,再或者是目標類沒有使用者提供的
// 相關介面,則使用Cglib代理實現代理邏輯的織入
if (config.isOptimize() || config.isProxyTargetClass() ||
hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: "
+ "Either an interface or a target is required for proxy creation.");
}
// 如果被代理的類是一個介面,或者被代理的類是使用Jdk代理生成的類,此時還是使用Jdk代理
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 返回Cglib代理織入類物件
return new ObjenesisCglibAopProxy(config);
} else {
// 返回Jdk代理織入類物件
return new JdkDynamicAopProxy(config);
}
}
這裡可以看到,本文需要講解的Cglib代理邏輯的織入就在ObjenesisCglibAopProxy.getProxy()
方法中。
2. 代理邏輯的織入
關於代理邏輯的織入,其實現主體還是通過Enhancer
來實現,即通過需要織入的Advisor列表,生成Callback物件,並將其設定到Enhancer
物件中,最後通過Enhancer
生成目標物件。如下是AopProxy.getProxy()
方法的原始碼:
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating CGLIB proxy: target source is "
+ this.advised.getTargetSource());
}
try {
Class<?> rootClass = this.advised.getTargetClass();
Assert.state(rootClass != null,
"Target class must be available for creating a CGLIB proxy");
// 判斷當前類是否是已經通過Cglib代理生成的類,如果是的,則獲取其原始父類,
// 並將其介面設定到需要代理的介面中
Class<?> proxySuperClass = rootClass;
if (ClassUtils.isCglibProxyClass(rootClass)) {
// 獲取父類
proxySuperClass = rootClass.getSuperclass();
// 獲取父類實現的介面,並將其設定到需要代理的介面中
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
for (Class<?> additionalInterface : additionalInterfaces) {
this.advised.addInterface(additionalInterface);
}
}
// 對目標類進行檢查,主要檢查點有三個:
// 1. 目標方法不能使用final修飾;
// 2. 目標方法不能是private型別的;
// 3. 目標方法不能是包訪問許可權的;
// 這三個點滿足任何一個,當前方法就不能被代理,此時該方法就會被略過
validateClassIfNecessary(proxySuperClass, classLoader);
// 建立Enhancer物件,並且設定ClassLoader
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
// 這裡AopProxyUtils.completeProxiedInterfaces()方法的主要目的是為要生成的代理類
// 增加SpringProxy,Advised,DecoratingProxy三個需要實現的介面。這裡三個介面的作用如下:
// 1. SpringProxy:是一個空介面,用於標記當前生成的代理類是Spring生成的代理類;
// 2. Advised:Spring生成代理類所使用的屬性都儲存在該介面中,
// 包括Advisor,Advice和其他相關屬性;
// 3. DecoratingProxy:該介面用於獲取當前代理物件所代理的目標物件的Class型別。
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new
ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));
// 獲取當前需要織入到代理類中的邏輯
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length];
for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
}
// 設定代理類中各個方法將要使用的切面邏輯,這裡ProxyCallbackFilter.accept()方法返回
// 的整型值正好一一對應上面Callback陣列中各個切面邏輯的下標,也就是說這裡的CallbackFilter
// 的作用正好指定了代理類中各個方法將要使用Callback陣列中的哪個或哪幾個切面邏輯
enhancer.setCallbackFilter(new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap,
this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);
// 生成代理物件
return createProxyClassAndInstance(enhancer, callbacks);
} catch (CodeGenerationException | IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of class ["
+ this.advised.getTargetClass() + "]: Common causes of this problem "
+ "include using a final class or a non-visible class", ex);
} catch (Throwable ex) {
throw new AopConfigException("Unexpected AOP exception", ex);
}
}
可以看到,這裡的AopProxy.getProxy()
方法就是生成代理物件的主幹邏輯。上面的邏輯中主要有兩個部分需要重點講解:①如果獲取Callback陣列;②CallbackFilter的作用。關於第一點,我們後面會進行重點講解,至於第二點,這裡我們需要理解的就是CallbackFilter.accept()方法接收一個Method型別的引數,該引數也即當前要生成的代理邏輯的方法,這裡的accept()方法將返回目標當前要織入代理邏輯的方法所需要使用的切面邏輯,也即Callback物件在Callback陣列中的下標。關於CallbackFilter的使用原理,讀者可以閱讀實戰CGLib系列之proxy篇(二):回撥過濾CallbackFilter這篇文章。下面我們繼續閱讀getCallbacks()
的原始碼:
private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
boolean exposeProxy = this.advised.isExposeProxy();
boolean isFrozen = this.advised.isFrozen();
boolean isStatic = this.advised.getTargetSource().isStatic();
// 使用者自定義的代理邏輯的主要織入類
Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
Callback targetInterceptor;
// 判斷如果要暴露代理物件,如果是,則使用AopContext設定將代理物件設定到ThreadLocal中
// 使用者則可以通過AopContext獲取目標物件
if (exposeProxy) {
// 判斷被代理的物件是否是靜態的,如果是靜態的,則將目標物件快取起來,每次都使用該物件即可,
// 如果目標物件是動態的,則在DynamicUnadvisedExposedInterceptor中每次都生成一個新的
// 目標物件,以織入後面的代理邏輯
targetInterceptor = isStatic ?
new StaticUnadvisedExposedInterceptor(
this.advised.getTargetSource().getTarget()) :
new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource());
} else {
// 下面兩個類與上面兩個的唯一區別就在於是否使用AopContext暴露生成的代理物件
targetInterceptor = isStatic ?
new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) :
new DynamicUnadvisedInterceptor(this.advised.getTargetSource());
}
// 當前Callback用於一般的不用背代理的方法,這些方法
Callback targetDispatcher = isStatic ?
new StaticDispatcher(this.advised.getTargetSource().getTarget())
: new SerializableNoOp();
// 將獲取到的callback組裝為一個數組
Callback[] mainCallbacks = new Callback[] {
aopInterceptor, // 使用者自己定義的攔截器
targetInterceptor, // 根據條件是否暴露代理物件的攔截器
new SerializableNoOp(), // 不做任何操作的攔截器
targetDispatcher, this.advisedDispatcher, // 用於儲存Advised物件的分發器
new EqualsInterceptor(this.advised), // 針對equals方法呼叫的攔截器
new HashCodeInterceptor(this.advised) // 針對hashcode方法呼叫的攔截器
};
Callback[] callbacks;
// 如果目標物件是靜態的,也即可以快取的,並且切面邏輯的呼叫鏈是固定的,
// 則對目標物件和整個呼叫鏈進行快取
if (isStatic && isFrozen) {
Method[] methods = rootClass.getMethods();
Callback[] fixedCallbacks = new Callback[methods.length];
this.fixedInterceptorMap = new HashMap<>(methods.length);
for (int x = 0; x < methods.length; x++) {
// 獲取目標物件的切面邏輯
List<Object> chain =
this.advised.getInterceptorsAndDynamicInterceptionAdvice(
methods[x], rootClass);
fixedCallbacks[x] = new FixedChainStaticTargetInterceptor(
chain, this.advised.getTargetSource().getTarget(),
this.advised.getTargetClass());
// 對呼叫鏈進行快取
this.fixedInterceptorMap.put(methods[x].toString(), x);
}
// 將生成的靜態呼叫鏈存入Callback陣列中
callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length];
System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length);
System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length,
fixedCallbacks.length);
// 這裡fixedInterceptorOffset記錄了當前靜態的呼叫鏈的切面邏輯的起始位置,
// 這裡記錄的用處在於後面使用CallbackFilter的時候,如果發現是靜態的呼叫鏈,
// 則直接通過該引數獲取相應的呼叫鏈,而直接略過了前面的動態呼叫鏈
this.fixedInterceptorOffset = mainCallbacks.length;
} else {
callbacks = mainCallbacks;
}
return callbacks;
}
這裡的getCallbacks()方法主要做了三件事:①獲取目標物件的動態呼叫鏈;②判斷是否設定了exposeProxy屬性,如果設定了,則生成一個可以暴露代理物件的Callback物件,否則生成一個不做任何處理直接呼叫目標物件的Callback物件;③判斷目標物件是否是靜態的,並且當前的切面邏輯是否是固定的,如果是,則將目標物件和呼叫鏈進行快取,以便後續直接呼叫。這裡需要說明的一個點在於第三點,因為在判斷目標物件為靜態物件,並且呼叫鏈是固定的時候,會將目標物件和呼叫鏈進行快取,並且封裝到指定的Callback物件中。這裡讀者可能會疑問為什麼動態呼叫鏈和靜態呼叫鏈都進行了快取,這和前面講解的CallbackFilter是息息相關的,因為上述程式碼最後使用fixedInterceptorOffset記錄了當前靜態呼叫鏈在陣列中儲存的位置,我們前面也講了,Enhancer可以通過CallbackFilter返回的整數值來動態的指定從當前物件Callback陣列中的第幾個環繞邏輯開始織入,這裡就會使用到fixedInterceptorOffset。從上述程式碼中可以看出,使用者自定義的呼叫鏈是在DynamicAdvisedInterceptor中生成的(關於靜態呼叫鏈的生成實際上是同樣的邏輯,只不過靜態呼叫鏈會被快取),這裡我們看看DynamicAdvisedInterceptor的實現原始碼:
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
// 通過TargetSource獲取目標物件
TargetSource targetSource = this.advised.getTargetSource();
try {
// 判斷如果需要暴露代理物件,則將當前代理物件設定到ThreadLocal中
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
// 獲取目標物件切面邏輯的環繞鏈
List<Object> chain = this.advised
.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
// 對引數進行處理,以使其與目標方法的引數型別一致,尤其對於陣列型別,
// 會單獨處理其資料型別與實際型別一致
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
// 因為沒有切面邏輯需要織入,這裡直接呼叫目標方法
retVal = methodProxy.invoke(target, argsToUse);
} else {
// 通過生成的呼叫鏈,對目標方法進行環繞呼叫
retVal = new CglibMethodInvocation(proxy, target, method,
args, targetClass, chain, methodProxy).proceed();
}
// 對返回值進行處理,如果返回值就是當前目標物件,那麼將代理生成的代理物件返回;
// 如果返回值為空,並且返回值型別是非void的基本資料型別,則丟擲異常;
// 如果上述兩個條件都不符合,則直接將生成的返回值返回
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
} finally {
// 如果目標物件不是靜態的,則呼叫TargetSource.releaseTarget()方法釋放目標物件
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
// 將代理物件設定為前面(外層邏輯)呼叫設定的物件,以防止暴露出來的代理物件不一致
if (setProxyContext) {
AopContext.setCurrentProxy(oldProxy);
}
}
}
這裡intercept()方法裡主要邏輯有兩點:①為目標物件生成切面邏輯呼叫鏈;②通過切面邏輯對目標物件進行環繞,並且進行呼叫。關於這兩點,我們都會進行講解,這裡我們首先看看Cglib是如何生成呼叫鏈的,如下是getInterceptorsAndDynamicInterceptionAdvice()
方法最終呼叫的原始碼,中間略過了部分比較簡單的呼叫:
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class<?> targetClass) {
List<Object> interceptorList = new ArrayList<>(config.getAdvisors().length);
Class<?> actualClass = (targetClass != null ?
targetClass : method.getDeclaringClass());
// 判斷切面邏輯中是否有IntroductionAdvisor型別的Advisor
boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
for (Advisor advisor : config.getAdvisors()) {
if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
// 這裡判斷切面邏輯的呼叫鏈是否提前進行過過濾,如果進行過,則不再進行目標方法的匹配,
// 如果沒有,則再進行一次匹配。這裡我們使用的AnnotationAwareAspectJAutoProxyCreator
// 在生成切面邏輯的時候就已經進行了過濾,因而這裡返回的是true,本文最開始也對這裡進行了講解
if (config.isPreFiltered() || pointcutAdvisor.getPointcut()
.getClassFilter().matches(actualClass)) {
// 將Advisor物件轉換為MethodInterceptor陣列
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
// 這裡進行匹配的時候,首先會檢查是否為IntroductionAwareMethodMatcher型別的
// Matcher,如果是,則呼叫其定義的matches()方法進行匹配,如果不是,則直接呼叫
// 當前切面的matches()方法進行匹配。這裡由於前面進行匹配時可能存在部分在靜態匹配時
// 無法確認的方法匹配結果,因而這裡呼叫是必要的,而對於能夠確認的匹配邏輯,這裡呼叫
// 也是非常迅速的,因為前面已經對匹配結果進行了快取
if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) {
// 判斷如果是動態匹配,則使用InterceptorAndDynamicMethodMatcher對其進行封裝
if (mm.isRuntime()) {
for (MethodInterceptor interceptor : interceptors) {
interceptorList.add(
new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
} else {
// 如果是靜態匹配,則直接將呼叫鏈返回
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
} else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
// 判斷如果為IntroductionAdvisor型別的Advisor,則將呼叫鏈封裝為Interceptor陣列
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
} else {
// 這裡是提供的使用自定義的轉換器對Advisor進行轉換的邏輯,因為getInterceptors()方法中
// 會使用相應的Adapter對目標Advisor進行匹配,如果能匹配上,通過其getInterceptor()方法
// 將自定義的Advice轉換為MethodInterceptor物件
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
return interceptorList;
}
這裡獲取呼叫鏈的邏輯其實比較簡單,其最終的目的就是將Advisor陣列一個一個的封裝為Interceptor物件。在進行Advisor封裝的時候,這裡分為了三種類型:
- 如果目標切面邏輯是一般的切面邏輯,即PointcutAdvisor,則會在執行時對目標方法進行動態匹配,因為前面可能存在還不能確認的是否應該應用切面邏輯的方法;
- 如果切面邏輯是IntroductionAdvisor的,則將其封裝為Interceptor型別的陣列;
- 如果以上兩個都不是,說明切面邏輯可能是使用者自定義的切面邏輯,這裡就通過註冊的AdvisorAdapter進行匹配,如果某個Adapter能夠支援當前Advisor的轉換,則呼叫其getInterceptor()方法將Advisor轉換為MethodInterceptor返回。
下面我們看看Cglib是如何通過生成的切面呼叫鏈將目標物件進行環繞的。前面我們講了,將切面邏輯進行織入的邏輯在CglibMethodInvocation
中,實際上其呼叫邏輯在其proceed()
方法中,這裡我們直接看該方法的原始碼:
public Object proceed() throws Throwable {
// 這裡currentInterceptorIndex記錄了當前呼叫鏈中正在呼叫的Intercepor的下標,該數值初始為-1
if (this.currentInterceptorIndex ==
this.interceptorsAndDynamicMethodMatchers.size() - 1) {
// 如果呼叫鏈為空,則直接呼叫目標方法
return invokeJoinpoint();
}
// 獲取下一個需要織入的Interceptor邏輯
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
// 對動態的方法進行匹配,如果匹配成功,才進行呼叫,否則直接進行下一個Interceptor的呼叫
if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
} else {
return proceed();
}
} else {
// 如果不需要進行動態匹配,則直接進行下一步的呼叫
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
這裡proceed()
方法的邏輯比較簡單,其使用一個索引記錄了當前正在呼叫的Interceptor在呼叫鏈中的位置,並且依次對呼叫鏈進行呼叫,從而實現將切面邏輯織入目標物件的目的。這裡最終對目標物件的呼叫的邏輯在invokeJoinpoint()
方法中。
3. 小結
本文首先講解Spring是如何通過配置的引數來選擇使用哪種代理方式的,然後重點講解了Spring Aop是如何使用Cglib代理實現代理邏輯的織入的。
原文連結:https://my.oschina.net/zhangxufeng/blog/1933830