服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 深入解析spring AOP原理及源码

深入解析spring AOP原理及源码

2022-11-13 12:00morris131 Java教程

这篇文章主要介绍了spring AOP原理及源码分析,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴,需要的朋友可以参考下

@EnableAspectJAutoProxy

@EnableAspectJAutoProxy注解用于开启AOP功能,那么这个注解底层到底做了什么呢?

查看@EnableAspectJAutoProxy的源码,发现它使用@Import注解向Spring容器中注入了一个类型为AspectJAutoProxyRegistrar的Bean:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
 
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
 
        // 注入一个bean名字为org.springframework.aop.config.internalAutoProxyCreator的AspectJAwareAdvisorAutoProxyCreator
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
 
        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                // proxyTargetClass为true
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                // exposeProxy为true
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }
    }
 
}

AspectJAutoProxyRegistrar实现了ImportBeanDefinitionRegistrar接口,而ImportBeanDefinitionRegistrar是spring提供的扩展点之一,主要用来向容器中注入BeanDefinition,spring会根据BeanDefinion来生成Bean。

那么AspectJAutoProxyRegistrar到底向容器中注入了什么BeanDefinion呢?

org.springframework.aop.config.AopConfigUtils#registerAspectJAnnotationAutoProxyCreatorIfNecessary(org.springframework.beans.factory.support.BeanDefinitionRegistry)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
    return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, null);
}
 
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
        BeanDefinitionRegistry registry, @Nullable Object source) {
    // AnnotationAwareAspectJAutoProxyCreator
    return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}
 
@Nullable
private static BeanDefinition registerOrEscalateApcAsRequired(
        Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {
 
    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
 
    if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
        BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
            int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
            int requiredPriority = findPriorityForClass(cls);
            if (currentPriority < requiredPriority) {
                apcDefinition.setBeanClassName(cls.getName());
            }
        }
        return null;
    }
 
    // 注入AspectJAwareAdvisorAutoProxyCreator
    RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
    beanDefinition.setSource(source);
    beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
    beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
    return beanDefinition;
}

从源码可以发现AspectJAutoProxyRegistrar向容器中注入了一个类型为AnnotationAwareAspectJAutoProxyCreator的Bean。

那么AnnotationAwareAspectJAutoProxyCreator又是干什么的呢?

AnnotationAwareAspectJAutoProxyCreator主要实现了三个接口(由父类AbstractAutoProxyCreator实现):

  • 实现了BeanFactoryAware,内部持有BeanFactory的引用。
  • 实现了SmartInstantiationAwareBeanPostProcessor(InstantiationAwareBeanPostProcessor).postProcessBeforeInstantiation,这个方法在bean的实例化(bean创建之前)之前执行。
  • 实现了BeanPostProcessor.postProcessBeforeInitialization(),这个方法在bean的初始化之前(bean创建之后,属性被赋值之前)执行,BeanPostProcessor.postProcessAfterInitialization()在bean的初始化之后执行。

AnnotationAwareAspectJAutoProxyCreator的继承结构:

深入解析spring AOP原理及源码

找切面

org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
    /**
     * @see AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors()
     */
    // 获取容器中所有的切面Advisor
    // 这里返回的切面中的方法已经是有序的了,先按注解顺序(Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),再按方法名称
    List<Advisor> candidateAdvisors = findCandidateAdvisors();
    // 获取所有能够作用于当前Bean上的Advisor
    List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
    /**
     * @see AspectJAwareAdvisorAutoProxyCreator#extendAdvisors(java.util.List)
     */
    // 往集合第一个位置加入了一个DefaultPointcutAdvisor
    extendAdvisors(eligibleAdvisors);
    if (!eligibleAdvisors.isEmpty()) {
        /**
         * @see AspectJAwareAdvisorAutoProxyCreator#sortAdvisors(java.util.List)
         */
        // 这里是对切面进行排序,例如有@Order注解或者实现了Ordered接口
        eligibleAdvisors = sortAdvisors(eligibleAdvisors);
    }
    return eligibleAdvisors;
}

org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

?
1
2
3
4
5
6
7
8
9
10
11
protected List<Advisor> findCandidateAdvisors() {
    // Add all the Spring advisors found according to superclass rules.
    // 获取容器中所有的切面Advisor
    List<Advisor> advisors = super.findCandidateAdvisors();
    // Build Advisors for all AspectJ aspects in the bean factory.
    if (this.aspectJAdvisorsBuilder != null) {
        // 这里还需要解析@Aspect注解,生成Advisor
        advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
    }
    return advisors;
}

org.springframework.aop.support.AopUtils#findAdvisorsThatCanApply

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
    if (candidateAdvisors.isEmpty()) {
        return candidateAdvisors;
    }
    List<Advisor> eligibleAdvisors = new ArrayList<>();
    // InstantiationModelAwarePointcutAdvisorImpl
    for (Advisor candidate : candidateAdvisors) {
        if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
            // IntroductionAdvisor类型为引入切面,具体类型为DeclareParentsAdvisor
            eligibleAdvisors.add(candidate);
        }
    }
    boolean hasIntroductions = !eligibleAdvisors.isEmpty();
    for (Advisor candidate : candidateAdvisors) {
        if (candidate instanceof IntroductionAdvisor) {
            // already processed
            continue;
        }
        // PointCut中的ClassFilter.match 匹配类
        // PointCut中的MethodMatcher.match 匹配方法
        if (canApply(candidate, clazz, hasIntroductions)) {
            // @Aspect,类型为InstantiationModelAwarePointcutAdvisorImpl
            eligibleAdvisors.add(candidate);
        }
    }
    return eligibleAdvisors;
}

代理对象的创建

代理对象的创建时机位于bean的初始化之后,因为代理对象内部还是需要去调用目标对象的方法,所以需要让目标对象实例化并完成初始化后才会创建代理对象。

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization

?
1
2
3
4
5
6
7
8
9
10
11
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.
    /**
     * @see AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean(java.lang.Class, java.lang.String, org.springframework.aop.TargetSource)
     */
    // 获取与当前Bean匹配的切面
    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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
        @Nullable Object[] specificInterceptors, TargetSource targetSource) {
 
    if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
        AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
    }
 
    // 创建代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);
 
    if (!proxyFactory.isProxyTargetClass()) {
        // 进来说明proxyTargetClass=false,指定JDK代理
        if (shouldProxyTargetClass(beanClass, beanName)) {
            // 进来这里说明BD中有个属性preserveTargetClass=true,可以BD中属性设置的优先级最高
            proxyFactory.setProxyTargetClass(true);
        }
        else {
            // 这里会判断bean有没有实现接口,没有就只能使用CGlib
            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);
    }
 
    // 使用JDK或者CGlib创建代理对象
    return proxyFactory.getProxy(getProxyClassLoader());
}

org.springframework.aop.framework.JdkDynamicAopProxy#getProxy(java.lang.ClassLoader)

?
1
2
3
4
5
6
7
8
public Object getProxy(@Nullable ClassLoader classLoader) {
    if (logger.isTraceEnabled()) {
        logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
    }
    Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
    findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
    return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

这里主要看JDK动态代理的实现,Proxy.newProxyInstance()的第三个参数为InvocationHandler,而这里传的是this,也就是当前的类肯定实现了InvocationHandler接口。

代理方法的执行

由于是JDK动态代理,那么代理方法的调用肯定会进入InvocationHandler.invoke()方法中,这里的InvocationHandler的实现类为org.springframework.aop.framework.JdkDynamicAopProxy。

org.springframework.aop.framework.JdkDynamicAopProxy#invoke

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;
 
    TargetSource targetSource = this.advised.targetSource;
    Object target = null;
 
    try {
        if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
            // The target does not implement the equals(Object) method itself.
            return equals(args[0]);
        }
        else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
            // The target does not implement the hashCode() method itself.
            return hashCode();
        }
        else if (method.getDeclaringClass() == DecoratingProxy.class) {
            // There is only getDecoratedClass() declared -> dispatch to proxy config.
            return AopProxyUtils.ultimateTargetClass(this.advised);
        }
        else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
                method.getDeclaringClass().isAssignableFrom(Advised.class)) {
            // Service invocations on ProxyConfig with the proxy config...
            return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
        }
 
        Object retVal;
 
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }
 
        // Get as late as possible to minimize the time we "own" the target,
        // in case it comes from a pool.
        target = targetSource.getTarget(); // 目标对象
        Class<?> targetClass = (target != null ? target.getClass() : null); // 目标对象的类型
 
        // Get the interception chain for this method.
        // 这里会对方法进行匹配,因为不是目标对象中的所有方法都需要增强
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
 
        // Check whether we have any advice. If we don't, we can fallback on direct
        // reflective invocation of the target, and avoid creating a MethodInvocation.
        if (chain.isEmpty()) {
            // We can skip creating a MethodInvocation: just invoke the target directly
            // Note that the final invoker must be an InvokerInterceptor so we know it does
            // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
            // 没有匹配的切面,直接通过反射调用目标对象的目标方法
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        }
        else {
            // We need to create a method invocation...
            MethodInvocation invocation =
                    new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // Proceed to the joinpoint through the interceptor chain.
            /**
             * @see ReflectiveMethodInvocation#proceed()
             */
            // 这里才是增强的调用,重点,火炬的传递
            retVal = invocation.proceed();
        }
 
        // Massage return value if necessary.
        Class<?> returnType = method.getReturnType();
        if (retVal != null && retVal == target &&
                returnType != Object.class && returnType.isInstance(proxy) &&
                !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
            // Special case: it returned "this" and the return type of the method
            // is type-compatible. Note that we can't help if the target sets
            // a reference to itself in another returned object.
            retVal = proxy;
        }
        else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
            throw new AopInvocationException(
                    "Null return value from advice does not match primitive return type for: " + method);
        }
        return retVal;
    }
    finally {
        if (target != null && !targetSource.isStatic()) {
            // Must have come from TargetSource.
            targetSource.releaseTarget(target);
        }
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}

org.springframework.aop.framework.ReflectiveMethodInvocation#proceed

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public Object proceed() throws Throwable {
    // We start with an index of -1 and increment early.
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 执行到最后一个Advice,才会到这里执行目标方法
        return invokeJoinpoint();
    }
 
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // Evaluate dynamic method matcher here: static part will already have
        // been evaluated and found to match.
        // dm.isRuntime()=true的走这
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        }
        else {
            // Dynamic matching failed.
            // Skip this interceptor and invoke the next in the chain.
            return proceed();
        }
    }
    else {
        // It's an interceptor, so we just invoke it: The pointcut will have
        // been evaluated statically before this object was constructed.
        // 走这
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

interceptorsAndDynamicMethodMatchers中第一个advice为org.springframework.aop.interceptor.ExposeInvocationInterceptor。

ExposeInvocationInterceptor#invoke

org.springframework.aop.interceptor.ExposeInvocationInterceptor#invoke

?
1
2
3
4
5
6
7
8
9
10
11
12
13
private static final ThreadLocal<MethodInvocation> invocation =
 new NamedThreadLocal<>("Current AOP method invocation");
 
public Object invoke(MethodInvocation mi) throws Throwable {
    MethodInvocation oldInvocation = invocation.get();
    invocation.set(mi);
    try {
        return mi.proceed();
    }
    finally {
        invocation.set(oldInvocation);
    }
}

ExposeInvocationInterceptor#invoke,只干了一件事就是将MethodInvocation加入到了ThreadLocal中,这样后续可以在其他地方使用ExposeInvocationInterceptor#currentInvocation获取到MethodInvocation,而MethodInvocation中封装了目标对象,目标方法,方法参数等信息。

环绕通知的执行

org.springframework.aop.aspectj.AspectJAroundAdvice#invoke

?
1
2
3
4
5
6
7
8
9
public Object invoke(MethodInvocation mi) throws Throwable {
    if (!(mi instanceof ProxyMethodInvocation)) {
        throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
    }
    ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
    ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
    JoinPointMatch jpm = getJoinPointMatch(pmi);
    return invokeAdviceMethod(pjp, jpm, null, null);
}

这里会去调用环绕通知的增强方法,而环绕通知的增强方法中会执行proceedingJoinPoint.proceed(),这样就会调用下一个MethodInterceptor–>MethodBeforeAdviceInterceptor。

前置通知的执行

org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor

?
1
2
3
4
public Object invoke(MethodInvocation mi) throws Throwable {
    this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
    return mi.proceed();
}

这里又会调用MethodInvocation.proceed()传递给下一个MethodInterceptor。

后置通知的执行

org.springframework.aop.aspectj.AspectJAfterAdvice#invoke

?
1
2
3
4
5
6
7
8
public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        return mi.proceed();
    }
    finally {
        invokeAdviceMethod(getJoinPointMatch(), null, null);
    }
}

先执行MethodInvocation.proceed(),最后在finally块中调用后置通知的增强,不管目标方法有没有抛出异常,finally代码块中的代码都会执行,也就是不管目标方法有没有抛出异常,后置通知都会执行。

返回后通知的执行

org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor#invoke

?
1
2
3
4
5
public Object invoke(MethodInvocation mi) throws Throwable {
    Object retVal = mi.proceed();
    this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
    return retVal;
}

先执行MethodInvocation.proceed(),然后再执行返回后通知的增强。

异常通知的执行

org.springframework.aop.aspectj.AspectJAfterThrowingAdvice#invoke

?
1
2
3
4
5
6
7
8
9
10
11
public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        return mi.proceed();
    }
    catch (Throwable ex) {
        if (shouldInvokeOnThrowing(ex)) {
            invokeAdviceMethod(getJoinPointMatch(), null, ex);
        }
        throw ex;
    }
}

先执行MethodInvocation.proceed(),如果目标方法抛出了异常就会执行异常通知的增强,然后抛出异常,所以这时返回后通知的增强就不会执行了。

总结各种通知的执行顺序:

?
1
2
3
4
5
6
7
8
9
Around begin // 环绕通知开始
Before // 前置通知
UserServiceImpl
 // 目标方法的执行
AfterReturning
 // 返回后通知
After
 // 后置通知
Around end // 环绕通知结束

到此这篇关于spring AOP原理及源码分析的文章就介绍到这了,更多相关spring AOP原理源码内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/u022812849/article/details/124167148

延伸 · 阅读

精彩推荐