Spring's @Cacheable source code analysis (Part 1)

1. @EnableCaching source code analysis

When you want to use the @Cacheable annotation, you need to introduce the @EnableCaching annotation to enable the caching function. why? Now let’s take a look at why we need to add the @EnableCaching annotation to enable the caching aspect? The source code is as follows:

@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;
}

It can be seen that a class CachingConfigurationSelector is imported through the @Import annotation. Guess, this class must be an entry class, or a trigger class. Note that mode() here defaults to AdviceMode.PROXY.

Enter the CachingConfigurationSelector class, the source code is as follows:

public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
    
    

	@Override
	public String[] selectImports(AdviceMode adviceMode) {
    
    
		switch (adviceMode) {
    
    
			case PROXY:
				return getProxyImports();
			case ASPECTJ:
				return getAspectJImports();
			default:
				return null;
		}
	}
	
	private String[] getProxyImports() {
    
    
		List<String> result = new ArrayList<>(3);
		result.add(AutoProxyRegistrar.class.getName());
		result.add(ProxyCachingConfiguration.class.getName());
		if (jsr107Present && jcacheImplPresent) {
    
    
			result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
		}
		return StringUtils.toStringArray(result);
	}
}

From the getProxyImports() method, we can know that two classes are imported: AutoProxyRegistrar class and ProxyCachingConfiguration class. Then let’s focus on analyzing what these two classes are used for?

1、AutoProxyRegistrar

You can be sure to register a class by seeing XxxRegistrar. Enter the core source code as follows:

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    
    

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    
    
		AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
	}

	@Nullable
	public static BeanDefinition registerAutoProxyCreatorIfNecessary(
			BeanDefinitionRegistry registry, @Nullable Object source) {
    
    
		// 缓存和事物都是通过这个 InfrastructureAdvisorAutoProxyCreator 基础增强类来生成代理对象
		return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
	}
}

Obviously, an InfrastructureAdvisorAutoProxyCreator class is registered here. This class is the same as the transaction aspect entry class. This class does not do any logic. All logic is in its parent class, AbstractAutoProxyCreator, which only acts as an entry class.

The AbstractAutoProxyCreator class is another application of the BeanPostProcessor interface. So focus on the two methods of this interface. Here you only need to pay attention to the second method postProcessAfterInitialization(). The source code is as follows:

	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    
    
		if (bean != null) {
    
    
			/** 获取缓存 key */
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (this.earlyProxyReferences.remove(cacheKey) != bean) {
    
    
				/** 是否有必要创建代理对象 */
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

Obviously, the wrapIfNecessary() method will be called in the AbstractAutoProxyCreator abstract class to determine whether a proxy object needs to be created for the current bean. So what is the basis for judgment here? Entering this method, the core source code is as follows:

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    
    

		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

		 /** 合适的通知不为空 */
		if (specificInterceptors != DO_NOT_PROXY) {
    
    
		
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			return proxy;
		}
		
		return bean;
	}

In the above logic, it is obvious that the getAdvicesAndAdvisorsForBean() method is the basis for judgment. If there is a value, you need to create a proxy, otherwise you don't need to. Then focus on the internal logic of the method. The core source code is as follows:

	@Override
	@Nullable
	protected Object[] getAdvicesAndAdvisorsForBean(
			Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
    
    

		/** tc_tag-99: 查找适合这个类的 advisors 切面通知 */
		List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
		if (advisors.isEmpty()) {
    
    
			return DO_NOT_PROXY;
		}
		return advisors.toArray();
	}

Continue to enter the internal logic of findEligibleAdvisors(). The core source code is as follows:

	protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
    
    

		List<Advisor> candidateAdvisors = findCandidateAdvisors();

		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);

		extendAdvisors(eligibleAdvisors);

		if (!eligibleAdvisors.isEmpty()) {
    
    
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

See the findCandidateAdvisors() method, the internal logic is as follows:

	protected List<Advisor> findCandidateAdvisors() {
    
    
		return this.advisorRetrievalHelper.findAdvisorBeans();
	}
	
	public List<Advisor> findAdvisorBeans() {
    
    
		String[] advisorNames = this.cachedAdvisorBeanNames;
		if (advisorNames == null) {
    
    
			advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
					this.beanFactory, Advisor.class, true, false);
			this.cachedAdvisorBeanNames = advisorNames;
		}
		
		List<Advisor> advisors = new ArrayList<>();

		for (String name : advisorNames) {
    
    
			if (isEligibleBean(name)) {
    
    
				advisors.add(this.beanFactory.getBean(name, Advisor.class));
			}
		}
		return advisors;
	}

As can be seen from the above code, Spring obtains all implementation classes that implement the Advsior interface in the Spring container by calling the beanNamesForTypeIncludingAncestors(Advisor.class) method.

So now let’s see the ProxyCachingConfiguration class. The source code is as follows:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
    
    

	// cache_tag: 缓存方法增强器
	@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
			CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
    
    

		BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
		// 缓存用来解析一些属性封装的对象 CacheOperationSource
		advisor.setCacheOperationSource(cacheOperationSource);
		// 缓存拦截器执行对象
		advisor.setAdvice(cacheInterceptor);
		if (this.enableCaching != null) {
    
    
			advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
		}
		return advisor;
	}
	// 先省略一部分...
}

It is obvious that BeanFactoryCacheOperationSourceAdvisor implements the Advisor interface. Then you can get the instance when you call the beanNamesForTypeIncludingAncestors(Advisor.class) method above. The instance will be added to the candidate collection advisors and returned.

Okay, after reading the findCandidateAdvisors() method, and then looking at the findAdvisorsThatCanApply() method, the former method obtains all implementations of the Advisor interface in the Spring container. Then call the findAdvisorsThatCanApply() method to determine which Advisors are applicable to the current bean. Enter the internal logic of findAdvisorsThatCanApply(). The core source code is as follows:

	public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
    
    

		/** 没有切面,匹配个屁 */
		if (candidateAdvisors.isEmpty()) {
    
    
			return candidateAdvisors;
		}

		List<Advisor> eligibleAdvisors = new ArrayList<>();
		for (Advisor candidate : candidateAdvisors) {
    
    
			if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
    
    
				eligibleAdvisors.add(candidate);
			}
		}
		boolean hasIntroductions = !eligibleAdvisors.isEmpty();
		for (Advisor candidate : candidateAdvisors) {
    
    
			if (candidate instanceof IntroductionAdvisor) {
    
    
				// already processed
				continue;
			}
			// tc_tag-96: 开始 for 循环 candidateAdvisors 每个增强器,看是否能使用与这个 bean
			if (canApply(candidate, clazz, hasIntroductions)) {
    
    
				eligibleAdvisors.add(candidate);
			}
		}
		return eligibleAdvisors;
	}

Continue to enter canApply(). The core logic is as follows:

	public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
    
    

		/**
		 * 通过 Pointcut 获取到 ClassFilter 类的匹配器
		 * 然后匹配 targetClass 是否在 Pointcut 配置的包路径下面么?具体实现看 AspectJExpressionPointcut
		 */
		if (!pc.getClassFilter().matches(targetClass)) {
    
    
			return false;
		}
		/**
		 * 通过 Pointcut 获取到 MethodMatcher 类的匹配器
		 * 然后判断这个类下面的方法是否在 Pointcut 配置的包路径下面么?
		 * 或者是这个方法上是否标注了 @Transactional、@Cacheable等注解呢?
		 */
		MethodMatcher methodMatcher = pc.getMethodMatcher();
		if (methodMatcher == MethodMatcher.TRUE) {
    
    
			// No need to iterate the methods if we're matching any method anyway...
			return true;
		}

		IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
		if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
    
    
			introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
		}

		Set<Class<?>> classes = new LinkedHashSet<>();
		if (!Proxy.isProxyClass(targetClass)) {
    
    
			classes.add(ClassUtils.getUserClass(targetClass));
		}
		classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
		for (Class<?> clazz : classes) {
    
    
			/**
			 * tc_tag1: 获取这个 targetClass 类下所有的方法,开始挨个遍历是否满 Pointcut 配置的包路径下面么?
			 * 或者是这个方法上是否标注了 @Transactional、@Cacheable等注解呢?
			 */
			Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
			for (Method method : methods) {
    
    
				if (introductionAwareMethodMatcher != null ?
						introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
						/**
						 * tc_tag2: 注意对于 @Transactional 注解的 Pointcut 匹配还是比较复杂的,匹配逻辑在 TransactionAttributeSourcePointcut
						 */
						methodMatcher.matches(method, targetClass)) {
    
    
					return true;
				}
			}
		}

		return false;
	}

Looking at the source code above, you can see two familiar old friends: the ClassFilter class and the MethodMatcher class. The ClassFilter class is used to determine whether the Class where the current bean is located is marked with @Caching, @Cacheable , @CachePut , and @CacheEvict . annotation. The MethodMatcher class is used to determine whether the current bean method is annotated with @Caching , @Cacheable , @CachePut , and @CacheEvict annotations.

So where are these two filter objects created?

Back to the ProxyCachingConfiguration class, the source code is as follows:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
    
    

	// cache_tag: 缓存方法增强器
	@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
			CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
    
    

		BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
		// 缓存用来解析一些属性封装的对象 CacheOperationSource
		advisor.setCacheOperationSource(cacheOperationSource);
		// 缓存拦截器执行对象
		advisor.setAdvice(cacheInterceptor);
		if (this.enableCaching != null) {
    
    
			advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
		}
		return advisor;
	}
}

See BeanFactoryCacheOperationSourceAdvisor class,We know that an Advisor must be composed of Advice and Pointcut.
Pointcut must be composed of ClassFilter and MethodMatcher
. Enter this class, the source code is as follows:

public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
    
    

	@Nullable
	private CacheOperationSource cacheOperationSource;

	private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
    
    

		@Nullable
		protected CacheOperationSource getCacheOperationSource() {
    
    
			return cacheOperationSource;
		}
	};

	public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {
    
    
		this.cacheOperationSource = cacheOperationSource;
	}

	public void setClassFilter(ClassFilter classFilter) {
    
    
		this.pointcut.setClassFilter(classFilter);
	}

	@Override
	public Pointcut getPointcut() {
    
    
		return this.pointcut;
	}
}

Pay attention to the CacheOperationSourcePointcut class and enter the source code as follows:

abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
    
    

	protected CacheOperationSourcePointcut() {
    
    
		setClassFilter(new CacheOperationSourceClassFilter());
	}

	@Override
	public boolean matches(Method method, Class<?> targetClass) {
    
    
		CacheOperationSource cas = getCacheOperationSource();
		return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
	}

	@Nullable
	protected abstract CacheOperationSource getCacheOperationSource();

	private class CacheOperationSourceClassFilter implements ClassFilter {
    
    

		@Override
		public boolean matches(Class<?> clazz) {
    
    
			if (CacheManager.class.isAssignableFrom(clazz)) {
    
    
				return false;
			}
			CacheOperationSource cas = getCacheOperationSource();
			return (cas == null || cas.isCandidateClass(clazz));
		}
	}
}

It can be seen from the source code that ClassFilter = CacheOperationSourceClassFilter, MethodMatcher = CacheOperationSourcePointcut. The matches() method of ClassFilter here does not filter classes. This class filtering is placed in the matches() method of MethodMatcher and is integrated with the method filtering.

So here we focus on the MethodMatcher#matches() matching method to see how it matches. The core source code is as follows:

abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
    
    

	@Override
	public boolean matches(Method method, Class<?> targetClass) {
    
    
		CacheOperationSource cas = getCacheOperationSource();
		return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
	}
}

Enter the getCacheOperations() method, the core source code is as follows:

	@Override
	@Nullable
	public Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass) {
    
    
		if (method.getDeclaringClass() == Object.class) {
    
    
			return null;
		}

		Object cacheKey = getCacheKey(method, targetClass);
		Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);

		if (cached != null) {
    
    
			return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
		}
		else {
    
    
			// cache_tag: 计算缓存注解上面的配置的值,然后封装成 CacheOperation 缓存属性对象,基本和事物的一样
			// 注意每个缓存注解对应一种不同的解析处理
			Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
			if (cacheOps != null) {
    
    
				if (logger.isTraceEnabled()) {
    
    
					logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
				}
				this.attributeCache.put(cacheKey, cacheOps);
			}
			else {
    
    
				this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
			}
			return cacheOps;
		}
	}

I found that this code matches the @Transactional annotation exactly the same. In fact, it uses the same set of logic, but the annotations are different. The matching process is exactly the same. See the computeCacheOperations() method. The core source code is as follows:

	@Nullable
	private Collection<CacheOperation> computeCacheOperations(Method method, @Nullable Class<?> targetClass) {
    
    
		if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    
    
			return null;
		}

		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

		Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
		if (opDef != null) {
    
    
			return opDef;
		}

		opDef = findCacheOperations(specificMethod.getDeclaringClass());
		if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
    
    
			return opDef;
		}

		if (specificMethod != method) {
    
    
			opDef = findCacheOperations(method);
			if (opDef != null) {
    
    
				return opDef;
			}
			// Last fallback is the class of the original method.
			opDef = findCacheOperations(method.getDeclaringClass());
			if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
    
    
				return opDef;
			}
		}
		return null;
	}
	
	@Nullable
	private Collection<CacheOperation> parseCacheAnnotations(
			DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {
    
    

		Collection<? extends Annotation> anns = (localOnly ?
				AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) :
				AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS));
		if (anns.isEmpty()) {
    
    
			return null;
		}
		// cache_tag: 熟悉不能再熟悉的缓存注解 Cacheable/CacheEvict/CachePut/Caching
		// 注意每一种类型的注解解析是不太一样的哦,具体看 parseCacheableAnnotation() 解析方法
		final Collection<CacheOperation> ops = new ArrayList<>(1);
		anns.stream().filter(ann -> ann instanceof Cacheable).forEach(
				ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann)));
		anns.stream().filter(ann -> ann instanceof CacheEvict).forEach(
				ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann)));
		anns.stream().filter(ann -> ann instanceof CachePut).forEach(
				ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann)));
		anns.stream().filter(ann -> ann instanceof Caching).forEach(
				ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops));
		return ops;
	}

From the source code above, you can see that the matching logic will match whether there are annotations such as @Cacheable on the current bean method. If the method is not found, it will look for the class where the current bean is located. If it is not found, it will look for the interface method. If it is not found, it will continue to look for the interface class. If it is not found, it will return null directly. This is his MethodMatcher#matches() matching process.

So the bottom layer of the canApply() method call is to call MethodMatcher to perform matching. The findAdvisorsThatCanApply() method calls canApply(), which is such a matching process. If the match is successful, it means that the Advisor can be used to enhance the current bean. Then you need to call the createProxy() method to create a proxy object. Let the proxy object call the enhanced logic in the Advisor, and then call the target method after execution. Return to the upper layer and call getAdvicesAndAdvisorsForBean(). The source code is as follows:

	@Override
	@Nullable
	protected Object[] getAdvicesAndAdvisorsForBean(
			Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
    
    

		/** tc_tag-99: 查找适合这个类的 advisors 切面通知 */
		List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
		if (advisors.isEmpty()) {
    
    
			return DO_NOT_PROXY;
		}
		return advisors.toArray();
	}

Return to the top layer and call wrapIfNecessary(). The source code is as follows:

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    
    

		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

		 /** 合适的通知不为空 */
		if (specificInterceptors != DO_NOT_PROXY) {
    
    
		
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			return proxy;
		}
		
		return bean;
	}

Finally, createProxy() is called to create the proxy object. At this point, creating the proxy object during Spring startup is completed. Next, we need to analyze the calling process.

2、ProxyCachingConfiguration

This class is to provide the support class needed for caching. For example, Advisor is registered in the Spring container in advance through @Bean method. The source code is as follows:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
    
    

	// cache_tag: 缓存方法增强器
	@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
			CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
    
    

		BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
		// 缓存用来解析一些属性封装的对象 CacheOperationSource
		advisor.setCacheOperationSource(cacheOperationSource);
		// 缓存拦截器执行对象
		advisor.setAdvice(cacheInterceptor);
		if (this.enableCaching != null) {
    
    
			advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
		}
		return advisor;
	}

	// cache_tag: Cache 注解解析器
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public CacheOperationSource cacheOperationSource() {
    
    
		return new AnnotationCacheOperationSource();
	}

	// cache_tag: 缓存拦截器执行器
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {
    
    
		CacheInterceptor interceptor = new CacheInterceptor();
		interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
		interceptor.setCacheOperationSource(cacheOperationSource);
		return interceptor;
	}

}

You only need to pay attention to Advisor here. The CacheOperationSource object is just a property encapsulation of the @Cacheable and other annotations after parsing them.

Advisor = Advice + Pointcut

Advisor = BeanFactoryCacheOperationSourceAdvisor

Advice = CacheInterceptor

Pointcut = CacheOperationSourcePointcut

Pointcut = ClassFilter + MethodMatcher

ClassFilter = CacheOperationSourceClassFilter

MethodMatcher = CacheOperationSourcePointcut

For the calling process, please jump to the next article Spring's @Cacheable source code analysis (Part 2)

Eventually you will find that the @EnableCaching annotation is basically the same as @EnableTransactionManagement, except that the annotation is changed, replacing @Transactional with @Cacheable and other annotations.

Guess you like

Origin blog.csdn.net/qq_35971258/article/details/128727311