分布式事务太繁琐?官方推荐Atomikos,5分钟帮你搞定

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"互联网应用架构:专注编程教学,架构,JAVA,Python,微服务,机器学习等领域,欢迎关注,一起学习。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ac/ac4c18a5b58772e5cb14569f7cba39b1.jpeg","alt":"分布式事务太繁琐?官方推荐Atomikos,5分钟帮你搞定","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近有个项目,里面涉及到多个数据源的操作,按照以前的做法采用MQ来做最终一致性,但是又觉得繁琐些,项目的量能其实也不大很小,想来想去最终采用Atomikos来实现。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"XA是啥","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在做Atomikos之前,我们先来了解一下什么是XA。XA是由X/Open组织提出的分布式事务的一种协议(或者称之为分布式架构)。它主要定义了两部分的管理器,全局事务管理器及资源管理器。在XA的设计理念中,把不同资源纳入到一个事务管理器进行统一管理,例如数据库资源,消息中间件资源等,从而进行全部资源的事务提交或者取消,目前主流的数据库,消息中间件都支持XA协议。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/41/41b909939a920c72f9b2f6b10d0d29e0.jpeg","alt":"分布式事务太繁琐?官方推荐Atomikos,5分钟帮你搞定","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"JTA又是啥","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面讲完XA协议,我们来聊聊JTA,JTA叫做Java Transaction API,它是XA协议的JAVA实现。目前在JAVA里面,关于JTA的定义主要是两部分","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、事务管理器接口-----javax.transaction.TransactionManager","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、资源管理器接口-----javax.transaction.xa.XAResource","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一般应用采用JTA接口实现事务,需要一个外置的JTA容器来存储这些事务,像Tomcat。今天我们要讲的是Atomikos,它是一个独立实现了JTA的框架,能够在我们的应用服务器中运行JTA事务。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下来我们直接进入到主题,在一个微服务应用中,针对多数据源的时候如何实现分布式事务。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7e/7e5ed8fd35611f2e772b5660b8d8cec0.jpeg","alt":"分布式事务太繁琐?官方推荐Atomikos,5分钟帮你搞定","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"基础包引入","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\n\n\t4.0.0\n\t\n\t\tcom.boots\n\t\tboots\n\t\t3.0.0.RELEASE\n\t\n\tboots-atomikos\n\tboots-atomikos\n\t分布式事务\n\t\n\n\t\t\n\t\t\n\t\t\torg.springframework.boot\n\t\t\tspring-boot-starter-jta-atomikos\n\t\t\n\n\t\t\n\t\t\n\t\t\tcom.boots\n\t\t\tmodule-boots-api\n\t\t\t${parent.version}\n\t\t\n\n\t\t\n\t\t\n\t\t\torg.mybatis\n\t\t\tmybatis\n\t\t\t3.5.4\n\t\t\n\t\t\n\t\t\torg.mybatis.spring.boot\n\t\t\tmybatis-spring-boot-starter\n\t\t\t2.1.2\n\t\t\n\n\t\t\n\t\t\n\t\t\tcom.alibaba\n\t\t\tdruid-spring-boot-starter\n\t\t\t1.1.21\n\t\t\n\n\t\t\n\t\t\n\t\t\tmysql\n\t\t\tmysql-connector-java\n\t\t\n\n\t\t\n\t\t\n\t\t\tcom.baomidou\n\t\t\tmybatis-plus-boot-starter\n\t\t\t3.3.2\n\t\t\n\n\t\t\n\t\t\n\t\t\tp6spy\n\t\t\tp6spy\n\t\t\t3.9.0\n\t\t\n\n\t\t\n\t\t\n\t\t\tcom.github.ulisesbocchio\n\t\t\tjasypt-spring-boot-starter\n\t\t\t3.0.2\n\t\t\n\n\t\t\n\t\t\t org.junit.jupiter\n\t\t\tjunit-jupiter-engine\n\t\t\ttest\n\t\t\n\t\t\n\t\t\torg.junit.platform\n\t\t\tjunit-platform-launcher\n\t\t\ttest\n\t\t\n\n\n\n\t\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"配置第一个数据源","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * All rights Reserved, Designed By 林溪\n * Copyright: Copyright(C) 2016-2020\n */\n\npackage com.boots.atomikos.common.config;\n\nimport javax.sql.DataSource;\n\nimport org.apache.ibatis.session.SqlSessionFactory;\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\n\nimport com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;\nimport com.boots.atomikos.common.constants.AtomikosConstant;\nimport com.boots.atomikos.common.data.FirstDbData;\nimport com.boots.atomikos.common.utils.JasyptUtils;\nimport com.mysql.cj.jdbc.MysqlXADataSource;\n\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * 第一数据源配置\n * @author:林溪\n * @date:2020年11月19日\n */\n@Configuration\n@MapperScan(basePackages = AtomikosConstant.FIRST_DAO, sqlSessionFactoryRef = AtomikosConstant.FIRST_SESSIONFACTORY)\n@Slf4j\npublic class FirstDataSourceConfig {\n\n @Autowired\n private FirstDbData firstDbData;\n\n /**\n * first数据源配置\n * @author OprCalf\n * @return DataSource\n */\n @Bean(AtomikosConstant.FIRST_DATASOURCE)\n @Primary\n public DataSource firstDataSource() {\n final MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();\n mysqlXaDataSource.setUrl(firstDbData.getFirstUrl());\n mysqlXaDataSource.setPassword(JasyptUtils.decryptMsg(firstDbData.getJasyptPassword(), firstDbData.getFirstPassword()));\n mysqlXaDataSource.setUser(firstDbData.getFirstUsername());\n final AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();\n xaDataSource.setXaDataSource(mysqlXaDataSource);\n xaDataSource.setUniqueResourceName(AtomikosConstant.FIRST_DATASOURCE);\n xaDataSource.setPoolSize(firstDbData.getMaxPoolPreparedStatementPerConnectionSize());\n xaDataSource.setMinPoolSize(firstDbData.getMinIdle());\n xaDataSource.setMaxPoolSize(firstDbData.getMaxActive());\n xaDataSource.setMaxIdleTime(firstDbData.getMinIdle());\n xaDataSource.setMaxLifetime(firstDbData.getMinEvictableIdleTimeMillis());\n xaDataSource.setConcurrentConnectionValidation(true);\n xaDataSource.setTestQuery(\"select 1 from dual\");\n log.info(\"初始化第一数据库成功\");\n return xaDataSource;\n }\n\n /**\n * 创建第一个SqlSessionFactory\n * @param firstDataSource\n * @return\n * @throws Exception\n */\n @Primary\n @Bean(AtomikosConstant.FIRST_SESSIONFACTORY)\n @SneakyThrows(Exception.class)\n public SqlSessionFactory firstSqlSessionFactory(@Qualifier(AtomikosConstant.FIRST_DATASOURCE) DataSource firstDataSource) {\n final MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();\n bean.setDataSource(firstDataSource);\n // 设置mapper位置\n bean.setTypeAliasesPackage(AtomikosConstant.FIRST_MODELS);\n // 设置mapper.xml文件的路径\n bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(AtomikosConstant.FIRST_MAPPER));\n return bean.getObject();\n }\n\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"配置第二个数据源","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * All rights Reserved, Designed By 林溪\n * Copyright: Copyright(C) 2016-2020\n */\n\npackage com.boots.atomikos.common.config;\n\nimport javax.sql.DataSource;\n\nimport org.apache.ibatis.session.SqlSessionFactory;\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\n\nimport com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;\nimport com.boots.atomikos.common.constants.AtomikosConstant;\nimport com.boots.atomikos.common.data.SecondDbData;\nimport com.boots.atomikos.common.utils.JasyptUtils;\nimport com.mysql.cj.jdbc.MysqlXADataSource;\n\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * 第二数据源配置\n * @author:林溪\n * @date:2020年11月19日\n */\n@Configuration\n@MapperScan(basePackages = AtomikosConstant.SECOND_DAO, sqlSessionFactoryRef = AtomikosConstant.SECOND_SESSIONFACTORY)\n@Slf4j\npublic class SecondDataSourceConfig {\n\n @Autowired\n private SecondDbData secondDbData;\n\n /**\n * second数据源配置\n * @author OprCalf\n * @return DataSource\n */\n @Bean(AtomikosConstant.SECOND_DATASOURCE)\n public DataSource secondDataSource() {\n // 使用mysql的分布式驱动,支持MySql5.*、MySql8.* 以上版本\n final MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();\n mysqlXaDataSource.setUrl(secondDbData.getSecondUrl());\n mysqlXaDataSource.setPassword(JasyptUtils.decryptMsg(secondDbData.getJasyptPassword(), secondDbData.getSecondPassword()));\n mysqlXaDataSource.setUser(secondDbData.getSecondUsername());\n final AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();\n xaDataSource.setXaDataSource(mysqlXaDataSource);\n xaDataSource.setUniqueResourceName(AtomikosConstant.SECOND_DATASOURCE);\n xaDataSource.setPoolSize(secondDbData.getMaxPoolPreparedStatementPerConnectionSize());\n xaDataSource.setMinPoolSize(secondDbData.getMinIdle());\n xaDataSource.setMaxPoolSize(secondDbData.getMaxActive());\n xaDataSource.setMaxIdleTime(secondDbData.getMinIdle());\n xaDataSource.setMaxLifetime(secondDbData.getMinEvictableIdleTimeMillis());\n xaDataSource.setConcurrentConnectionValidation(true);\n xaDataSource.setTestQuery(\"select 1 from dual\");\n log.info(\"初始化第二数据库成功\");\n return xaDataSource;\n }\n\n /**\n * 创建第一个SqlSessionFactory\n * @param secondDataSource\n * @return\n * @throws Exception\n */\n @Bean(AtomikosConstant.SECOND_SESSIONFACTORY)\n @SneakyThrows(Exception.class)\n public SqlSessionFactory secondSqlSessionFactory(@Qualifier(AtomikosConstant.SECOND_DATASOURCE) DataSource secondDataSource) {\n final MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();\n bean.setDataSource(secondDataSource);\n // 设置mapper位置\n bean.setTypeAliasesPackage(AtomikosConstant.SECOND_MODELS);\n // 设置mapper.xml文件的路径\n bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(AtomikosConstant.SECOND_MAPPER));\n return bean.getObject();\n }\n\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"配置数据源管理器","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * All rights Reserved, Designed By 林溪\n * Copyright: Copyright(C) 2016-2020\n */\n\npackage com.boots.atomikos.common.config;\n\nimport javax.transaction.TransactionManager;\nimport javax.transaction.UserTransaction;\n\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.transaction.PlatformTransactionManager;\nimport org.springframework.transaction.annotation.EnableTransactionManagement;\nimport org.springframework.transaction.jta.JtaTransactionManager;\n\nimport com.atomikos.icatch.jta.UserTransactionImp;\nimport com.atomikos.icatch.jta.UserTransactionManager;\n\nimport lombok.SneakyThrows;\n\n/**\n * Atomikos事务管理器\n * @author:林溪\n * @date:2020年11月17日\n */\n@Configuration\n@EnableTransactionManagement\npublic class AtomikosConfig {\n\n /**\n * 初始化JTA事务管理器\n * @author 林溪\n * @return UserTransaction\n */\n @Bean(name = \"userTransaction\")\n @SneakyThrows(Exception.class)\n public UserTransaction userTransaction() {\n final UserTransactionImp userTransactionImp = new UserTransactionImp();\n userTransactionImp.setTransactionTimeout(20000);\n return userTransactionImp;\n }\n\n /**\n * 初始化Atomikos事务管理器\n * @author 林溪\n * @return TransactionManager\n */\n @Bean(name = \"atomikosTransactionManager\")\n @SneakyThrows(Exception.class)\n public TransactionManager atomikosTransactionManager() {\n final UserTransactionManager userTransactionManager = new UserTransactionManager();\n userTransactionManager.setForceShutdown(false);\n return userTransactionManager;\n }\n\n /**\n * 加载事务管理\n * @author 林溪\n * @param atomikosTransactionManager\n * @param userTransaction\n * @return PlatformTransactionManager\n */\n @Bean(name = \"transactionManager\")\n @SneakyThrows(Throwable.class)\n public PlatformTransactionManager transactionManager(@Qualifier(\"atomikosTransactionManager\") TransactionManager atomikosTransactionManager, @Qualifier(\"userTransaction\") UserTransaction userTransaction) {\n return new JtaTransactionManager(userTransaction(), atomikosTransactionManager());\n }\n\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"配置常量","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * All rights Reserved, Designed By 林溪\n * Copyright: Copyright(C) 2016-2020\n */\n\npackage com.boots.atomikos.common.constants;\n\n/**\n * 分布式事务常量\n * @author:林溪\n * @date:2020年11月16日\n */\n\npublic class AtomikosConstant {\n\n /*****************第一数据库配置****************************/\n\n // 数据源配置\n public final static String FIRST_DATASOURCE = \"firstDataSource\";\n\n // 会话工厂配置\n public final static String FIRST_SESSIONFACTORY = \"firstSessionFactory\";\n\n // 映射接口配置\n public final static String FIRST_DAO= \"com.boots.atomikos.business.afuser.dao\";\n\n // 数据对象路径\n public final static String FIRST_MODELS = \"com.boots.atomikos.business.afuser.model\";\n\n // 映射目录配置\n public final static String FIRST_MAPPER = \"classpath:mappers/AfUserMapper.xml\";\n\n /*****************第二数据库配置****************************/\n\n // 数据源配置\n public final static String SECOND_DATASOURCE = \"secondDataSource\";\n\n // 会话工厂配置\n public final static String SECOND_SESSIONFACTORY = \"secondSessionFactory\";\n\n // 映射接口配置\n public final static String SECOND_DAO= \"com.boots.atomikos.business.afcustomer.dao\";\n\n // 数据对象路径\n public final static String SECOND_MODELS = \"com.boots.atomikos.business.afcustomer.model\";\n\n // 映射目录配置\n public final static String SECOND_MAPPER = \"classpath:mappers/AfCustomerMapper.xml\";\n\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"配置信息","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"######配置基本信息######\n##配置应用名称\nspring.application.name: boots-atomikos\n##配置时间格式,为了避免精度丢失,全部换成字符串\nspring.jackson.timeZone: GMT+8\nspring.jackson.dateFormat: yyyy-MM-dd HH:mm:ss\nspring.jackson.generator.writeNumbersAsStrings: true\n#####配置数据源#######\nfirst.datasource.url: jdbc:mysql://127.0.0.1:3306/atomikos_first?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true\nfirst.datasource.username: root\nfirst.datasource.password: yiOtQ2YkCWwOvRNmLI4eaPG/fx/q3AIB20JFFz87T96+udBorAm0tNxI2YKfFdeA\n#####配置数据源#######\nsecond.datasource.url: jdbc:mysql://127.0.0.1:3306/atomikos_second?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true\nsecond.datasource.username: root\nsecond.datasource.password: yiOtQ2YkCWwOvRNmLI4eaPG/fx/q3AIB20JFFz87T96+udBorAm0tNxI2YKfFdeA","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"运行测试","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们定义了一个接口并实现该接口,定义了一个test方法,根据不同情况手动抛出异常,在运行后可以直接看到数据并没有被插入到数据中","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * All rights Reserved, Designed By 林溪开源\n * Copyright: Copyright(C) 2016-2020\n * Company 林溪开源 Ltd.\n */\n\npackage com.boots.atomikos.business.afcustomer.service.impl;\n\nimport javax.transaction.Transactional;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\nimport com.boots.atomikos.business.afcustomer.dao.IAfCustomerDao;\nimport com.boots.atomikos.business.afcustomer.model.AfCustomer;\nimport com.boots.atomikos.business.afcustomer.service.IAfCustomerService;\nimport com.boots.atomikos.business.afuser.dao.IAfUserDao;\nimport com.boots.atomikos.business.afuser.model.AfUser;\nimport com.module.boots.exception.CommonRuntimeException;\n\n/**\n * 客户表逻辑服务实现层\n * @author:林溪\n * @date: 2020年11月17日\n */\n@Service\npublic class AfCustomerServiceImpl extends ServiceImpl implements IAfCustomerService {\n\n @Autowired\n private IAfCustomerDao afCustomerDao;\n\n @Autowired\n private IAfUserDao afUserDao;\n\n @Override\n @Transactional(rollbackOn = CommonRuntimeException.class)\n public void test() {\n final AfCustomer afCustomer = AfCustomer.builder().customerName(\"客户1\").build();\n final AfUser afUser = AfUser.builder().userName(\"用户1\").build();\n final int i = afCustomerDao.insert(afCustomer);\n if (i > 0) {\n throw new CommonRuntimeException(\"新增失败\");\n }\n afUserDao.insert(afUser);\n }\n\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"实验结果测试没问题,这里就不贴出来,有兴趣的同学可以通过以下获取源码","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"h ttps://gitee.com/lemeno/boots","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"--END--","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"作者:","attrs":{}},{"type":"text","text":"@互联网应用架构","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"原创作品,抄袭必究","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"如需要源码,转发,关注后私信我","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"部分图片或代码来源网络,如侵权请联系删除,谢谢!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章