分佈式事務太繁瑣?官方推薦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}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章