Spring声明式事务原理

Spring声明式事务原理,第1张

本文我们将通过一个简单的例子回顾Spring声明式事务的使用,并通过源码解读内部实现原理,最后通过列举一些常见事务不生效的场景来加深对Spring事务原理的理解。

1. 案例

新建SpringBoot项目,然后引入如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

这里引入了JDBC、MySQL驱动和mybatis等依赖。

然后在Spring入口类上加上@EnableTransactionManagement注解,以开启事务:

@SpringBootApplication
@EnableTransactionManagement
public class SpringTransactionDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringTransactionDemoApplication.class, args);
    }

}

接着新建名称为demo的MySQL数据库,并创建USER表:

CREATE TABLE `USER` (
  `USER_ID` varchar(10) NOT NULL COMMENT '用户ID',
  `USERNAME` varchar(10) DEFAULT NULL COMMENT '用户名',
  `AGE` varchar(3) DEFAULT NULL COMMENT '用户年龄',
  PRIMARY KEY (`USER_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

其中USER_ID字段非空。

在application.properties配置中添加数据库相关配置:

# 应用名称
spring.application.name=spring-transaction-demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8

创建USER表对应实体类User:(嫌麻烦直接用MP生成即可…)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private String userId;
    
    private String username;
    
    private String age;
    
}

创建UserMapper:

@Mapper
public interface UserMapper {

    @Insert("insert into user(user_id,username,age) values(#{userId},#{username},#{age})")
    void save(User user);
}

包含一个新增用户的方法save。

创建Service接口UserService:

public interface UserService {

    void saveUser(User user);

}

其实现类UserServiceImpl:

@Service
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;

    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Transactional
    @Override
    public void saveUser(User user) {
        userMapper.save(user);
        // 测试事务回滚
        if (!StringUtils.hasText(user.getUsername())) {
            throw new RuntimeException("username不能为空");
        }
    }
}

我们直接在SpringBoot的入口类中测试即可:

@SpringBootApplication
@EnableTransactionManagement
public class SpringTransactionDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringTransactionDemoApplication.class, args);
        UserService userService = context.getBean(UserService.class);
        User user = new User("1", null, "18");
        userService.saveUser(user);
    }

}

如果事务生效的话,这条数据将不会被插入到数据库中,运行程序后,查看库表:

可以看到数据并没有被插入,说明事务控制成功。

我们注释掉UserServiceImpl的saveUser方法上的@Transactional注解,重新运行程序,查看库表:

可以看到数据被插入到数据库中了,事务控制失效。

2. 事务原理 2.1 @EnableTransactionManagement

上面例子中,我们通过模块驱动注解@EnableTransactionManagement开启了事务管理功能,查看其源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({TransactionManagementConfigurationSelector.class})
public @interface EnableTransactionManagement {
    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;

    int order() default 2147483647;
}

从源码可以看出通过@Import注解引入了TransactionManagementConfigurationSelector,同时默认为PROXY模式。

接着查看TransactionManagementConfigurationSelector的源码:

public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
    public TransactionManagementConfigurationSelector() {
    }

    protected String[] selectImports(AdviceMode adviceMode) {
        switch(adviceMode) {
        case PROXY:
            return new String[]{AutoProxyRegistrar.class.getName(), ProxyTransactionManagementConfiguration.class.getName()};
        case ASPECTJ:
            return new String[]{this.determineTransactionAspectClass()};
        default:
            return null;
        }
    }

    private String determineTransactionAspectClass() {
        return ClassUtils.isPresent("javax.transaction.Transactional", this.getClass().getClassLoader()) ? "org.springframework.transaction.aspectj.AspectJJtaTransactionManagementConfiguration" : "org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration";
    }
}

我们只需要关注PROXY模式的那段代码即可。他让容器里面注册了AutoProxyRegistrarProxyTransactionManagementConfiguration

所以接下来我们重点关注AutoProxyRegistrar和ProxyTransactionManagementConfiguration的逻辑即可。

2.2 AutoProxyRegistrar

查看AutoProxyRegistrar的源码:

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    private final Log logger = LogFactory.getLog(this.getClass());

    public AutoProxyRegistrar() {
    }

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        boolean candidateFound = false;
        Set<String> annTypes = importingClassMetadata.getAnnotationTypes();
        Iterator var5 = annTypes.iterator();

        while(var5.hasNext()) {
            String annType = (String)var5.next();
            AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
            if (candidate != null) {
                Object mode = candidate.get("mode");
                Object proxyTargetClass = candidate.get("proxyTargetClass");
                if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() && Boolean.class == proxyTargetClass.getClass()) {
                    candidateFound = true;
                    if (mode == AdviceMode.PROXY) {
                        AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                        if ((Boolean)proxyTargetClass) {
                            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                            return;
                        }
                    }
                }
            }
        }

        if (!candidateFound && this.logger.isInfoEnabled()) {
            String name = this.getClass().getSimpleName();
            this.logger.info(String.format("%s was imported but no annotations were found having both 'mode' and 'proxyTargetClass' attributes of type AdviceMode and boolean respectively. This means that auto proxy creator registration and configuration may not have occurred as intended, and components may not be proxied as expected. Check to ensure that %s has been @Import'ed on the same class where these annotations are declared; otherwise remove the import of %s altogether.", name, name, name));
        }

    }
}

首先它实现了ImportBeanDefinitionRegistrar接口,所以该类的作用为往IOC容器中导入某个组件,具体查看registerBeanDefinitions方法。

由于我们已经知道了它是PROXY模式,所以可以发现它是通过AopConfigUtilsregisterAutoProxyCreatorIfNecessary方法向容器中注册组件。

查看AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry)源码:

大家可以发现注册的类型InfrastructureAdvisorAutoProxyCreator,我们查看它的相关类图:

从类图可以大致推断出InfrastructureAdvisorAutoProxyCreator的作用为:为目标Service创建代理对象,增强目标Service方法,用于事务控制。

2.3 ProxyTransactionManagementConfiguration

查看ProxyTransactionManagementConfiguration源码:

@Configuration(
    proxyBeanMethods = false
)
@Role(2)
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {
    public ProxyTransactionManagementConfiguration() {
    }

    @Bean(
        name = {"org.springframework.transaction.config.internalTransactionAdvisor"}
    )
    @Role(2)
    public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {
        BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
        advisor.setTransactionAttributeSource(transactionAttributeSource);
        advisor.setAdvice(transactionInterceptor);
        if (this.enableTx != null) {
            advisor.setOrder((Integer)this.enableTx.getNumber("order"));
        }

        return advisor;
    }

    @Bean
    @Role(2)
    public TransactionAttributeSource transactionAttributeSource() {
        return new AnnotationTransactionAttributeSource();
    }

    @Bean
    @Role(2)
    public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionAttributeSource(transactionAttributeSource);
        if (this.txManager != null) {
            interceptor.setTransactionManager(this.txManager);
        }

        return interceptor;
    }
}

从源码中可以看出注册了三个组件。

  • 注册BeanFactoryTransactionAttributeSourceAdvisor增强器,该增强器需要如下两个Bean:

    • TransactionAttributeSource
    • TransactionInterceptor
  • 注册TransactionAttributeSource:

    方法体内部创建了一个类型为AnnotationTransactionAttributeSource的Bean,查看其源码:

    我们可以发现他是一个无参构造,然后由于环境中没有jta和ejb相关依赖,所以直接创建SpringTransactionAnnotationParser

    查看SpringTransactionAnnotationParser源码:

    可以看到这个类的作用为解析@Transactional注解上的各个属性值,并将其进行包装。

  • 注册TransactionInterceptor事务拦截器:

    查看TransactionInterceptor源码,其实现了MethodInterceptor方法拦截器接口,在学习AOP的时候大家应该知道,MethodBeforeAdviceInterceptor、AspectJAfterAdvice、AfterReturningAdviceInterceptor和AspectJAfterThrowingAdvice等增强器都是MethodInterceptor的实现类,目标方法执行的时候,对应拦截器的invoke方法会被执行,所以重点关注TransactionInterceptor实现的invoke方法:

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

    查看invokeWithinTransaction方法源码:

    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, TransactionAspectSupport.InvocationCallback invocation) throws Throwable {
        // 获取目标方法注解属性
        TransactionAttributeSource tas = this.getTransactionAttributeSource();
        TransactionAttribute txAttr = tas != null ? tas.getTransactionAttribute(method, targetClass) : null;
        TransactionManager tm = this.determineTransactionManager(txAttr);
        // 忽略和reactive相关的逻辑
        if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
            TransactionAspectSupport.ReactiveTransactionSupport txSupport = (TransactionAspectSupport.ReactiveTransactionSupport)this.transactionSupportCache.computeIfAbsent(method, (key) -> {
                if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && TransactionAspectSupport.KotlinDelegate.isSuspend(method)) {
                    throw new TransactionUsageException("Unsupported annotated transaction on suspending function detected: " + method + ". Use TransactionalOperator.transactional extensions instead.");
                } else {
                    ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
                    if (adapter == null) {
                        throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " + method.getReturnType());
                    } else {
                        return new TransactionAspectSupport.ReactiveTransactionSupport(adapter);
                    }
                }
            });
            return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, (ReactiveTransactionManager)tm);
        } else {
            //获取事务管理器
            PlatformTransactionManager ptm = this.asPlatformTransactionManager(tm);
            String joinpointIdentification = this.methodIdentification(method, targetClass, txAttr);
            //这里应该获取JDBC的事务管理器,所以看else
            if (txAttr != null && ptm instanceof CallbackPreferringPlatformTransactionManager) {
                TransactionAspectSupport.ThrowableHolder throwableHolder = new TransactionAspectSupport.ThrowableHolder();
    
                Object result;
                try {
                    result = ((CallbackPreferringPlatformTransactionManager)ptm).execute(txAttr, (statusx) -> {
                        TransactionAspectSupport.TransactionInfo txInfo = this.prepareTransactionInfo(ptm, txAttr, joinpointIdentification, statusx);
    
                        Object var9;
                        try {
                            Object retVal = invocation.proceedWithInvocation();
                            if (retVal != null && vavrPresent && TransactionAspectSupport.VavrDelegate.isVavrTry(retVal)) {
                                retVal = TransactionAspectSupport.VavrDelegate.evaluateTryFailure(retVal, txAttr, statusx);
                            }
    
                            var9 = retVal;
                            return var9;
                        } catch (Throwable var13) {
                            if (txAttr.rollbackOn(var13)) {
                                if (var13 instanceof RuntimeException) {
                                    throw (RuntimeException)var13;
                                }
    
                                throw new TransactionAspectSupport.ThrowableHolderException(var13);
                            }
    
                            throwableHolder.throwable = var13;
                            var9 = null;
                        } finally {
                            this.cleanupTransactionInfo(txInfo);
                        }
    
                        return var9;
                    });
                } catch (TransactionAspectSupport.ThrowableHolderException var20) {
                    throw var20.getCause();
                } catch (TransactionSystemException var21) {
                    if (throwableHolder.throwable != null) {
                        this.logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                        var21.initApplicationException(throwableHolder.throwable);
                    }
    
                    throw var21;
                } catch (Throwable var22) {
                    if (throwableHolder.throwable != null) {
                        this.logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                    }
    
                    throw var22;
                }
    
                if (throwableHolder.throwable != null) {
                    throw throwableHolder.throwable;
                } else {
                    return result;
                }
            } else {
                //创建事务信息
                TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
    
                Object retVal;
                try {
                    //通过环绕通知的方式增加目标方法,这里用反射来执行的
                    retVal = invocation.proceedWithInvocation();
                } catch (Throwable var18) {
                    //目标方法执行出错就执行该方法
                    this.completeTransactionAfterThrowing(txInfo, var18);
                    throw var18;
                } finally {
                    //最终清除事务信息,回收内存
                    this.cleanupTransactionInfo(txInfo);
                }
    
                if (retVal != null && vavrPresent && TransactionAspectSupport.VavrDelegate.isVavrTry(retVal)) {
                    TransactionStatus status = txInfo.getTransactionStatus();
                    if (status != null && txAttr != null) {
                        retVal = TransactionAspectSupport.VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                    }
                }
    
                //目标方法执行不出错则执行该方法
                this.commitTransactionAfterReturning(txInfo);
                return retVal;
            }
        }
    }
    

    completeTransactionAfterThrowing源码如下:

    protected void completeTransactionAfterThrowing(@Nullable TransactionAspectSupport.TransactionInfo txInfo, Throwable ex) {
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex);
            }
            //目标方法注解属性不为空的时候,并且方法异常在指定回滚范围内则执行if
            if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    //获取事务管理器,执行回滚
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
                } catch (TransactionSystemException var6) {
                    this.logger.error("Application exception overridden by rollback exception", ex);
                    var6.initApplicationException(ex);
                    throw var6;
                } catch (Error | RuntimeException var7) {
                    this.logger.error("Application exception overridden by rollback exception", ex);
                    throw var7;
                }
            } else {
                try {
                    //否则直接提交事务	
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                } catch (TransactionSystemException var4) {
                    this.logger.error("Application exception overridden by commit exception", ex);
                    var4.initApplicationException(ex);
                    throw var4;
                } catch (Error | RuntimeException var5) {
                    this.logger.error("Application exception overridden by commit exception", ex);
                    throw var5;
                }
            }
        }
    
    }
    

    假如没有在@Transactional注解上指定回滚的异常类型的话,默认只对RunTimeExcetion和Error类型异常进行回滚。

    commitTransactionAfterReturning源码如下:

    protected void commitTransactionAfterReturning(@Nullable TransactionAspectSupport.TransactionInfo txInfo) {
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
            }
            // 通过事务管理器执行commit提交事务
            txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
        }
    
    }
    
3. 事务不生效场景

对Spring事务机制不熟悉的开发者经常会遇到事务不生效的场景,这里列举两个最为常见的场景,并给出对应的解决方案。

3.1 异常类型

Service方法抛出的异常不是RuntimeException或者Error类型,并且@Transactional注解上没有指定回滚异常类型。

对应的代码例子为:

@Service
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;

    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Transactional
    @Override
    public void saveUser(User user) throws Exception {
        userMapper.save(user);
        // 测试事务回滚
        if (!StringUtils.hasText(user.getUsername())) {
            throw new Exception("username不能为空");
        }
    }
}

这冲情况下,Spring并不会进行事务回滚 *** 作。

默认情况下,Spring事务只对RuntimeException或者Error类型异常(错误)进行回滚,检查异常(通常为业务类异常)不会导致事务回滚。

所以要解决上面这个事务不生效的问题,我们主要有以下两种方式:

  1. 手动在@Transactional注解上声明回滚的异常类型(方法抛出该异常及其所有子类型异常都能触发事务回滚):

    @Service
    public class UserServiceImpl implements UserService {
    
        private final UserMapper userMapper;
    
        public UserServiceImpl(UserMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        @Transactional(rollbackFor = Exception.class)
        @Override
        public void saveUser(User user) throws Exception {
            userMapper.save(user);
            // 测试事务回滚
            if (!StringUtils.hasText(user.getUsername())) {
                throw new Exception("username不能为空");
            }
        }
    }
    
  2. 方法内手动抛出的检查异常类型改为RuntimeException子类型:

    定义一个自定义异常类型ParamInvalidException:

    public class ParamInvalidException extends RuntimeException{
    
        public ParamInvalidException(String message) {
            super(message);
        }
    }
    

    修改UserServiceImpl的saveUser方法:

    @Service
    public class UserServiceImpl implements UserService {
    
        private final UserMapper userMapper;
    
        public UserServiceImpl(UserMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        @Transactional
        @Override
        public void saveUser(User user) {
            userMapper.save(user);
            // 测试事务回滚
            if (!StringUtils.hasText(user.getUsername())) {
                throw new ParamInvalidException("username不能为空");
            }
        }
    }
    

    这两种方式都能让事务按照我们的预期生效。

3.2 非事务方法通过this调用事务方法

非事务方法直接通过this调用本类事务方法。这种情况也是比较常见的,举个例子,修改UserServiceImpl:

@Service
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;

    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public void saveUserTest(User user) {
        this.saveUser(user);
    }

    @Transactional
    @Override
    public void saveUser(User user) {
        userMapper.save(user);
        // 测试事务回滚
        if (!StringUtils.hasText(user.getUsername())) {
            throw new ParamInvalidException("username不能为空");
        }
    }
}

在UserServiceImpl中,我们新增了saveUserTest方法,(UserService记得也要加)该方法没有使用@Transactional注解标注,为非事务方法,内部直接调用了saveUser事务方法。

在入口类里测试该方法的调用:

@SpringBootApplication
@EnableTransactionManagement
public class SpringTransactionDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringTransactionDemoApplication.class, args);
        UserService userService = context.getBean(UserService.class);
        User user = new User("3", null, "18");
        userService.saveUserTest(user);
    }

}

启动程序,观察数据库数据:

可以看到,事务并没有回滚,数据已经被插入到了数据库中。

这种情况下事务失效的原因为:Spring事务控制使用AOP代理实现,通过对目标对象的代理来增强目标方法。而上面例子直接通过this调用本类的方法的时候,this的指向并非代理类,而是该类本身。

使用debug来验证this是否为代理对象:

这种情况下要让事务生效主要有如下两种解决方式(原理都是使用代理对象来替代this):

  1. 从IOC容器中获取UserService Bean,然后调用它的saveUser方法:
@Service
public class UserServiceImpl implements UserService, ApplicationContextAware {

    private final UserMapper userMapper;
    private ApplicationContext context;

    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public void saveUserTest(User user) {
        UserService userService = context.getBean(UserService.class);
        userService.saveUser(user);
    }

    @Transactional
    @Override
    public void saveUser(User user) {
        userMapper.save(user);
        // 测试事务回滚
        if (!StringUtils.hasText(user.getUsername())) {
            throw new ParamInvalidException("username不能为空");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
}

上面代码我们通过实现ApplicationContextAware接口注入了应用上下文ApplicationContext,然后从中取出UserService Bean来代替this。

  1. 从AOP上下文中取出当前代理对象:

这种情况首先需要引入AOP Starter:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-aopartifactId>
dependency>

然后在SpringBoot入口类中通过注解@EnableAspectJAutoProxy(exposeProxy = true)将当前代理对象暴露到AOP上下文中(通过AopContext的ThreadLocal实现)。

最后在UserServcieImpl的saveUserTest方法中通过AopContext获取UserServce的代理对象:

@Service
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;

    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public void saveUserTest(User user) {
        UserService userService = (UserService) AopContext.currentProxy();
        userService.saveUser(user);
    }

    @Transactional
    @Override
    public void saveUser(User user) {
        userMapper.save(user);
        // 测试事务回滚
        if (!StringUtils.hasText(user.getUsername())) {
            throw new ParamInvalidException("username不能为空");
        }
    }

}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/801500.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-07
下一篇 2022-05-07

发表评论

登录后才能评论

评论列表(0条)

保存