• spring成神之路第四十五篇:带你吃透 Spring 事务 7 种传播行为


    本文详解Spring事务中的7种传播行为,还是比较重要的。

    环境

    1. jdk1.8
    2. Spring 5.2.3.RELEASE
    3. mysql5.7

    什么是事务传播行为?

    事务的传播行为用来描述:系统中的一些方法交由spring来管理事务,当这些方法之间出现嵌套调用的时候,事务所表现出来的行为是什么样的?

    比如下面2个类,Service1中的m1方法和Service2中的m2方法上面都有@Transactional注解,说明这2个方法由spring来控制事务。

    但是注意m1中2行代码,先执行了一个insert,然后调用service2中的m2方法,service2中的m2方法也执行了一个insert。

    那么大家觉得这2个insert会在一个事务中运行么?也就是说此时事务的表现行为是什么样的呢?这个就是spring事务的传播行为来控制的事情,不同的传播行为,表现会不一样,可能他们会在一个事务中执行,也可能不会在一个事务中执行,这就需要看传播行为的配置了。

    @Component
    public class Service1 {
        @Autowired
        private Service2 service2;

        @Autowired
        private JdbcTemplate jdbcTemplate;

        @Transactional
        public void m1() {
            this.jdbcTemplate.update("INSERT into t1 values ('m1')");
            this.service2.m2();
        }
    }

    @Component
    public class Service2 {
        @Autowired
        private JdbcTemplate jdbcTemplate;

        @Transactional
        public void m2() {
            this.jdbcTemplate.update("INSERT into t1 values ('m2')");
        }
    }

    如何配置事务传播行为?

    通过@Transactional注解中的propagation属性来指定事务的传播行为

    Propagation propagation() default Propagation.REQUIRED;

    Propagation是个枚举,有7种值,如下:

    事务传播行为类型说明
    REQUIRED 如果当前事务管理器中没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择,是默认的传播行为。
    SUPPORTS 支持当前事务,如果当前事务管理器中没有事务,就以非事务方式执行
    MANDATORY 使用当前的事务,如果当前事务管理器中没有事务,就抛出异常。
    REQUIRES_NEW 新建事务,如果当前事务管理器中存在事务,把当前事务挂起,然后会新建一个事务。
    NOT_SUPPORTED 以非事务方式执行操作,如果当前事务管理器中存在事务,就把当前事务挂起。
    NEVER 以非事务方式执行,如果当前事务管理器中存在事务,则抛出异常。
    NESTED 如果当前事务管理器中存在事务,则在嵌套事务内执行;如果当前事务管理器中没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

    注意:这7种传播行为有个前提,他们的事务管理器是同一个的时候,才会有上面描述中的表现行为。

    下面通过案例对7中表现行为来做说明,在看案例之前,先来回顾几个知识点

    1、Spring声明式事务处理事务的过程

    spring声明式事务是通过事务拦截器TransactionInterceptor拦截目标方法,来实现事务管理的功能的,事务管理器处理过程大致如下:

    1、获取事务管理器
    2、通过事务管理器开启事务
    try{
     3、调用业务方法执行db操作
     4、提交事务
    }catch(RuntimeException | Error){
     5、回滚事务
    }

    2、何时事务会回滚?

    默认情况下,目标方法抛出RuntimeException或者Error的时候,事务会被回滚

    3、Spring事务管理器中的Connection和业务中操作db的Connection如何使用同一个的?

    以DataSourceTransactionManager为事务管理器,操作db使用JdbcTemplate来说明一下。

    创建DataSourceTransactionManager和JdbcTemplate的时候都需要指定dataSource,需要将他俩的dataSource指定为同一个对象。

    当事务管理器开启事务的时候,会通过dataSource.getConnection()方法获取一个db连接connection,然后会将dataSource->connection丢到一个Map中,然后将map放到ThreadLocal中。

    当JdbcTemplate执行sql的时候,以JdbcTemplate.dataSource去上面的ThreadLocal中查找,是否有可用的连接,如果有,就直接拿来用了,否则调用JdbcTemplate.dataSource.getConnection()方法获取一个连接来用。

    所以spring中可以确保事务管理器中的Connection和JdbcTemplate中操作db的Connection是同一个,这样才能确保spring可以控制事务。

    代码验证

    准备db

    DROP DATABASE IF EXISTS javacode2018;
    CREATE DATABASE if NOT EXISTS javacode2018;

    USE javacode2018;
    DROP TABLE IF EXISTS user1;
    CREATE TABLE user1(
      id int PRIMARY KEY AUTO_INCREMENT,
      name varchar(64NOT NULL DEFAULT '' COMMENT '姓名'
    );

    DROP TABLE IF EXISTS user2;
    CREATE TABLE user2(
      id int PRIMARY KEY AUTO_INCREMENT,
      name varchar(64NOT NULL DEFAULT '' COMMENT '姓名'
    );

    spring配置类MainConfig6

    准备JdbcTemplate和事务管理器。

    package com.javacode2018.tx.demo6;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    import javax.sql.DataSource;

    @EnableTransactionManagement //开启spring事务管理功能
    @Configuration //指定当前类是一个spring配置类
    @ComponentScan //开启bean扫描注册
    public class MainConfig6 {
        //定义一个数据源
        @Bean
        public DataSource dataSource() {
            org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
            dataSource.setDriverClassName("com.mysql.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8");
            dataSource.setUsername("root");
            dataSource.setPassword("root123");
            dataSource.setInitialSize(5);
            return dataSource;
        }

        //定义一个JdbcTemplate,用来执行db操作
        @Bean
        public JdbcTemplate jdbcTemplate(DataSource dataSource) {
            return new JdbcTemplate(dataSource);
        }

        //定义我一个事务管理器
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }

    来3个service

    后面的案例中会在这3个service中使用spring的事务来演示效果。

    User1Service

    package com.javacode2018.tx.demo6;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Component;

    @Component
    public class User1Service {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    }

    User2Service

    package com.javacode2018.tx.demo6;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Component;

    @Component
    public class User2Service {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    }

    TxService

    package com.javacode2018.tx.demo6;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;

    @Component
    public class TxService {
        @Autowired
        private User1Service user1Service;
        @Autowired
        private User2Service user2Service;
    }

    测试用例Demo6Test

    before方法会在每个@Test标注的方法之前执行一次,这个方法主要用来做一些准备工作:启动spring容器、清理2个表中的数据;after方法会在每个@Test标注的方法执行完毕之后执行一次,我们在这个里面输出2个表的数据;方便查看的测试用例效果。

    package com.javacode2018.tx.demo6;

    import org.junit.Before;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;

    public class Demo6Test {

        private TxService txService;
        private JdbcTemplate jdbcTemplate;

        //每个@Test用例执行之前先启动一下spring容器,并清理一下user1、user2中的数据
        @Before
        public void before() {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig6.class);
            txService = context.getBean(TxService.class);
            jdbcTemplate = context.getBean(JdbcTemplate.class);
            jdbcTemplate.update("truncate table user1");
            jdbcTemplate.update("truncate table user2");
        }

        @After
        public void after() {
            System.out.println("user1表数据:" + jdbcTemplate.queryForList("SELECT * from user1"));
            System.out.println("user2表数据:" + jdbcTemplate.queryForList("SELECT * from user2"));
        }

    }

    1、REQUIRED

    User1Service

    添加1个方法,事务传播行为:REQUIRED

    @Transactional(propagation = Propagation.REQUIRED)
    public void required(String name) {
        this.jdbcTemplate.update("insert into user1(name) VALUES (?)", name);
    }

    User2Service

    添加2个方法,事务传播行为:REQUIRED,注意第2个方法内部最后一行会抛出一个异常

    @Transactional(propagation = Propagation.REQUIRED)
    public void required(String name) {
        this.jdbcTemplate.update("insert into user1(name) VALUES (?)", name);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void required_exception(String name) {
        this.jdbcTemplate.update("insert into user1(name) VALUES (?)", name);
        throw new RuntimeException();
    }

    场景1(1-1)

    外围方法没有事务,外围方法内部调用2个REQUIRED级别的事务方法。

    案例中都是在TxService的方法中去调用另外2个service,所以TxService中的方法统称外围方法,另外2个service中的方法称内部方法

    验证方法1

    TxService添加
    public void notransaction_exception_required_required() {
        this.user1Service.required("张三");
        this.user2Service.required("李四");
        throw new RuntimeException();
    }
    测试用例,Demo6Test中添加
    @Test
    public void notransaction_exception_required_required() {
        txService.notransaction_exception_required_required();
    }
    运行输出
    user1表数据:[{id=1, name=张三}]
    user2表数据:[{id=1, name=李四}]

    验证方法2

    TxService添加
    public void notransaction_required_required_exception() {
        this.user1Service.required("张三");
        this.user2Service.required_exception("李四");
    }
    测试用例,Demo6Test中添加
    @Test
    public void notransaction_required_required_exception() {
        txService.notransaction_required_required_exception();
    }
    运行输出
    user1表数据:[{id=1, name=张三}]
    user2表数据:[]

    结果分析

    验证方法序号数据库结果结果分析
    1 “张三”、“李四”均插入。 外围方法未开启事务,插入“张三”、“李四”方法在自己的事务中独立运行,外围方法异常不影响内部插入“张三”、“李四”方法独立的事务。
    2 “张三”插入,“李四”未插入。 外围方法没有事务,插入“张三”、“李四”方法都在自己的事务中独立运行,所以插入“李四”方法抛出异常只会回滚插入“李四”方法,插入“张三”方法不受影响。

    结论

    通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

    场景2(1-2)

    外围方法开启事务(Propagation.REQUIRED),这个使用频率特别高。

    验证方法1

    TxService添加
    @Transactional(propagation = Propagation.REQUIRED)
    public void transaction_exception_required_required() {
        user1Service.required("张三");
        user2Service.required("李四");
        throw new RuntimeException();
    }
    测试用例,Demo6Test中添加
    @Test
    public void transaction_exception_required_required() {
        txService.transaction_exception_required_required();
    }
    运行输出
    user1表数据:[]
    user2表数据:[]

    验证方法2

    TxService添加
    @Transactional(propagation = Propagation.REQUIRED)
    public void transaction_required_required_exception() {
        user1Service.required("张三");
        user2Service.required_exception("李四");
    }
    测试用例,Demo6Test中添加
    @Test
    public void transaction_required_required_exception() {
        txService.transaction_required_required_exception();
    }
    运行输出
    user1表数据:[]
    user2表数据:[]

    验证方法3

    TxService添加
    @Transactional(propagation = Propagation.REQUIRED)
    public void transaction_required_required_exception_try() {
        user1Service.required("张三");
        try {
            user2Service.required_exception("李四");
        } catch (Exception e) {
            System.out.println("方法回滚");
        }
    }
    测试用例,Demo6Test中添加
    @Test
    public void transaction_required_required_exception_try() {
        txService.transaction_required_required_exception_try();
    }
    运行输出
    方法回滚
    user1表数据:[]
    user2表数据:[]

    结果分析

    验证方法序号数据库结果结果分析
    1 “张三”、“李四”均未插入。 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚
    2 “张三”、“李四”均未插入。 外围方法开启事务,内部方法加入外围方法事务,内部方法抛出异常回滚,外围方法感知异常致使整体事务回滚
    3 “张三”、“李四”均未插入。 外围方法开启事务,内部方法加入外围方法事务,内部方法抛出异常回滚,即使方法被catch不被外围方法感知,整个事务依然回滚

    结论

    以上试验结果我们证明在外围方法开启事务的情况下Propagation.REQUIRED修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚整个事务均回滚

    2、PROPAGATION_REQUIRES_NEW

    User1Service

    添加1个方法,事务传播行为:REQUIRES_NEW

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requires_new(String name) {
        this.jdbcTemplate.update("insert into user1(name) VALUES (?)", name);
    }

    User2Service

    添加2个方法,事务传播行为:REQUIRES_NEW,注意第2个方法内部最后一行会抛出一个异常

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requires_new(String name) {
        this.jdbcTemplate.update("insert into user2(name) VALUES (?)", name);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requires_new_exception(String name) {
        this.jdbcTemplate.update("insert into user2(name) VALUES (?)", name);
        throw new RuntimeException();
    }

    场景1(2-1)

    外围方法没有事务

    验证方法1

    TxService添加
    public void notransaction_exception_requiresNew_requiresNew(){
        user1Service.requires_new("张三");
        user2Service.requires_new("李四");
        throw new RuntimeException();
    }
    Demo6Test中添加
    @Test
    public void notransaction_exception_requiresNew_requiresNew() {
        txService.notransaction_exception_requiresNew_requiresNew();
    }
    运行输出
    user1表数据:[{id=1, name=张三}]
    user2表数据:[{id=1, name=李四}]

    验证方法2

    TxService添加
    public void notransaction_requiresNew_requiresNew_exception(){
        user1Service.requires_new("张三");
        user2Service.requires_new_exception("李四");
    }
    测试用例,Demo6Test中添加
    @Test
    public void notransaction_requiresNew_requiresNew_exception() {
        txService.notransaction_requiresNew_requiresNew_exception();
    }
    运行输出
    user1表数据:[{id=1, name=张三}]
    user2表数据:[]

    结果分析

    验证方法序号数据库结果结果分析
    1 “张三”插入,“李四”插入。 外围方法没有事务,插入“张三”、“李四”方法都在自己的事务中独立运行,外围方法抛出异常回滚不会影响内部方法。
    2 “张三”插入,“李四”未插入 外围方法没有开启事务,插入“张三”方法和插入“李四”方法分别开启自己的事务,插入“李四”方法抛出异常回滚,其他事务不受影响。

    结论

    通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰

    场景2(2-2)

    外围方法开启事务

    验证方法1

    TxService添加
    @Transactional(propagation = Propagation.REQUIRED)
    public void transaction_exception_required_requiresNew_requiresNew() {
        user1Service.required("张三");

        user2Service.requires_new("李四");

        user2Service.requires_new("王五");
        throw new RuntimeException();
    }
    测试用例,Demo6Test中添加
    @Test
    public void transaction_exception_required_requiresNew_requiresNew() {
        txService.transaction_exception_required_requiresNew_requiresNew();
    }
    运行输出
    user1表数据:[]
    user2表数据:[{id=1, name=李四}, {id=2, name=王五}]

    验证方法2

    TxService添加
    @Transactional(propagation = Propagation.REQUIRED)
    public void transaction_required_requiresNew_requiresNew_exception() {
        user1Service.required("张三");

        user2Service.requires_new("李四");

        user2Service.requires_new_exception("王五");
    }
    Demo6Test中添加
    @Test
    public void transaction_required_requiresNew_requiresNew_exception() {
        txService.transaction_required_requiresNew_requiresNew_exception();
    }
    运行输出
    user1表数据:[]
    user2表数据:[{id=1, name=李四}]

    验证方法3

    TxService添加
    @Transactional(propagation = Propagation.REQUIRED)
    public void transaction_required_requiresNew_requiresNew_exception_try(){
        user1Service.required("张三");

        user2Service.requires_new("李四");

        try {
            user2Service.requires_new_exception("王五");
        } catch (Exception e) {
            System.out.println("回滚");
        }
    }
    Demo6Test中添加
    @Test
    public void transaction_required_requiresNew_requiresNew_exception_try() {
        txService.transaction_required_requiresNew_requiresNew_exception_try();
    }
    运行输出
    回滚
    user1表数据:[{id=1, name=张三}]
    user2表数据:[{id=1, name=李四}]

    结果分析

    验证方法序号数据库结果结果分析
    1 “张三”未插入,“李四”插入,“王五”插入。 外围方法开启事务,插入“张三”方法和外围方法一个事务,插入“李四”方法、插入“王五”方法分别在独立的新建事务中,外围方法抛出异常只回滚和外围方法同一事务的方法,故插入“张三”的方法回滚。
    2 “张三”未插入,“李四”插入,“王五”未插入。 外围方法开启事务,插入“张三”方法和外围方法一个事务,插入“李四”方法、插入“王五”方法分别在独立的新建事务中。插入“王五”方法抛出异常,首先插入 “王五”方法的事务被回滚,异常继续抛出被外围方法感知,外围方法事务亦被回滚,故插入“张三”方法也被回滚。
    3 “张三”插入,“李四”插入,“王五”未插入。 外围方法开启事务,插入“张三”方法和外围方法一个事务,插入“李四”方法、插入“王五”方法分别在独立的新建事务中。插入“王五”方法抛出异常,首先插入“王五”方法的事务被回滚,异常被catch不会被外围方法感知,外围方法事务不回滚,故插入“张三”方法插入成功。

    结论

    外围方法开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。

    3、PROPAGATION_NESTED

    User1Service

    添加1个方法,事务传播行为:NESTED

    @Transactional(propagation = Propagation.NESTED)
    public void nested(String name) {
        this.jdbcTemplate.update("insert into user1(name) VALUES (?)", name);
    }

    User2Service

    添加2个方法,事务传播行为:NESTED,注意第2个方法内部最后一行会抛出一个异常。

    @Transactional(propagation = Propagation.NESTED)
    public void nested(String name) {
        this.jdbcTemplate.update("insert into user2(name) VALUES (?)", name);
    }

    @Transactional(propagation = Propagation.NESTED)
    public void nested_exception(String name) {
        this.jdbcTemplate.update("insert into user2(name) VALUES (?)", name);
        throw new RuntimeException();
    }

    场景1(3-1)

    外围方法没有事务

    验证方法1

    TxService添加
    public void notransaction_exception_nested_nested(){
        user1Service.nested("张三");
        user2Service.nested("李四");
        throw new RuntimeException();
    }
    Demo6Test中添加
    @Test
    public void notransaction_exception_nested_nested() {
        txService.notransaction_exception_nested_nested();
    }
    运行输出
    user1表数据:[{id=1, name=张三}]
    user2表数据:[{id=1, name=李四}]

    验证方法2

    TxService添加
    public void notransaction_nested_nested_exception(){
        user1Service.nested("张三");
        user2Service.nested_exception("李四");
    }
    测试用例,Demo6Test中添加
    @Test
    public void notransaction_nested_nested_exception() {
        txService.notransaction_nested_nested_exception();
    }
    运行输出
    user1表数据:[{id=1, name=张三}]
    user2表数据:[]

    结果分析

    验证方法序号数据库结果结果分析
    1 “张三”、“李四”均插入。 外围方法未开启事务,插入“张三”、“李四”方法在自己的事务中独立运行,外围方法异常不影响内部插入“张三”、“李四”方法独立的事务。
    2 “张三”插入,“李四”未插入。 外围方法没有事务,插入“张三”、“李四”方法都在自己的事务中独立运行,所以插入“李四”方法抛出异常只会回滚插入“李四”方法,插入“张三”方法不受影响。

    结论

    通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.NESTEDPropagation.REQUIRED作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰

    场景2(3-1)

    外围方法开启事务

    验证方法1

    TxService添加
    @Transactional
    public void transaction_exception_nested_nested(){
        user1Service.nested("张三");
        user2Service.nested("李四");
        throw new RuntimeException();
    }
    测试用例,Demo6Test中添加
    @Test
    public void transaction_exception_nested_nested() {
        txService.transaction_exception_nested_nested();
    }
    运行输出
    user1表数据:[]
    user2表数据:[]

    验证方法2

    TxService添加
    @Transactional
    public void transaction_nested_nested_exception(){
        user1Service.nested("张三");
        user2Service.nested_exception("李四");
    }
    Demo6Test中添加
    @Test
    public void transaction_nested_nested_exception() {
        txService.transaction_nested_nested_exception();
    }
    运行输出
    user1表数据:[]
    user2表数据:[]

    验证方法3

    TxService添加
    @Transactional
    public void transaction_nested_nested_exception_try(){
        user1Service.nested("张三");
        try {
            user2Service.nested_exception("李四");
        } catch (Exception e) {
            System.out.println("方法回滚");
        }
    }
    Demo6Test中添加
    @Test
    public void transaction_nested_nested_exception_try() {
        txService.transaction_nested_nested_exception_try();
    }
    运行输出
    方法回滚
    user1表数据:[{id=1, name=张三}]
    user2表数据:[]

    结果分析

    验证方法序号数据库结果结果分析
    1 “张三”、“李四”均未插入。 外围方法开启事务,内部事务为外围事务的子事务,外围方法回滚,内部方法也要回滚。
    2 “张三”、“李四”均未插入。 外围方法开启事务,内部事务为外围事务的子事务,内部方法抛出异常回滚,且外围方法感知异常致使整体事务回滚。
    3 “张三”插入、“李四”未插入。 外围方法开启事务,内部事务为外围事务的子事务,插入“李四”内部方法抛出异常,可以单独对子事务回滚。

    结论

    以上试验结果我们证明在外围方法开启事务的情况下Propagation.NESTED修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务

    内部事务原理

    以mysql为例,mysql中有个savepoint的功能,NESTED内部事务就是通过这个实现的。

    REQUIRED,REQUIRES_NEW,NESTED比较

    由“场景2(1-2)”和“场景2(3-2)”对比,我们可知:

    REQUIRED和NESTED修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,所以NESTED方法抛出异常被回滚,不会影响到外围方法的事务。

    由“场景2(2-2)”和“场景2(3-2)”对比,我们可知:

     

  • 相关阅读:
    OC方法交换swizzle详细介绍——不再有盲点
    集合深浅拷贝以及经常遇到的坑(面试常问)
    网络安全——一图看懂HTTPS建立过程
    NSTimer定时器进阶——详细介绍,循环引用分析与解决
    NSRunLoop原理详解——不再有盲点
    灵活、可高度自定义的——Progress进度圈、弹窗、加载进度、小菊花
    swift学习笔记5——其它部分(自动引用计数、错误处理、泛型...)
    swift学习笔记4——扩展、协议
    swift学习笔记3——类、结构体、枚举
    个推推送处理
  • 原文地址:https://www.cnblogs.com/konglxblog/p/15517941.html
Copyright © 2020-2023  润新知