spring源码系列8:AOP源码解析之代理的建立

回顾

首先回顾:java

JDK动态代理与CGLIB动态代理spring

Spring中的InstantiationAwareBeanPostProcessor和BeanPostProcessor的区别数组

咱们得知 JDK动态代理两要素:Proxy+InvocationHandler CGLB动态代理两要素:Enhancer + MethodInterceptor(Callback)缓存

springAOP底层是经过动态代理和CGLB代理实现的。也就是spring最终的落脚点还应该是在Proxy+InvocationHandler 或者Enhancer + MethodInterceptor上。springboot

带着这个期待咱们看看spring是如何组织AOP的,并在动态代理之上创建一个AOP体系的。bash

概念的理解:

  • 通知: 通知能够理解加强器, 就是作的额外工做.
  • 链接点: 能够容许切面插入的点, 这个点能够是方法调用时, 抛出异常时等.
  • 切点: 并非全部的链接点都须要被切面插入, 切点就是定义哪些链接点能够插入切面.

总结为:从全部链接点中选出部分链接点, 进行拦截,执行额外操做。框架

组件的理解:

AOP最先由AOP联盟的组织提出的,制定了一套规范.Spring将AOP思想引入到框架中,必须遵照AOP联盟的规范.ide

注意:各类组件很是之多. 我只把自认为涉及到主线理解的组件列出工具


AOP联盟规范定义组件

通知(加强器): 不要纠结于太多. 就统一理解为拦截器或者加强器。post

file

注意一点:这里的MethodInterceptor区别于上文提到的
org.springframework.cglib.proxy.MethodInterceptor不要混了。
复制代码

链接点(被拦截):spring大多都是对方法调用的拦截, 这里能够理解为方法调用.

file


spring定义组件

1.Pointcut定义哪些链接点能够插入切面。提供ClassFilter 与MethodMatcher两种匹配器,匹配链接点。

2.Advisor : 包装Advice和Pointcut .语义: 在哪里拦截, 插入什么样的加强器(在哪里作什么)Advisor=Advice+Pointcut 分为

  • IntroductionAdvisor: 引介加强,在不修改某个类的状况下, 让其具有某接口的功能。
  • PointcutAdvisor: 与切点有关的Advisor

3.TargetSource: 对目标对象的封装。

4.Advised接口 : 包装 Advisor 与 TargetSource。 实现类AdvisedSupport:从其两个属性上,真切看出其封装了 Advisor 与 TargetSource

  • TargetSource targetSource = EMPTY_TARGET_SOURCE; 表示目标对象
  • private List advisors = new ArrayList(); 存储Advisor

5.AopProxyAOP代理对象

  • 分JDK代理(JdkDynamicAopProxy)
  • Cglib代理(CglibAopProxy)
    在这里插入图片描述

JdkDynamicAopProxy与CglibAopProxy都实现了AopProxy的getProxy()方法,用于返回真正的代理对象。

6.AopProxyFactoryAOP代理策略工厂类。根据条件选择使用JDK仍是Cglib的方式来建立代理对象。

file

7.ProxyCreatorSupport: 从其语义上理解,对建立代理对象的支持。咱们从其继承关系上看看他是如何提供支持的。

  • ProxyConfig: 提供aop配置属性。
  • AdvisedSupport:继承ProxyConfig,实现了Advised接口。封装通知(Advise)和TargetSource,并提供对他们的操做
  • ProxyCreatorSupport: 包含AopProxyFactory,AopProxyFactory策略工厂提供对JdkDynamicAopProxy与CglibAopProxy的建立

file

8.AbstractAutoProxyCreator: 从其继承关系。咱们分析下这个组件。

  • 首先他是一个(BeanPostProcessor)InstantiationAwareBeanPostProcessor 因此他会在spring的Bean生产线createBean()过程当中被执行。在BeanDefinition建立Bean的过程当中进行拦截,根据要求建立出代理对象。
  • Aware: 能够获取到BeanFactory仓库, 与BeanClassLoader加载器。
  • ProxyCreatorSupport,继承了建立代理的功能。

关于BeanPostProcessor与InstantiationAwareBeanPostProcessor的理解,仍是深入理解Spring中的InstantiationAwareBeanPostProcessor和BeanPostProcessor的区别

file

小结

若是总结下组件之间的关系:

开发人员定义切面信息--》spring解析出Advice,切点等配置信息--》AopProxyFactory根据配置建立代理对象--》代理方法执行。

这也是springAOP大部分工做内容。

笼统来看:

  • 《1》定义的解析:
  • 《2》代理的建立:
  • 《3》代理的执行:

下面咱们重点看看定义的解析与代理的建立。

代理的生成过程:

代理的执行过程: 咱们仍是以springboot环境下的AOP实现来说讲AOP的过程.

1.配置AOP的扩展器,拦截BeanDefiniiton建立Bean的过程:AnnotationAwareAspectJAutoProxyCreator:

首先配置AOP环境

@EnableAspectJAutoProxy注解上的@Import(AspectJAutoProxyRegistrar.class)引入AspectJAutoProxyRegistrar类.AspectJAutoProxyRegistrar实现了ImportBeanDefinitionRegistrar接口. 因此其registerBeanDefinitions()会有一些注入BeanDefiniiton的操做 . registerBeanDefinitions()方法会向仓库中注入一个AnnotationAwareAspectJAutoProxyCreator.

@Override
	public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

//注入AnnotationAwareAspectJAutoProxyCreator扩展器
		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
			AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
		}
		if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
			AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
		}
	}
复制代码

AnnotationAwareAspectJAutoProxyCreator 间接继承了AOP扩展工具AbstractAutoProxyCreator, 是一个BeanPostProcessor。

AbstractAutoProxyCreator:

  • 是一个BeanPostProcessor,能够拦截Bean建立过程
  • 继承了ProxyCreatorSupport具备代理类的建立能力。

这样:AOP环境有了。

2.拦截生产过程,建立AOP对象:

createBean()生产线在建立Bean的过程当中. 会执行全部的BeanPostProcessor. AnnotationAwareAspectJAutoProxyCreator(AbstractAutoProxyCreator)做为一个BeanPostProcessor也在此时执行.

Spring中的InstantiationAwareBeanPostProcessor和BeanPostProcessor的区别 讲过. 实现了InstantiationAwareBeanPostProcessor接口的BeanPostProcessor的会有5个方法伴随Bean建立过程且执行顺序。 postProcessBeforeInstantiation() -->postProcessAfterInstantiation-->postProcessPropertyValues-->postProcessBeforeInitialization()-->postProcessAfterInitialization()

过程:

(2.1)首先执行的是postProcessBeforeInstantiation 实例化前置处理方法

@Override
	public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
		Object cacheKey = getCacheKey(beanClass, beanName);
		if (beanName == null || !this.targetSourcedBeans.contains(beanName)) {
			if (this.advisedBeans.containsKey(cacheKey)) {
				return null;
			}
			if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
				this.advisedBeans.put(cacheKey, Boolean.FALSE);
				return null;
			}
		}
		if (beanName != null) {
			TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
			if (targetSource != null) {
				this.targetSourcedBeans.add(beanName);
				Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
				Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
				this.proxyTypes.put(cacheKey, proxy.getClass());
				return proxy;
			}
		}
		return null;
	}
复制代码

主要分为两部分: (2.1.1)先判断是否应该代理

  • isInfrastructureClass(beanClass)若是是AOP基础设施类, 不代理。指的是Advice、Pointcut、Advisor等AOP相关定义类不该该建立代理
  • shouldSkip()应该跳过的类,不代理。判断的依据:默认是都不跳过的. 子类可能重写其判断依据.AnnotationAwareAspectJAutoProxyCreator的父类AnnotationAwareAspectJAutoProxyCreator重写了shouldSkip方法(《1》并伴随切面定义的解析)。

重点看看shouldSkip()

/* 1.首先查找适于当前Bean的Advisor .通findCandidateAdvisors()去查找 findCandidateAdvisors()会通过两个渠道获取Advisors. > BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans():是从仓库中找实现了Advisor类的Advisor > BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors()会从仓库中取出@Aspect注解的切面类,解析出Advisor 重点说一下其中buildAspectJAdvisors >遍历BeanFactory仓库中的BeanDefinition,根据其类型判断其是不是isAspect(): 判断条件是类是否被@Aspect注释,而且没有被aspectj编译过 >若是是, 根据当前BeanDefinition建立一个(AspectJAdvisorFactory)BeanFactoryAspectInstanceFactory工厂对象. >使用AspectJAdvisorFactory 工厂建立List<Advisor> >上文已经说过Advisor=Advice+Pointcut, Advice 就是对通知的封装(@Before...), Pointcut 是对切点的封装. 小结:此次知道咱们定义一个切面类, 被解析什么样子了吧 2.判断找到的CandidateAdvisors中有没有当前BeanDefinition,有的话就跳过。 */
@Override
	protected boolean shouldSkip(Class<?> beanClass, String beanName) {
		// TODO: Consider optimization by caching the list of the aspect names
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		for (Advisor advisor : candidateAdvisors) {
			if (advisor instanceof AspectJPointcutAdvisor) {
				if (((AbstractAspectJAdvice) advisor.getAdvice()).getAspectName().equals(beanName)) {
					return true;
				}
			}
		}
		return super.shouldSkip(beanClass, beanName);
	}
复制代码

(2.1.2) 判断是否有自定义的 TargetSource。 若是有自定义TargetSource ,将当前beanName放入targetSourcedBeans缓存中,直接走建立代理的分支,不会走createBean去建立Bean,这里就是给一个机会。 关于自定义TargetSource这个分支暂时不讲。

if (beanName != null) {
			TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
			if (targetSource != null) {
				this.targetSourcedBeans.add(beanName);
				Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
				Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
				this.proxyTypes.put(cacheKey, proxy.getClass());
				return proxy;
			}
		}
复制代码

总结postProcessBeforeInstantiation工做内容:

  • 解析切面定义
  • 提供一个机会直接返回代理对象,不走createBean()流水线。自定义TargetSource

(2.2)postProcessAfterInstantiation

  • return true ;

(2.3)postProcessBeforeInitialization

  • 返回Bean没有作处理

(2.4)postProcessAfterInitialization() (敲黑板,重点

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (this.earlyProxyReferences.remove(cacheKey) != bean) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}
复制代码

关键便在于wrapIfNecessary方法: 名字 wrap if Necessary 若是知足条件包装 这个方法就能够拆成两部分看:

  1. 知足条件:
  2. 包装:
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		//1.自定义TargetSource,已经进行过代理子类生成 。 不包装直接返回Bean实例
		if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
			return bean;
		}
		//2.已经断定不须要代理的, 不代理
		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
			return bean;
		}
		//3.isInfrastructureClass(bean.getClass())是基础设施类的不代理
		//4.shouldSkip(bean.getClass(), beanName)应该跳过的不代理
		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;
		}
		//5.没有具体拦截器的不代理
		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}
复制代码

咱们挨个分析下这两部分


(2.4.1) 知足条件

不代理的状况

  • 自定义TargetSource,已经进行过代理子类生成 。 不包装直接返回Bean实例。()
  • 已经断定不须要代理的, 不代理。 直接返回Bean实例
  • isInfrastructureClass(bean.getClass())是基础设施类的不代理。 (上文已经提过)
  • shouldSkip(bean.getClass(), beanName)应该跳过的不代理 (上文已经提过)
  • 没有具体拦截器的不代理

代理的的状况

  • 有具体Advice的才代理:getAdvicesAndAdvisorsForBean() 的返回不为空的。(Create proxy if we have advice.)

Advisor寻找:重点就落在了此处。

即getAdvicesAndAdvisorsForBean方法,这里进行的即是去容器中寻找适用于当前bean的Advisor,最终调用的是

AbstractAdvisorAutoProxyCreator.findEligibleAdvisors:

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;
	}
复制代码
  • findCandidateAdvisors: 获取全部Advisor ,前面已经说过。 由于作了缓存此处直接从缓存中取。

  • findAdvisorsThatCanApply: 看其传入的参数,candidateAdvisors(全部的候选Advisor), 也就是这个方法确定就是从全部的候选Advisor找出适合当前Bean

  • extendAdvisors: 容许子类添加advisor

适用性判断 findAdvisorsThatCanApply最终调用AopUtils.findAdvisorsThatCanApply:

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
		if (candidateAdvisors.isEmpty()) {
			return candidateAdvisors;
		}
		List<Advisor> eligibleAdvisors = new LinkedList<Advisor>();
		//第一遍
		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;
			}
			if (canApply(candidate, clazz, hasIntroductions)) {
				eligibleAdvisors.add(candidate);
			}
		}
		return eligibleAdvisors;
	}
复制代码

此方法有两个for循环。 第一个for循环寻找IntroductionAdvisor(引介加强)类型的advisor,调用AopUtils.canApply 第二遍for循环寻找普通的advisor,调用AopUtils.canApply

AopUtils.canApply针对两种类型的Advisor作了不一样的判断:

  • 针对IntroductionAdvisor类型advisor的,只须要校验Advisor的ClassFilter是否匹配当前类
  • 针对普通的advisor:(1)首先查看定的类是否在Pointcut的匹配范围内;(2)是的话,再查看是否能匹配此类任意方法,是的话返回true;(3)不能匹配任意方法,便会用反射的方法获取targetClass(被检测类)的所有方法逐一交由Pointcut的MethodMatcher.matches方法进行检测。
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
		Assert.notNull(pc, "Pointcut must not be null");
		if (!pc.getClassFilter().matches(targetClass)) {
			return false;
		}
		//是否配置任意方法
		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<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
		classes.add(targetClass);
		for (Class<?> clazz : classes) {
			Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
			for (Method method : methods) {
				if ((introductionAwareMethodMatcher != null &&
						introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
						methodMatcher.matches(method, targetClass)) {
					return true;
				}
			}
		}

		return false;
	}
复制代码

到此,AopUtils.canApply返回true后。findAdvisorsThatCanApply()算是找到了能应用于当前类的Advisors. 再extendAdvisors后;对,应用于当前类的Advisors一番排序后,getAdvicesAndAdvisorsForBean工做完成。

getAdvicesAndAdvisorsForBean()返回若是不为Null。那下面就是包装。


(2.4.2) 包装 在getAdvicesAndAdvisorsForBean返回advisors不为null后,能够建立代理。

protected Object createProxy( Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

		if (!proxyFactory.isProxyTargetClass()) {
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		return proxyFactory.getProxy(getProxyClassLoader());
	}
复制代码
  • 首先建立ProxyFactory 工厂,从AbstractAutoProxyCreator复制代理属性,由于AbstractAutoProxyCreator继承了ProxyConfig,因此自己也是一个代理配置类。
  • 判断其是基于类代理, 仍是基于接口代理。若是基于接口代理,获取接口,并设置到ProxyFactory 。
  • buildAdvisors()方法,整合getAdvicesAndAdvisorsForBean找的advisors 与AbstractAutoProxyCreator. interceptorNames属性可能设置的加强器,统一包装成Advisor类型数组,设置到ProxyFactory 。而且AbstractAutoProxyCreator. interceptorNames属性设置的加强器在前。
  • customizeProxyFactory(proxyFactory); 子类能够继续处理ProxyFactory
  • 设置ProxyFactory的Frozen属性
  • setPreFiltered设置ProxyFactory 的preFiltered
  • getProxy 返回代理对象

咱们能够看出,这里两大部分:

(1)建立proxyFactory工厂,并配置工厂

(2)proxyFactory.getProxy(getProxyClassLoader())返回代理对象。

proxyFactory.getProxy(ClassLoader classLoader)

proxyFactory.getProxy(ClassLoader classLoader) 首先会先建立一个默认的策略工厂DefaultAopProxyFactory。DefaultAopProxyFactory 会根据proxyFactory是基于接口的代理仍是基于类的代理,选择建立JdkDynamicAopProxy对象,或者建立一个CglibAopProxy对象

这里有必要介绍下DefaultAopProxyFactory.createAopProxy方法,此方法用来判断是JDK代理,仍是CGLB代理。

if (config.isOptimize() //是否对代理类的生成使用策略优化
    || config.isProxyTargetClass() //设置proxy-target-class="true
    || hasNoUserSuppliedProxyInterfaces(config)) {//目标类是否有接口存在 且只有一个接口的时候接口类型不是
        //SpringProxy类型 
			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的方式生成代理对象
            //若是目标类是Proxy类型 则仍是使用JDK的方式生成代理对象
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			//CGLB代理。
			return new ObjenesisCglibAopProxy(config);
		}
		else {
		    //JDK代理
			return new JdkDynamicAopProxy(config);
		}
复制代码

咱们能够看出高版本的会根据设置和目标类的实际状况选择使用CGLB或者JDK代理。也就是说,设置了proxy-target-class="true"不必定就是使用CGLB,而是根据实际状况定。具体版本具体对待。

JdkDynamicAopProxy.getProxy()或者CglibAopProxygetProxy()才是真正返回代理对象。


再次回顾JDK动态代理与CGLIB动态代理

JdkDynamicAopProxy.getProxy()

@Override
	public Object getProxy(ClassLoader classLoader) {
		if (logger.isDebugEnabled()) {
			logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
		}
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}
复制代码

咱们看到JDK动态代理的两个要素: Proxy+InvocationHandler InvocationHandler 此时就是JdkDynamicAopProxy,同时JdkDynamicAopProxy封装了advised。这样完美的把advised与JDK动态代理联系在了一块儿

接下来就是在内存中生成一个字节码JDK代理类$Proxy66.class,生成真正的代理对象了。

CglibAopProxygetProxy() (省略部分源码,直奔主题)

// Configure CGLIB Enhancer...
			Enhancer enhancer = createEnhancer();
			Callback[] callbacks = getCallbacks(rootClass);
			enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
			return createProxyClassAndInstance(enhancer, callbacks);
		}
		
复制代码

咱们看到了Cglb熟悉的要素Enhancer 。MethodInterceptor去哪了呢?

跟进getCallbacks()方法,会发现此方法会建立一个DynamicAdvisedInterceptor。DynamicAdvisedInterceptor实现了MethodInterceptor,并封装了advisors。

Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
复制代码

这样,CGLB动态代理完美的与advised结合在了一块儿。 接下来就是在内存中生成一个新的字节码CGLB代理类***$$FastClassByCGLIB$$29e52466,并生成真实代理对象了。

总结

springaop 底层仍是JDK动态代理,CGLB动态代理。经过把加强器封装到Advised中,把Advised与InvocationHandler或者MethodInterceptor联系起来,完美的实现AOP技术

都说事务是根据AOP实现的,趁热打铁,看看事务是如何建立代理的?敬请期待 欢迎你们关注个人公众号【源码行动】,最新我的理解及时奉送。

在这里插入图片描述
相关文章
相关标签/搜索