当前位置: 首页 > news >正文

Spring源码分析のAOP

文章目录

  • 前言
  • 一、wrapIfNecessary
    • 1.1、getAdvicesAndAdvisorsForBean
      • 1.1.1、findCandidateAdvisors
      • 1.1.2、findAdvisorsThatCanApply
    • 1.2、createProxy
  • 二、invoke
    • 2.1、getInterceptorsAndDynamicInterceptionAdvice
      • 2.1.1、getInterceptors
    • 2.2、proceed
      • 2.2.1、invoke
  • 三、@AspectJ模式下注解的解析
  • 总结


前言

  在Spring中,AOP通常是通过动态代理实现的,通过运行时增强的机制,以实现在目标代码执行前后的统一的逻辑。而Spring将两大动态代理,封装成为了ProxyFactory,在ProxyFactory中,会自动进行动态代理方式的选择:

@Component
public class UserService {
    public void originalMethod() {
        System.out.println("UserService originalMethod");
    }
}

  同时在ProxyFactory中,允许在目标对象上添加切面(advisors 或 advices),自定义切面的逻辑:

/**
 * 方法执行前后执行
 */
public class AroundAdvice implements MethodInterceptor {
    /**
     * Implement this method to perform extra treatments before and
     * after the invocation. Polite implementations would certainly
     * like to invoke {@link Joinpoint#proceed()}.
     *
     * @param invocation the method invocation joinpoint
     * @return the result of the call to {@link Joinpoint#proceed()};
     * might be intercepted by the interceptor
     * @throws Throwable if the interceptors or the target object
     *                   throws an exception
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("方法执行前");
        invocation.proceed();
        System.out.println("方法执行后");
        return null;
    }
}

  其中ThrowsAdvice 较为特殊,接口中没有具体的方法,但是子类在定义具体的实现时,方法的名称,签名必须要遵循特定的格式:
在这里插入图片描述

/**
 * 抛出异常后执行
 */
public class AfterThrowingAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
        System.out.println("AfterThrowingAdvice");
    }
}

  在ProxyFactory中添加切面,其中Advisor是Advice + Pointcut组合,等于切入点+切面,指定在哪些方法上应用 Advice:

public class ProxyFactoryDemo {
    public static void main(String[] args) {
        UserService userService = new UserService();


        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(userService);
     
        proxyFactory.addAdvice(new AroundAdvice());
        proxyFactory.addAdvisor(new PointcutAdvisor() {
            @Override
            public Pointcut getPointcut() {
                return new StaticMethodMatcherPointcut() {
                    @Override
                    public boolean matches(Method method, Class<?> targetClass) {
                        return method.getName().equals("originalMethod");
                    }
                };
            }
            @Override
            public Advice getAdvice() {
                return new BeforeAdvice();
            }
            @Override
            public boolean isPerInstance() {
                return false;
            }
        });
        
        UserService proxy = (UserService) proxyFactory.getProxy();
        proxy.originalMethod();
    }
}

  而在Spring的底层,如果通过AOP配置类的方式进行:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("org.ragdollcat.aop.aspect")
public class AppConfig {
}
@Aspect
@Component
public class MyAspect {
    @Before("execution(public void org.ragdollcat.aop.aspect.UserService.originalMethod())")
    public void before(JoinPoint joinPoint) {
        System.out.println("before method");
    }
}
@Component
public class UserService {
    public void originalMethod() {
        System.out.println("UserService originalMethod");
    }
}
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService bean = (UserService) context.getBean("userService");
        bean.originalMethod();
    }
}

  会在refresh的invokeBeanFactoryPostProcessors这一步,解析AppConfig 配置类,扫描到了@EnableAspectJAutoProxy注解,就会向
beanDefinitionMap中存放一个AnnotationAwareAspectJAutoProxyCreator类型的bean后处理器。
在这里插入图片描述在这里插入图片描述  最终在bean生命周期的初始化后这一步
在这里插入图片描述  执行AbstractAutoProxyCreatorpostProcessAfterInitialization方法:
在这里插入图片描述
在这里插入图片描述

一、wrapIfNecessary

  最终通过AbstractAutoProxyCreatorpostProcessAfterInitialization方法,会进入wrapIfNecessary方法,在AbstractAutoProxyCreator中,有两个重要的属性:

  • targetSourcedBeans是一个set集合,用于存储不需要代理的 Bean 名称。
  • advisedBeans 是一个Map,记录了哪些bean需要代理,哪些不需要代理,value存放了布尔值,表示该bean是否应该被代理。

  这两个属性都起到缓存的作用。

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { 
    // 如果 beanName 非空且 targetSourcedBeans 集合中包含该 beanName,则直接返回 bean,不进行代理
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { 
        return bean;
    }

    // 如果 cacheKey 对应的 bean 在 advisedBeans 中已明确标记为不需要代理,则直接返回 bean
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { 
        return bean;
    }

    // 如果该 bean 是基础设施类(比如加上了@Component注解的自定义advice类)或者应跳过代理(shouldSkip 方法返回 true),
    // 则将其在 advisedBeans 中标记为不需要代理,并直接返回 bean
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { 
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

    // 1.1、getAdvicesAndAdvisorsForBean 获取该 bean 适用的增强(Advice)或拦截器(Advisor)
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

    // 如果返回的拦截器数组不是 DO_NOT_PROXY(表示需要代理),则创建代理对象
    if (specificInterceptors != DO_NOT_PROXY) { 
        // 在 advisedBeans 记录该 bean 需要代理
        this.advisedBeans.put(cacheKey, Boolean.TRUE);

        // 1.2、createProxy 创建代理对象
        Object proxy = createProxy(
                bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));

        // 进行缓存,方便后续使用
        this.proxyTypes.put(cacheKey, proxy.getClass());

        // 返回代理对象
        return proxy;
    }

    // 如果不需要代理,则记录该 bean 不需要代理,并返回原始 bean
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

1.1、getAdvicesAndAdvisorsForBean

  getAdvicesAndAdvisorsForBean最终会进入findEligibleAdvisors方法,在该方法中,做了四件事,主要是用于判断,当前的bean是否需要aop

  • 获取所有候选的 Advisor(增强逻辑),即当前 Spring 容器中所有可用的通知器。
  • 从所有候选 Advisor(candidateAdvisors)中筛选出适用于当前 beanClass 的 Advisor。
  • 对筛选出的 Advisor 进行扩展处理。
  • 如果有可用的 Advisor,则进行排序。
    在这里插入图片描述

1.1.1、findCandidateAdvisors

  在findCandidateAdvisors中,也做了两件事,分别是调用父类findCandidateAdvisors()方法,获取 Spring 机制自动发现的 Advisor,以及调用 buildAspectJAdvisors() 解析所有 @Aspect 类。
在这里插入图片描述  如果我们像前言中那样手动注册了Advisor,则会走父类的逻辑,找到所有类型为Advisor的自定义切面。
在这里插入图片描述  否则使用了 @Aspect 注解,就会走后续的逻辑,先是从容器中获取所有的bean,然后判断哪些bean上加入了@Aspect注解,在getAdvisors方法中真正进行解析:
在这里插入图片描述  ReflectiveAspectJAdvisorFactorygetAdvisors,会找到标注了@Aspect类中,没有加@PointCut的方法,然后循环这些方法,解析成切面:
在这里插入图片描述  最终包装成InstantiationModelAwarePointcutAdvisorImpl对象返回:
在这里插入图片描述  InstantiationModelAwarePointcutAdvisorImplAdvisor的子类
在这里插入图片描述

1.1.2、findAdvisorsThatCanApply

  这一段代码的重点在于canApply方法,会判断是否有匹配的切点。
在这里插入图片描述

// 该方法用于判断一个给定的 Pointcut 是否适用于目标类
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
    // 确保 Pointcut 对象不为 null,若为 null 则抛出异常
    Assert.notNull(pc, "Pointcut must not be null");

    // 如果目标类不匹配 Pointcut 的类过滤器,则直接返回 false
    if (!pc.getClassFilter().matches(targetClass)) {
        return false;
    }

    // 获取与 Pointcut 相关联的方法匹配器
    MethodMatcher methodMatcher = pc.getMethodMatcher();

    // 如果方法匹配器是 "TRUE",表示所有方法都可以匹配,直接返回 true
    if (methodMatcher == MethodMatcher.TRUE) {
        return true;
    }

    // 如果方法匹配器是 IntroductionAwareMethodMatcher 类型,则强制转换为该类型
    IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
    if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
        introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
    }

    // 使用 LinkedHashSet 存储目标类和它的接口,避免重复
    Set<Class<?>> classes = new LinkedHashSet<>();

    // 如果目标类不是代理类,则添加目标类的用户类(去除代理的类)
    if (!Proxy.isProxyClass(targetClass)) {
        classes.add(ClassUtils.getUserClass(targetClass));
    }

    // 将目标类的所有接口也加入到类集合中
    classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

    // 遍历类集合中的每一个类,检查该类中每个方法是否匹配 Pointcut
    for (Class<?> clazz : classes) {
        // 获取该类的所有的方法
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
        
        // 遍历方法,检查每个方法是否匹配 Pointcut 的方法匹配器
        for (Method method : methods) {
            // 如果使用的是 IntroductionAwareMethodMatcher,则使用其匹配方法,否则使用普通的 methodMatcher 匹配
            if (introductionAwareMethodMatcher != null ? 
                introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : 
                methodMatcher.matches(method, targetClass)) {
                
                // 如果匹配成功,返回 true
                return true;
            }
        }
    }

    // 如果没有方法匹配,则返回 false
    return false;
}

1.2、createProxy

  createProxy 方法的作用是创建代理对象,底层使用的也是proxyFactory:
在这里插入图片描述  最终调用的也是proxyFactorygetProxy方法,在该方法中,主要做了两件事:

  • 代理模式的选择
  • 根据选择的代理模式,创建代理对象

在这里插入图片描述

  • 如果当前的jvm是GraalVM,就直接用jdk动态代理。
  • 如果当前的jvm不是GraalVM,并且下面三个条件满足其一,就会再次进行判断:启用优化(cglib的性能某些情况要优于jdk动态代理)或isProxyTargetClass为true或没有为目标类提供用户自定义的代理接口。
    • 如果目标类是接口或目标类是代理类或目标类是 Lambda 类,还是会使用jdk动态代理。
    • 否则就会走cglib动态代理

在这里插入图片描述  我的案例中,使用的是jdk动态代理,在进行了代理的选择后
在这里插入图片描述  就会根据jdk和cglib不同的方式去创建代理对象:
在这里插入图片描述  以jdk动态代理为例:
在这里插入图片描述

二、invoke

  invoke方法是在调用代理类目标方法时执行的逻辑。
  在invoke方法中首先会进行判断,如果当前方法是equals或者hashcode,就不会进行代理,进到invoke是以被代理类中单个方法的维度。
在这里插入图片描述  并且@EnableAspectJAutoProxyexposeProxy属性为true时(默认为false),就会把当前的代理放入ThreadLocal中。(解决事务自调用失效)
在这里插入图片描述  然后会获取方法的拦截器链:
在这里插入图片描述  最后会进行判断,如果拦截器链为空,说明没有适合该方法的切面,则不需要AOP,直接调用目标方法,否则会按照顺序调用拦截器链:
在这里插入图片描述

2.1、getInterceptorsAndDynamicInterceptionAdvice

  获取方法的拦截器链,最终会调用DefaultAdvisorChainFactorygetInterceptorsAndDynamicInterceptionAdvice方法,这一步会拿到所有的advisor
在这里插入图片描述  如果定义的是advice,也会被封装成advisor:
在这里插入图片描述  接着会去遍历所有的advisor,关键性的代码:

if (advisor instanceof PointcutAdvisor) {
	// Add it conditionally.
	PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
	//先匹配类,可以重写Pointcut接口的getClassFilter方法,自定义匹配的逻辑 比如根据类名匹配
	if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
		//再匹配方法,可以重写Pointcut接口的getMethodMatcher方法,自定义匹配的逻辑 
		MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
		boolean match;
		if (mm instanceof IntroductionAwareMethodMatcher) {
			if (hasIntroductions == null) {
				hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
			}
			match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
		}
		else {
			//matches如果没有自定义,默认是true
			match = mm.matches(method, actualClass);
		}
		if (match) {
			//2.1.1、getInterceptors	将所有的advisor转换为MethodInterceptor对象
			MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
			//如果方法匹配器的.isRuntime属性设置成了true,就会对所有匹配的方法,再进行参数的筛选。
			if (mm.isRuntime()) {
				// Creating a new object instance in the getInterceptors() method
				// isn't a problem as we normally cache created chains.
				for (MethodInterceptor interceptor : interceptors) {
					interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
				}
			}
			else {
				interceptorList.addAll(Arrays.asList(interceptors));
			}
		}
	}
}

2.1.1、getInterceptors

  getInterceptors是将advisor封装成MethodInterceptor的逻辑,在这一段代码中,运用了适配器模式

public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
	List<MethodInterceptor> interceptors = new ArrayList<>(3);
	Advice advice = advisor.getAdvice();
	//如果advice 的类型本来就属于 MethodInterceptor
	if (advice instanceof MethodInterceptor) {
		//直接添加进集合
		interceptors.add((MethodInterceptor) advice);
	}
	//遍历所有的适配器
	for (AdvisorAdapter adapter : this.adapters) {
		//如果某一个适配器和当前的advice类型匹配
		if (adapter.supportsAdvice(advice)) {
			//就由该适配器进行类型转换
			interceptors.add(adapter.getInterceptor(advisor));
		}
	}
	if (interceptors.isEmpty()) {
		throw new UnknownAdviceTypeException(advisor.getAdvice());
	}
	return interceptors.toArray(new MethodInterceptor[0]);
}

  在构造DefaultAdvisorAdapterRegistry时,默认添加了三个适配器:
在这里插入图片描述  适配器类
在这里插入图片描述  某一个具体子类,实现了适配器类:
在这里插入图片描述

2.2、proceed

  proceed是执行拦截器链以及目标方法的逻辑,有两个实现,目前主要看ReflectiveMethodInvocation的实现:
在这里插入图片描述
  在该类中有一个重要的属性,初始值是-1,会根据该属性的值去判断是否应该执行目标方法:
在这里插入图片描述  proceed方法中,主要对于InterceptorAndDynamicMethodMatcher 类型的拦截器进行了判断,如果拦截器是该类型,说明需要对于方法的参数也进行匹配。

public Object proceed() throws Throwable { // proceed 方法用于执行拦截器链中的下一个拦截器,或最终执行目标方法
    // 当拦截器链中的所有拦截器都执行完毕时(即当前索引等于最后一个拦截器的索引),执行目标方法
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 通过反射调用目标方法,相当于 method.invoke(target, args);
        return invokeJoinpoint();
    }

    // 从拦截器链中取出下一个拦截器(或拦截器 + 动态方法匹配器)
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

    // 判断当前拦截器是否是 InterceptorAndDynamicMethodMatcher 类型
    //InterceptorAndDynamicMethodMatcher 类型的拦截器,支持对于方法的参数进行匹配
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // 强制类型转换,将拦截器对象转换为 InterceptorAndDynamicMethodMatcher
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        
        // 确定目标类,如果 targetClass 为空,则使用 method 所在的类
        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 {
            // 如果方法不匹配,则跳过该拦截器,继续执行下一个拦截器
            return proceed();
        }
    } else {
        // 如果拦截器不是 InterceptorAndDynamicMethodMatcher 类型,则直接执行其 invoke 方法
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

  在invoke方法中,实际调用的就是各自切面的逻辑,假设我的切面类如下:

@Aspect
@Component
public class MyAspect {


    @Before("execution(public void org.ragdollcat.aop.aspect.UserService.originalMethod())")
    public void before(JoinPoint joinPoint) {
        System.out.println("before method");
    }

    @Around("execution(public void org.ragdollcat.aop.aspect.UserService.originalMethod())")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around - before method");
        Object proceed = pjp.proceed();
        System.out.println("around - after method");
        return proceed;
    }
    

    @After("execution(public void org.ragdollcat.aop.aspect.UserService.originalMethod())")
    public void after() {
        System.out.println("after method");
    }
}

  那么执行切面的顺序是:
在这里插入图片描述

2.2.1、invoke

  这里的invoke最终调用的是各个切面的invoke方法
  基于基于 Spring AOP代理方式的拦截器:

  • AfterReturningAdviceInterceptor:在目标方法正常返回后执行的拦截器(不包括异常情况),需要实现AfterReturningAdvice接口。相当于注解模式下的@AfterReturning 和@After注解
  • MethodBeforeAdviceInterceptor:在目标方法执行前执行的拦截器,需要实现MethodBeforeAdvice接口。相当于注解模式下的@Before注解
  • ThrowsAdviceInterceptor:在目标方法抛出异常后执行的拦截器。需要实现ThrowsAdvice接口。相当于注解模式下@AfterThrowing注解
  • MethodInterceptor:目标方法执行前、执行后,相当于注解模式下的@Around注解

  是基于 AspectJ 注解方式的拦截器:

  • AspectJAfterAdvice:在目标方法执行后触发,不管方法是否抛出异常,对应@After注解。
  • AspectJMethodBeforeAdvice:在目标方法执行前触发,对应@Before 注解
  • AspectJAroundAdvice:在目标方法执行前、执行后、异常时都可以进行拦截,相当于 @Before + @AfterReturning + @AfterThrowing 组合,对应@Around注解。
  • AspectJAfterThrowingAdvice:异常通知拦截器,在目标方法抛出异常后触发。对应@AfterThrowing注解。
  • AspectJAfterReturningAdvice:在目标方法正常返回后执行的拦截器(不包括异常情况),对应@AfterReturning注解。

  可以简单的看一下其中的invoke逻辑:
在这里插入图片描述前置增强,先执行前置增强逻辑,然后继续调用链

在这里插入图片描述后置增强,先执行调用链,最终再执行后置增强逻辑

三、@AspectJ模式下注解的解析

  在@AspectJ模式下,对于注解的解析,最终会调用到ReflectiveAspectJAdvisorFactorygetAdvice方法,其中有对于每个注解的解析逻辑:
在这里插入图片描述  最终也是将其包装成不同的切面,然后会统一再次封装成InstantiationModelAwarePointcutAdvisorImpl对象,作为该对象的instantiatedAdvice属性。
在这里插入图片描述  并且在2.1.1、getInterceptors中,会对其进行二次适配,目的是将不同类型的 Advice 转换为 MethodInterceptor
在这里插入图片描述
在这里插入图片描述

Advice 类型作用适配的 AdvisorAdapter转换后的 MethodInterceptor
@Before方法执行前MethodBeforeAdviceAdapterMethodBeforeAdviceInterceptor
@AfterReturning方法正常返回后AfterReturningAdviceAdapterAfterReturningAdviceInterceptor
@AfterThrowing方法抛出异常后ThrowsAdviceAdapterThrowsAdviceInterceptor
@After方法执行完成(无论是否异常)AspectJAfterAdvice直接实现了 MethodInterceptor
@Around方法执行前后AspectJAroundAdvice直接实现了 MethodInterceptor

总结

  Spring的AOP,分为了两部分:
  第一部分是@EnableAspectJAutoProxy向Spring容器中添加的AnnotationAwareAspectJAutoProxyCreator后置处理器的调用(在bean生命周期的初始化后调用),主要完成了:

  • 获取并解析切面
    1. 获取所有候选的 Advisor(增强逻辑),即当前 Spring 容器中所有可用的通知器。
      • 对于Spring AOP API注册的切面的适配。
      • 对于AspectJ 注解方式的解析。
    2. 从所有候选 Advisor(candidateAdvisors)中筛选出适用于当前 beanClass 的 Advisor。
    3. 对筛选出的 Advisor 进行扩展处理。
    4. 如果有可用的 Advisor,则进行排序。
  • 构建代理
    1. 选择代理模式
    2. 创建代理对象

  第二部分是执行目标方法,代理类的invoke方法的调用:

  1. 获取到与方法匹配的拦截器链,这里会再次进行切面匹配:
    • 匹配类
    • 匹配方法
    • 特殊情况匹配方法的参数
  2. 目标方法的调用(递归)

相关文章:

  • 正则表达式梳理(基于python)
  • SPI驱动(二) -- SPI驱动程序模型
  • #UVM# 关于field automation机制中的 pack_bytes 和unpack_bytes 函数剖析
  • SpringBoot为什么默认使用CGLIB?
  • 大型语言模型演变之路:从Transformer到DeepSeek-R1
  • 【量化策略】波动率突破策略
  • 大白话html第十章前沿的网页开发技术
  • (二 十 二)趣学设计模式 之 备忘录模式!
  • ThreadLocal---java
  • 016.3月夏令营:数理类
  • Redis数据结构——list
  • Cpu100%问题处理(包括-线上docker服务)
  • 从17款IT项目管理系统中挑选合适的工具
  • 【练习】【二叉树】力扣热题100 102. 二叉树的层序遍历
  • PHP之Cookie和Session
  • Java 大视界 -- Java 大数据在智慧交通信号灯智能控制中的应用(116)
  • 为解决局域网IP、DNS切换的Windows BAT脚本
  • jupyter notebook更改文件存储路径
  • 多线程-锁升级和对象的内存布局
  • [自动驾驶-传感器融合] 多激光雷达的外参标定
  • 域名服务器ip/网站优化排名提升
  • 金山网站制作/电脑培训零基础培训班
  • 政府网站开发 扬州/网络广告的发布方式包括
  • 建设一个微信小说网站/seo网站系统
  • 学编程学哪一种比较好/seo管理是什么
  • 网站正在建设中 给你带来/百度seo外链推广教程