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

spring声明式事务原理01-调用第1层@Transactional方法(事务访问入口)

文章目录

  • 【README】
  • 【步骤1】UserAppService调用userSupport.saveNewUser()
  • 【步骤2】获取到TransactionInterceptor
  • 【步骤3】chain不为空,接着执行CglibMethodInvocation#proceed方法
    • 【补充】AopContext作用
  • 【步骤4】CglibMethodInvocation#proceed方法
  • 【步骤5】调用ReflectiveMethodInvocation#proceed方法(反射方法调用类)
  • 【步骤6】调用 TransactionInterceptor#invoke(事务拦截器#invoke方法)
    • 【补充】TransactionInterceptor-事务拦截器源码
  • 【步骤7】调用TransactionAspectSupport#invokeWithinTransaction方法
  • 【步骤8】根据事务属性txAttr获取事务管理器,tm=JdbcTransactionManager
    • 【补充】事务管理器是什么?
  • 【步骤9】TransactionAspectSupport#invokeWithinTransaction()-在事务中执行业务逻辑(非常重要)
    • 【步骤9.1】按需创建事务-createTransactionIfNecessary()
    • 【补充】TransactionInfo事务信息定义
    • 【补充】TransactionStatus事务状态定义
    • 【步骤9.2】执行目标方法

【README】

1)声明式事务代码样例:

在这里插入图片描述

public interface UserMapper {
    UserPO qryUserById(@Param("id") String id);
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    void insertUser(UserPO userPO);
}

public interface UserAccountMapper {

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    void insertUserAccount(UserAccountPO userAccountPO);
}

【代码解说】

  • 代码调用链路:UserAppService -> userSupport.saveNewUser -> userMapper.insertUser + userAccountMapper.insertUserAccount
  • userSupport.saveNewUser 带有 @Transactional 注解; 第1层(最外层)@Transactional标注的方法
    • userMapper.insertUser 带有 @Transactional 注解; 第2层@Transactional标注的方法1
    • userAccountMapper.insertUserAccount 带有 @Transactional 注解; 第2层@Transactional标注的方法2

2)@Transactional:事务注解,用于定义事务元数据,包括事务管理器名称,事务传播行为,超时时间(单位秒),是否只读,回滚的异常类型;

  • @Transactional可以标注类与方法,不管是标注类还是方法,@Transactional标注所在的类的bean都会被spring通过aop代理进行增强;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    String timeoutString() default "";

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}


【步骤1】UserAppService调用userSupport.saveNewUser()

1)因为 userSupport中的saveNewUser方法被@Transaction标注,所以该bean被spring增强为aop代理,所以访问aop代理的入口方法intercept(),即CglibAopProxy#DynamicAdvisedInterceptor静态内部类的 intercept方法;

在这里插入图片描述



【步骤2】获取到TransactionInterceptor

在这里插入图片描述



【步骤3】chain不为空,接着执行CglibMethodInvocation#proceed方法

chain有一个元素: TransactionInterceptor-事务拦截器;

接着执行 467行:retVal = (new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed();

在这里插入图片描述


【补充】AopContext作用

【补充455行代码】: AopContext实际上是带有ThreadContext的容器对象,用于存储代理对象;

public final class AopContext {
    private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal("Current AOP proxy");

    private AopContext() {
    }

    public static Object currentProxy() throws IllegalStateException {
        Object proxy = currentProxy.get();
        if (proxy == null) {
            throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.");
        } else {
            return proxy;
        }
    }

    @Nullable
    static Object setCurrentProxy(@Nullable Object proxy) {
        Object old = currentProxy.get();
        if (proxy != null) {
            currentProxy.set(proxy);
        } else {
            currentProxy.remove();
        }

        return old;
    }
}


【步骤4】CglibMethodInvocation#proceed方法

CglibMethodInvocation#proceed方法, 即Cglib方法调用类#proceed方法 (proceed=继续或进行或处理)

【CglibAopProxy#CglibMethodInvocation】

private static class CglibMethodInvocation extends ReflectiveMethodInvocation {
    public CglibMethodInvocation(Object proxy, @Nullable Object target, Method method, Object[] arguments, @Nullable Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers, MethodProxy methodProxy) {
        super(proxy, target, method, arguments, targetClass, interceptorsAndDynamicMethodMatchers);
    }

    // 调用proceed 
    @Nullable
    public Object proceed() throws Throwable {
        try {
            return super.proceed();
        } catch (RuntimeException var2) {
            throw var2;
        } catch (Exception var3) {
            if (!ReflectionUtils.declaresException(this.getMethod(), var3.getClass()) && !KotlinDetector.isKotlinType(this.getMethod().getDeclaringClass())) {
                throw new UndeclaredThrowableException(var3);
            } else {
                throw var3;
            }
        }
    }
}


【步骤5】调用ReflectiveMethodInvocation#proceed方法(反射方法调用类)

调用ReflectiveMethodInvocation#proceed方法, ReflectiveMethodInvocation=反射方法调用类

// ReflectiveMethodInvocation=反射方法调用类
// interceptorsAndDynamicMethodMatcher 是一个列表,包含一个元素 TransactionInterceptor 
// currentInterceptorIndex 初始值=-1 
public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return this.invokeJoinpoint();
    } else {
        Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
// interceptorOrInterceptionAdvice 就是 TransactionInterceptor ,而不是 InterceptorAndDynamicMethodMatcher
        if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
            InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;
            Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass();
            return dm.matcher().matches(this.method, targetClass, this.arguments) ? dm.interceptor().invoke(this) : this.proceed();
        } else {
            return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this); // 调用到这里 
        }
    }
}

在这里插入图片描述

获取到的interceptorOrInterceptionAdvice的属性如下:

在这里插入图片描述



【步骤6】调用 TransactionInterceptor#invoke(事务拦截器#invoke方法)

调用 TransactionInterceptor#invoke(ReflectiveMethodInvocation), 传入的入参this是ReflectiveMethodInvocation-反射方法调用对象(包装了 UserSupportImpl#saveNewUser方法的代理对象)

【TransactionInterceptor#invoke】

public Object invoke(MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
    Method var10001 = invocation.getMethod();
    Objects.requireNonNull(invocation);
    return this.invokeWithinTransaction(var10001, targetClass, invocation::proceed);
}

上述 invocation::proceed中的invocation实际是 ReflectiveMethodInvocaion-反射方法调用类,ReflectiveMethodInvocaion封装了代理对象proxy,方法method等属性;

public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {
    protected final Object proxy;
    @Nullable
    protected final Object target;
    protected final Method method;
    protected Object[] arguments;
    @Nullable
    private final Class<?> targetClass;
    @Nullable
    private Map<String, Object> userAttributes;
    protected final List<?> interceptorsAndDynamicMethodMatchers;
    private int currentInterceptorIndex = -1;

    protected ReflectiveMethodInvocation(Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments, @Nullable Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {
        this.proxy = proxy;
        this.target = target;
        this.targetClass = targetClass;
        this.method = BridgeMethodResolver.findBridgedMethod(method);
        this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);
        this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;
    }

【补充】TransactionInterceptor-事务拦截器源码

TransactionInterceptor继承自TransactionAspectSupport;

public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
    public TransactionInterceptor() {
    }

    public TransactionInterceptor(TransactionManager ptm, TransactionAttributeSource tas) {
        this.setTransactionManager(ptm);
        this.setTransactionAttributeSource(tas);
    }
    
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
        Method var10001 = invocation.getMethod();
        Objects.requireNonNull(invocation);
        // 调用到这里, 注意最后一个入参是 invocation::proceed;
        return this.invokeWithinTransaction(var10001, targetClass, invocation::proceed);
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
//...
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
     // ...
    }
}


【步骤7】调用TransactionAspectSupport#invokeWithinTransaction方法

因为TransactionInterceptor 继承自TransactionAspectSupport; 而invokeWithinTransaction方法的最后一个入参invocation就是封装了目标方法,即UserSupportImpl#saveNewUser方法的包装对象ReflectiveMethodInvocation;

在这里插入图片描述

获取到的事务管理器类型是 JdbcTransactionManger, 而事务属性TransactionAttribute保存了注解@Transaction标注的方法的元数据,包括事务传播行为,隔离级别,超时时间,是否只读,事务管理器等属性; 而TransactionAttribute继承自TransactionDefinition-事务定义接口;

【TransactionAttribute】事务属性-TransactionAttribute定义

public interface TransactionAttribute extends TransactionDefinition {
    @Nullable
    String getQualifier();

    Collection<String> getLabels();

    boolean rollbackOn(Throwable ex);
}

【TransactionAspectSupport#invokeWithinTransaction方法】执行到获取PlatformTransactionManager-平台事务管理器;

在这里插入图片描述



【步骤8】根据事务属性txAttr获取事务管理器,tm=JdbcTransactionManager

上述第7步执行到145行,接着判断tm(也就是JdbcTransactionManager)是否是ReactiveTransactionManager类型;

JdbcTransactionManager父类是DataSourceTransactionManager,而DataSourceTransactionManager父类是AbstractPlatformTransactionManager,而AbstractPlatformTransactionManager父类是PlatformTransactionManager;结论:tm(JdbcTransactionManager)不是CallbackPreferringPlatformTransactionManager,所以执行到160行;

在这里插入图片描述

又160行得到的ptm还是JdbcTransactionManager,与tm指向同一个对象,162行判断是否为CallbackPreferringPlatformTransactionManager类型不通过,接着执行第220行;

【补充】事务管理器是什么?

public class JdbcTransactionManager extends DataSourceTransactionManager;

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean;
    
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, ConfigurableTransactionManager, Serializable; 

【DataSourceTransactionManager】数据源事务管理器-方法列表

  • doBegin-开启事务
  • doCommit-提交事务
  • doGetTransaction-获取事务
  • doResume-恢复事务
  • doRollback-回滚事务
  • doSuspend-挂起事务;

在这里插入图片描述

【DataSourceTransactionManager父类AbstractPlatformTransactionManager】抽象平台事务管理器-部分方法列表

  • doBegin 开启事务
  • doCommit 提交
  • doGetTransation 获取事务
  • doResume 恢复事务
  • doSuspend 挂起事务
  • doRollback 回滚
  • setNestedTransactionAllowed -设置允许嵌套事务;


【步骤9】TransactionAspectSupport#invokeWithinTransaction()-在事务中执行业务逻辑(非常重要)

1)TransactionAspectSupport#invokeWithinTransaction()-在事务中执行业务逻辑,具体步骤如下(跳过了分支判断):

  • 步骤9.1:调用createTransactionIfNecessary方法,创建事务;
  • 步骤9.2:调用invocation.proceedWithInvocation(),执行具体业务逻辑(执行目标方法);
    • 步骤9.2.1(或有) : 抛出异常,执行completeTransactionAfterThrowing
  • 步骤9.3:执行完成(无论是否抛出异常),调用cleanupTransactionInfo()
  • 步骤9.4:判断返回值是否不为null 且 事务属性是否不为null
  • 步骤9.5:调用commitTransactionAfterReturning(),提交事务;

【TransactionAspectSupport#invokeWithinTransaction()】 在事务中调用;上游是TransactionInterceptor,而TransactionInterceptor是TransactionAspectSupport的子类; 源码如下。

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
    TransactionAttributeSource tas = this.getTransactionAttributeSource();
    TransactionAttribute txAttr = tas != null ? tas.getTransactionAttribute(method, targetClass) : null;
    TransactionManager tm = this.determineTransactionManager(txAttr);
    if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
        // tm类型是JdbcTransactionManager,所以不会执行到这里
        // ... 
        return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
    } else {
        PlatformTransactionManager ptm = this.asPlatformTransactionManager(tm);
        String joinpointIdentification = this.methodIdentification(method, targetClass, txAttr);
        if (txAttr != null && ptm instanceof CallbackPreferringPlatformTransactionManager cpptm) {
			// ptm类型是JdbcTransactionManager,所以不会执行到这里
            // ...
       
        } else {
            // 程序执行到这里 
            // 1 创建事务,【非常重要】 
            TransactionInfo txInfo = this.createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

            Object retVal;
            try {
                // 2 通过反射执行具体业务逻辑, invocation就是包装了业务方法UserSuuportImpl#saveUser()方法的调用对象 
                retVal = invocation.proceedWithInvocation();
            } catch (Throwable var22) {
                // 2.1 抛出异常 (或有)
                this.completeTransactionAfterThrowing(txInfo, var22);
                throw var22;
            } finally {
                // 3 最后清理事务信息 
                this.cleanupTransactionInfo(txInfo);
            }
			
			// 4 判断返回值是否不为null 且 事务属性是否不为null 
            if (retVal != null && txAttr != null) {
                TransactionStatus status = txInfo.getTransactionStatus();
                if (status != null) {
                    label185: {
                        if (retVal instanceof Future) {
                            // 5 若返回值为 Future类型(异步线程执行结果类)
                            Future<?> future = (Future)retVal;
                            if (future.isDone()) {
                                try {
                                    future.get();
                                } catch (ExecutionException var27) {
                                    if (txAttr.rollbackOn(var27.getCause())) {
                                        status.setRollbackOnly();
                                    }
                                } catch (InterruptedException var28) {
                                    Thread.currentThread().interrupt();
                                }
                                break label185;
                            }
                        }

                        if (vavrPresent && TransactionAspectSupport.VavrDelegate.isVavrTry(retVal)) {
                            retVal = TransactionAspectSupport.VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                        }
                    }
                }
            }
			// 5 提交事务 
            this.commitTransactionAfterReturning(txInfo);
            return retVal;
        }
    }
}


【步骤9.1】按需创建事务-createTransactionIfNecessary()

TransactionAspectSupport#createTransactionIfNecessary()详情参见 spring声明式事务原理02-调用第1层@Transactional方法-按需创建事务createTransactionIfNecessary
在这里插入图片描述根据事务管理器+事务属性+aop连接点(切点)创建事务,如下:

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
    if (txAttr != null && ((TransactionAttribute)txAttr).getName() == null) {
        txAttr = new DelegatingTransactionAttribute((TransactionAttribute)txAttr) {
            public String getName() {
                return joinpointIdentification;
            }
        };
    }

    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            // 通过事务管理器获取事务
            status = tm.getTransaction((TransactionDefinition)txAttr); 
        } else if (this.logger.isDebugEnabled()) {
            this.logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured");
        }
    }
    // 准备事务信息TransactionInfo 
    return this.prepareTransactionInfo(tm, (TransactionAttribute)txAttr, joinpointIdentification, status);
}

在这里插入图片描述


【补充】TransactionInfo事务信息定义

1)TransactionInfo是TransactionAspectSupport的 静态内部类,是一个聚合类,封装了事务相关的多个组件:

  • transactionManager:事务管理器(封装了事务提交,回滚功能)
  • TransactionAttribute:事务属性 (@Transaction注解元数据)
  • joinpointIdentification:切点标识(被@Transaction标注的目标方法的全限定名称)
  • TransactionStatus:事务状态 (封装获取事务状态的方法,如是否只读,是否回滚,是否保存点,事务是否完成,当前线程是否存在事务等)
  • oldTransactionInfo-上一个事务信息对象;

其中 oldTransactionInfo-上一个事务信息对象,是bindToThread()方法与restoreThreadLocalStatus() 需要用到的;

protected static final class TransactionInfo {
    @Nullable
    private final PlatformTransactionManager transactionManager;
    @Nullable
    private final TransactionAttribute transactionAttribute;
    private final String joinpointIdentification;
    @Nullable
    private TransactionStatus transactionStatus;
    @Nullable
    private TransactionInfo oldTransactionInfo;

    public TransactionInfo(@Nullable PlatformTransactionManager transactionManager, @Nullable TransactionAttribute transactionAttribute, String joinpointIdentification) {
        this.transactionManager = transactionManager;
        this.transactionAttribute = transactionAttribute;
        this.joinpointIdentification = joinpointIdentification;
    }

    public PlatformTransactionManager getTransactionManager() {
        Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
        return this.transactionManager;
    }

    @Nullable
    public TransactionAttribute getTransactionAttribute() {
        return this.transactionAttribute;
    }

    public String getJoinpointIdentification() {
        return this.joinpointIdentification;
    }

    public void newTransactionStatus(@Nullable TransactionStatus status) {
        this.transactionStatus = status;
    }

    @Nullable
    public TransactionStatus getTransactionStatus() {
        return this.transactionStatus;
    }

    public boolean hasTransaction() {
        return this.transactionStatus != null;
    }

    private void bindToThread() {
        this.oldTransactionInfo = (TransactionInfo)TransactionAspectSupport.transactionInfoHolder.get();
        TransactionAspectSupport.transactionInfoHolder.set(this);
    }

    private void restoreThreadLocalStatus() {
        TransactionAspectSupport.transactionInfoHolder.set(this.oldTransactionInfo);
    }    
}

// 事务信息持有器(transactionInfoHolder)是一个ThreadLocal对象,
// 用于保存当前事务信息,及上一个事务信息(挂起时),以便恢复上一个事务信息;
private static final ThreadLocal<TransactionInfo> transactionInfoHolder = 
    new NamedThreadLocal("Current aspect-driven transaction");

事务信息持有器(transactionInfoHolder):是一个ThreadLocal对象,用于保存当前事务信息,及上一个事务信息(挂起时),以便恢复上一个事务信息;

【补充】TransactionStatus事务状态定义

1)TransactionStatus继承自 TransactionExecution, SavepointManager

2)TransactionStatus封装了事务状态方法,包括是否有保存点,当前线程是否存在事务,当前线程是否存在新事务,是否只读,设置回滚状态或判断是否回滚,事务是否完成等状态方法;

3)封装的事务方法包括:

  • 保存点相关方法:包括 hasSavepoint , createSavepoint, rollbackToSavepoint, releaseSavepoint
  • 事务相关方法: 包括hasTransaction, isNewTransaction, isReadOnly, setRollbackOnly, isRollbackOnly, isCompleted;
  • 嵌套传播模式相关方法:isNested (嵌套模式底层原理是保存点)
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    default boolean hasSavepoint() {
        return false;
    }

    default void flush() {
    }
}
// 事务执行 
public interface TransactionExecution {
    default String getTransactionName() {
        return "";
    }

    default boolean hasTransaction() {
        return true;
    }

    default boolean isNewTransaction() {
        return true;
    }

    default boolean isNested() {
        return false;
    }

    default boolean isReadOnly() {
        return false;
    }

    default void setRollbackOnly() {
        throw new UnsupportedOperationException("setRollbackOnly not supported");
    }

    default boolean isRollbackOnly() {
        return false;
    }

    default boolean isCompleted() {
        return false;
    }
}
// 保存点管理器
public interface SavepointManager {
    Object createSavepoint() throws TransactionException;

    void rollbackToSavepoint(Object savepoint) throws TransactionException;

    void releaseSavepoint(Object savepoint) throws TransactionException;
}


【步骤9.2】执行目标方法

1)调用 invocation.proceedWithInvocation() 调用目标方法; invocation的类是 CglibMethodInvocation,即cglib代理方法调用类;

在这里插入图片描述

2)invocation.proceedWithInvocation():调用CglibMethodInvocation#proceed() 方法,又CglibMethodInvocation是封装了目标方法的cglib方法调用代理对象,所以接着会调用目标方法,即UserSupportImpl#saveNewUser()方法;

在这里插入图片描述

3)接着调用父类ReflectiveMethodInvocation的proceed()方法;

在这里插入图片描述

4)接着执行ReflectiveMethodInvocation#invokeJoinpoint方法,如下。

在这里插入图片描述

5)接着执行 AopUtils#invokeJoinpointUsingReflection() 方法

其中 originalMethod.invoke(target, args) 是通过反射调用具体的目标方法;

在这里插入图片描述

相关文章:

  • [蓝桥杯]花束搭配【算法赛】
  • Ubuntu从源码安装Webots
  • 网络编程、URI和URL的区别、TCP/IP协议、IP和端口、URLConnection
  • MySQL相关参数
  • 【C++多线程】thread
  • SDL3 游戏开发 Windows 环境搭建
  • 介绍如何使用YOLOv8模型进行基于深度学习的吸烟行为检测
  • Matlab 矢量控制和SVPWM的感应电机控制
  • 算法——图论——关键活动
  • Blender插件NodeWrangler导入贴图报错解决方法
  • Docker生存手册:安装到服务一本通
  • Razor C# 变量
  • 数据结构(全)
  • 机器学习 [白板推导](二)[线性回归]
  • 第四章-PHP文件包含
  • JavaScript性能优化的12种方式
  • 【开原宝藏】30天学会CSS - DAY1 第一课
  • SOA(面向服务架构)与微服务架构的区别与联系
  • GreenKGC: A Lightweight Knowledge Graph Completion Method(论文笔记)
  • 开发一个go模块并在其他项目中引入
  • 建立一个同城网站要怎么做/东莞今天发生的重大新闻
  • 广州专业网站制作公司/销售平台排名
  • 网络营销的主要形式有建设网站/天津搜索引擎优化
  • 虞城网站建设/搜索引擎优化指南
  • 泉州哪里做网站开发/湖南长沙疫情最新消息
  • 高邑做网站/内蒙古seo