SpringBoot+AOP实现多数据源动态切换

  • 背景
  • 设计总体思路
  • 步骤

背景

系统后端需要访问多个数据库,现有的数据库连接配置写入配置文件中。后端需要从一个数据库的配置表里动态的读取其它mysql数据库的链接配置信息,并根据链接信息动态创建数据库链接,发起请求,而且还要能使用现在的一些连接池

设计总体思路

SpringBoot+AOP方式实现多数据源切换,继承AbstractRoutingDataSource实现数据源动态的获取,在service层使用注解指定数据源

步骤

  1. 对数据源库表进行设计,相关SQL语句如下所示:

    CREATE TABLE "YUDB"."DOM_DATABASE" ("DB_ID" NUMBER NOT NULL ENABLE,"SRC_ID" NUMBER,"DB_NAME" VARCHAR2 ( 30 ),"DB_C_NAME" VARCHAR2 ( 60 ),"NOTE" VARCHAR2 ( 60 ),"TAB_NUM" NUMBER,"DB_SIZE" NUMBER,"UD_TIME" DATE NOT NULL ENABLE,"MD_FILE_ID" VARCHAR2 ( 32 ),CONSTRAINT "PK_DATABASE" PRIMARY KEY ( "DB_ID" ) USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "USERS" ENABLE,CONSTRAINT "SYS_C0011295" CHECK ( "DB_ID" IS NOT NULL ) ENABLE,CONSTRAINT "SYS_C0011296" CHECK ( "UD_TIME" IS NOT NULL ) ENABLE,CONSTRAINT "FK_DATABASE" FOREIGN KEY ( "SRC_ID" ) REFERENCES "YUDB"."DOM_DATASOURCE" ( "SRC_ID" ) ON DELETE CASCADE ENABLE
    ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "YU"
    
    CREATE TABLE "YUDB"."DOM_DATASOURCE" ("SRC_ID" NUMBER NOT NULL ENABLE,"CHG_ID" NUMBER,"SRC_SID" VARCHAR2 ( 20 ),"TABLE_SPACE" VARCHAR2 ( 20 ),"IP_ADDR" VARCHAR2 ( 30 ),"SRC_PORT" VARCHAR2 ( 10 ),"SRC_USER" VARCHAR2 ( 20 ),"SRC_PSW" VARCHAR2 ( 20 ),"ORGID" NUMBER,"DBTYPE" VARCHAR2 ( 100 ),CONSTRAINT "PK_DATASOURCE" PRIMARY KEY ( "SRC_ID" ) USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "USERS" ENABLE,
    CONSTRAINT "SYS_C0011298" CHECK ( "SRC_ID" IS NOT NULL ) ENABLE
    ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE ( INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT ) TABLESPACE "YU"
    

    DataBase库表设计

    DataSource库表设计:

    ER图如下图所示:

    往yu数据库中的表dataSource里增加数据库的相关配置信息。

  2. 在配置文件application-dev.yml中,进行多数据源配置
    spring:devtools:restart:enabled: true  #设置开启热部署additional-paths: src/main/java #重启目录exclude: WEB-INF/**datasource:main:username: YUDBpassword: YUDB_HHurl: jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521/RAC1driver-class-name: oracle.jdbc.driver.OracleDriverzyml:username: xxxxxxpassword: xxxxxxurl: jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521/HHUdriver-class-name: oracle.jdbc.driver.OracleDriverbjobj:username: xxxxxxpassword: xxxxxxurl: jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521/BJOBJdriver-class-name: oracle.jdbc.driver.OracleDriver#####DruidDataSource配置#####################type: com.alibaba.druid.pool.DruidDataSourceinitialSize: 5minIdle: 5maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: false# 打开PSCache,并且指定每个连接上PSCache的大小poolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙filters: stat,wall,log4j# 通过connectProperties属性来打开mergeSql功能;慢SQL记录connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000#合并多个DruidDataSource的监控数据useGlobalDataSourceStat: true###############以上为配置druid添加的配置########################################
    
  3. 创建实体类DataSource和DataBase
    DataBase表和DataSource表通过外键src_id链接,先创建DataBase实体类

    package org.hhu.yu.system.rdbms.entity;import lombok.Data;import javax.persistence.*;
    import java.util.Date;@Data
    public class DataBase {private Long db_id;@JoinColumn(name="SRC_ID")@ManyToOne(cascade = CascadeType.ALL)//数据库名private String db_name;//数据库中文名private String db_c_nname;//数据库描述private String db_desc;//表数量private Long table_num;//数据库大小private Long data_size;//数据库更新时间private Date update_time;private String metaId;
    }
    

    然后创建DataSource实体类

    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    import lombok.*;@ApiModel(description = "数据源对象Model")
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @RequiredArgsConstructor
    public class DataSource{@NonNull@ApiModelProperty(value = "数据源id",name ="src_id" )private Long src_id;@NonNull@ApiModelProperty(value= "数据源实例名",name="src_sid",required=true)private String src_sid;//数据源表空间private String table_space;@ApiModelProperty(value= "数据源ip地址",name="ip_addr",required=true)private String ip_addr;@ApiModelProperty(value= "数据源端口",name="src_port",required=true)private String src_port;@ApiModelProperty(value= "数据源用户名",name="src_user",required=true)private String src_user;@ApiModelProperty(value= "数据源密码",name="src_pwd",required=true)private String src_pwd;@ApiModelProperty(value= "数据库类型",name="db_type",required=true)private String db_type;@NonNullprivate DataBase dataBase;
    }
  4. 创建DruidDBConfig.java,配置Druid数据库连接池
@Configuration
public class DruidDBConfig {@Bean@Qualifier("mainDataSource")@ConfigurationProperties(prefix = "spring.datasource.main")DataSource mainConfig() throws SQLException{DruidDataSource build = DruidDataSourceBuilder.create().build();List<Filter> filters = new ArrayList<>();filters.add(statFilter());filters.add(logFilter());build.setProxyFilters(filters);return build;}@Bean@Qualifier("zymlDataSource")@ConfigurationProperties(prefix = "spring.datasource.zyml")DataSource zymlConfig(){return DruidDataSourceBuilder.create().build();}@Bean@Qualifier("bjobjDataSource")@ConfigurationProperties(prefix = "spring.datasource.bjobj")DataSource bjobjConfig(){return DruidDataSourceBuilder.create().build();}@Bean(name = "dynamicDataSource")@Primary  //优先使用,多数据源@Qualifier("dynamicDataSource")public DynamicDataSource dynamicDataSource() throws SQLException {DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setDebug(false);// 默认数据源配置 DefaultTargetDataSourcedynamicDataSource.setDefaultTargetDataSource(mainConfig());Map<Object, Object> targetDataSources = new HashMap<Object, Object>();//多数据源配置 TargetDataSourcestargetDataSources.put("mainDataSource", mainConfig());targetDataSources.put("zymlDataSource", zymlConfig());targetDataSources.put("bjobjDataSource", mainConfig());dynamicDataSource.setTargetDataSources(targetDataSources);return dynamicDataSource;}
}
  1. 创建DBContextHolder类,实现数据源的切换服务

    @Slf4j
    public class DBContextHolder {// 对当前线程的操作-线程安全的private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();// 调用此方法,切换数据源public static void setDataSource(String dataSource) {contextHolder.set(dataSource);log.info("已切换到数据源:{}",dataSource);}// 获取数据源public static String getDataSource() {return contextHolder.get();}// 删除数据源public static void clearDataSource() {contextHolder.remove();log.info("已切换到主数据源");}
    }
    
  2. 创建核心的动态数据源配置类,该类继承AbstractRoutingDataSource实现数据源动态的获取。

@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {private boolean debug = true;private Map<Object, Object> dynamicTargetDataSources;private Object dynamicDefaultTargetDataSource;@Overrideprotected Object determineCurrentLookupKey() {String datasource = DBContextHolder.getDataSource();if (!StringUtils.isEmpty(datasource)) {Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;if (dynamicTargetDataSources2.containsKey(datasource)) {log.info("---当前数据源:" + datasource + "---");} else {log.info("不存在的数据源:");throw new ADIException("不存在的数据源:"+datasource,500);return null;               }} else {log.info("---当前数据源:默认数据源---");}return datasource;}@Overridepublic void setTargetDataSources(Map<Object, Object> targetDataSources) {super.setTargetDataSources(targetDataSources);this.dynamicTargetDataSources = targetDataSources;}@Overridepublic void setDefaultTargetDataSource(Object defaultTargetDataSource) {super.setDefaultTargetDataSource(defaultTargetDataSource);this.dynamicDefaultTargetDataSource = defaultTargetDataSource;}/*** @param debug* @description the debug to set*/public void setDebug(boolean debug) {this.debug = debug;}/*** @return the debug*/public boolean isDebug() {return debug;}/*** @return the dynamicTargetDataSources*/public Map<Object, Object> getDynamicTargetDataSources() {return dynamicTargetDataSources;}/*** @param dynamicTargetDataSources*            the dynamicTargetDataSources to set*/public void setDynamicTargetDataSources(Map<Object, Object> dynamicTargetDataSources) {this.dynamicTargetDataSources = dynamicTargetDataSources;}/*** @return the dynamicDefaultTargetDataSource*/public Object getDynamicDefaultTargetDataSource() {return dynamicDefaultTargetDataSource;}/*** @param dynamicDefaultTargetDataSource* the dynamicDefaultTargetDataSource to set*/public void setDynamicDefaultTargetDataSource(Object dynamicDefaultTargetDataSource) {this.dynamicDefaultTargetDataSource = dynamicDefaultTargetDataSource;}/*** @param dataSource* @throws Exception* @description 创建新数据源时检查数据源是否存在*/public void createDataSourceWithCheck(DataSource dataSource) throws Exception {@NonNull Long src_id = dataSource.getSrc_id();log.info("正在检查数据源:"+src_id);Map<Object, Object> currentDynamicTargetDataSources = this.dynamicTargetDataSources;if (currentDynamicTargetDataSources.containsKey(src_id)) {log.info("数据源"+src_id+"之前已经创建,准备测试数据源是否正常...");DruidDataSource druidDataSource = (DruidDataSource) currentDynamicTargetDataSources.get(src_id);boolean rightFlag = true;Connection connection = null;try {log.info(src_id+"数据源的概况->当前闲置连接数:"+druidDataSource.getPoolingCount());long activeCount = druidDataSource.getActiveCount();log.info(src_id+"数据源的概况->当前活动连接数:"+activeCount);if(activeCount > 0) {log.info(src_id+"数据源的概况->活跃连接堆栈信息:"+druidDataSource.getActiveConnectionStackTrace());}log.info("准备获取数据库连接...");connection = druidDataSource.getConnection();log.info("数据源"+src_id+"正常");} catch (Exception e) {log.error(e.getMessage(),e); //把异常信息打印到日志文件rightFlag = false;log.info("缓存数据源"+src_id+"已失效,准备删除...");if(delDatasources(src_id)) {log.info("缓存数据源删除成功");} else {log.info("缓存数据源删除失败");}} finally {if(null != connection) {connection.close();}}if(rightFlag) {log.info("不需要重新创建数据源");return;} else {log.info("准备重新创建数据源...");createDataSource(dataSource);log.info("重新创建数据源完成");}} else {createDataSource(dataSource);}}private  void createDataSource(DataSource dataSource) throws Exception {DBUtils dbUtils = SpringUtils.getBean(DBUtils.class);@NonNull Long src_id = dataSource.getSrc_id();log.info("准备创建数据源"+src_id);String db_type = dataSource.getDb_type();String username = dataSource.getSrc_user();String password = dataSource.getSrc_pwd();String ip_addr = dataSource.getIp_addr();String src_port = dataSource.getSrc_port();@NonNull String src_sid = dataSource.getSrc_sid();String url = null;String driveClass = null;if("mysql".equalsIgnoreCase(db_type)) {driveClass = DBUtils.MYSQL_DRIVER;dbUtils.setMySQLYUrl(ip_addr,src_port,src_sid);url = dbUtils.getMySQLYUrl();} else if("oracle".equalsIgnoreCase(db_type)){driveClass = DBUtils.ORACLE_DRIVER;dbUtils.setOracleUrl(ip_addr,src_port,src_sid);url = dbUtils.getOracleUrl();} else if("dm".equalsIgnoreCase(db_type)){driveClass = DBUtils.DM_DRIVER;dbUtils.setDMUrl(ip_addr,src_port,src_sid);url = dbUtils.getDMUrl();} else if("sqlserver".equalsIgnoreCase(db_type)){driveClass = DBUtils.SQLSERVER_DRIVER;dbUtils.setSQLserverUrl(ip_addr,src_port,src_sid);url = dbUtils.getSQLserverUrl();}if(testDatasource(src_id.toString(),driveClass,url,username,password)) {boolean result = this.createDataSource(src_id.toString(), driveClass, url, username, password, db_type);if(!result) {log.error("数据源"+src_id+"配置正确,但是创建失败");throw new ADIException("数据源"+src_id+"配置正确,但是创建失败",500);}} else {log.error("数据源配置有错误");throw new ADIException("数据源配置有错误",500);}}/*** @description 自定义创建数据源* @param key* @param driveClass* @param url* @param username* @param password* @param db_type* @return*/public boolean createDataSource(String key, String driveClass, String url, String username, String password, String db_type) {try {try { // 排除连接不上的错误Class.forName(driveClass);DriverManager.getConnection(url, username, password);//连接数据库} catch (Exception e) {return false;}@SuppressWarnings("resource")
//            HikariDataSource druidDataSource = new HikariDataSource();DruidDataSource druidDataSource = new DruidDataSource();druidDataSource.setName(key);druidDataSource.setDriverClassName(driveClass);druidDataSource.setUrl(url);druidDataSource.setUsername(username);druidDataSource.setPassword(password);druidDataSource.setInitialSize(1); //初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时druidDataSource.setMaxActive(20); //最大连接池数量druidDataSource.setMaxWait(60000); //获取连接时最大等待时间,单位毫秒。当链接数已经达到了最大链接数的时候,应用如果还要获取链接就会出现等待的现象,等待链接释放并回到链接池,如果等待的时间过长就应该踢掉这个等待,不然应用很可能出现雪崩现象druidDataSource.setMinIdle(5); //最小连接池数量String validationQuery = "select 1 from dual";if("mysql".equalsIgnoreCase(db_type)) {driveClass = DBUtils.MYSQL_DRIVER;validationQuery = "select 1";} else if("oracle".equalsIgnoreCase(db_type)){driveClass = DBUtils.ORACLE_DRIVER;druidDataSource.setPoolPreparedStatements(true); //是否缓存preparedStatement,PSCache对支持游标的ORACLE数据库性能提升巨大,在mysql下建议关闭。druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(50);druidDataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=300000");//对于耗时长的查询sql,会受限于ReadTimeout的控制,单位毫秒} else if("dm".equalsIgnoreCase(db_type)){driveClass = DBUtils.DM_DRIVER;validationQuery = "select 1";} else if("sqlserver".equalsIgnoreCase(db_type)){driveClass = DBUtils.SQLSERVER_DRIVER;validationQuery = "select 1";}//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用druidDataSource.setTestOnBorrow(true);//申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。druidDataSource.setTestWhileIdle(true);//用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。druidDataSource.setValidationQuery(validationQuery);//属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:walldruidDataSource.setFilters("stat");//配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒druidDataSource.setTimeBetweenEvictionRunsMillis(60000);//配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000druidDataSource.setMinEvictableIdleTimeMillis(180000);//打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,// 则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select * from dual,只要// minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断druidDataSource.setKeepAlive(true);//是否移除泄露的连接/超过时间限制是否回收。druidDataSource.setRemoveAbandoned(true);//泄露连接的定义时间(要超过最大事务的处理时间);单位为秒。这里配置为1小时druidDataSource.setRemoveAbandonedTimeout(3600);//移除泄露连接发生是是否记录日志druidDataSource.setLogAbandoned(true);druidDataSource.init();this.dynamicTargetDataSources.put(key, druidDataSource);// 将map赋值给父类的TargetDataSourcessetTargetDataSources(this.dynamicTargetDataSources);super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理log.info(key+"数据源初始化成功");//log.info(key+"数据源的概况:"+druidDataSource.dump());return true;} catch (Exception e) {log.error(e + "");return false;}}/*** 删除数据源* @param db_id* @return*/public boolean delDatasources(Long db_id) {Map<Object, Object> currentDynamicTargetDataSources = this.dynamicTargetDataSources;if (currentDynamicTargetDataSources.containsKey(db_id)) {Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();for (DruidDataSource l : druidDataSourceInstances) {if (db_id.equals(l.getName())) {currentDynamicTargetDataSources.remove(db_id);DruidDataSourceStatManager.removeDataSource(l);// 将map赋值给父类的TargetDataSourcessetTargetDataSources(currentDynamicTargetDataSources);// 将TargetDataSources中的连接信息放入resolvedDataSources管理super.afterPropertiesSet();return true;}}return false;} else {return false;}}/*** 测试数据源连接* @param driveClass* @param url* @param username* @param password* @return*/public boolean testDatasource(String key,String driveClass, String url, String username, String password) {try {Class.forName(driveClass);DriverManager.getConnection(url, username, password);return true;} catch (Exception e) {return false;}}}

SpringBoot+AOP实现多数据源动态切换相关推荐

  1. Spring-Boot + AOP实现多数据源动态切换

    2019独角兽企业重金招聘Python工程师标准>>> 最近在做保证金余额查询优化,在项目启动时候需要把余额全量加载到本地缓存,因为需要全量查询所有骑手的保证金余额,为了不影响主数据 ...

  2. springboot使用mybatis多数据源动态切换的实现

    需求:项目使用了读写分离,或者数据进行了分库处理,我们希望在操作不同的数据库的时候,我们的程序能够动态的切换到相应的数据库,执行相关的操作. 首先,你需要一个能够正常运行的springboot项目,配 ...

  3. SpringBoot+AOP构建多数据源的切换实践

    针对微服务架构中常用的设计模块,通常我们都会需要使用到druid作为我们的数据连接池,当架构发生扩展的时候 ,通常面对的数据存储服务器也会渐渐增加,从原本的单库架构逐渐扩展为复杂的多库架构. 当在业务 ...

  4. 多数据源解决方案——AOP实现多数据源动态切换

    文章目录 1. 场景描述 2. 项目依赖 3. 场景实现 4. 项目源代码 GitHub 1. 场景描述 在springboot开发中,可能遇见操作多个数据库的情形.一种解决方案就是:配置动态数据源, ...

  5. springboot多数据源动态切换和自定义mybatis分页插件

    1.配置多数据源 增加druid依赖 完整pom文件 数据源配置文件 route.datasource.driver-class-name= com.mysql.jdbc.Driver route.d ...

  6. Spring+Mybatis多数据源配置(四)——AbstractRoutingDataSource实现数据源动态切换

    欢迎支持笔者新作:<深入理解Kafka:核心设计与实践原理>和<RabbitMQ实战指南>,同时欢迎关注笔者的微信公众号:朱小厮的博客. 欢迎跳转到本文的原文链接:https: ...

  7. Proxool配置多数据源动态切换

    2019独角兽企业重金招聘Python工程师标准>>> 前段时间遇到多数据源动态切换问题,总结一下,做个记录,以备后续之需! 首先附上proxool连接池的配置方法:http://3 ...

  8. SpringBoot+MybatisPlus多数据源动态切换

    公司某项目做大屏展示,但数据来源自7个不同的数据库,需要涉及跨库查询,要求. 本项目采用SpringBoot+MybatisPlus做服务端提供RESTful接口,前后端分离开发,总结一下项目中实现的 ...

  9. springboot多数据源动态切换,事务下切换数据源(非分布式事务)

    目录 1.业务场景 2.主要思路 3.加载默认数据源 4.多数据源规则配置 5.程序启动加载子公司数据源 6.自定义事务注解 7.程序中调用 1.业务场景 因为业务业务需求,需要把基础数据与子公司业务 ...

最新文章

  1. Microsoft Surface Toolkit Beta 版发布
  2. OSChina 周六乱弹 ——土肥圆装高富帅相亲节目现场拆穿
  3. html div数据替换,javascript – 将html添加到div而不替换其中的当前内容
  4. 【转】每天一个linux命令(50):crontab命令
  5. 【深入学习iOS开发(五)】Archive(归档)
  6. c语言分配飞机10个座位,leetcode1227(飞机座位分配)--C语言实现
  7. ajax json 封装,Ajax--json(Ajax调用返回json封装代码、格式及注意事项)
  8. python 3d重建_三维人脸重建(一)——Python读取obj文件
  9. java工厂模式和抽象工厂_Java中的抽象工厂设计模式
  10. 最新emoji表情代码大全_10月最新早上好问候语表情图片大全,朋友们大家早上好表情包!...
  11. android horizontalscrollview 动画,Android 用HorizontalScrollView实现滑动标签tabView
  12. 微信小程序蓝牙控制开门
  13. vue项目中引入vuex------初试
  14. SICNU ACM新生第一次考核
  15. OutMan——Objective-C中的ARC介绍和block的使用
  16. windows生产力提升-装机必备
  17. win10 子系统之 Ubuntu,解放你的生产力
  18. html 怎么几秒后自动隐藏,3秒或5秒后自动隐藏提示消息和重定向到另一页
  19. Douyin-Bot 项目优化-改进,优化效率,本地cv2识别过滤
  20. URLencode转换

热门文章

  1. mysql on delete关键字_MySQL外键约束On Delete、On Update各取值的含义
  2. 浅谈python_浅谈python-Django
  3. vue 单选框样式_作为一位Vue工程师,这些开发技巧你都会吗?
  4. POI:根据单元格的自定义名获取单元格的位置
  5. js调用php函数兵每秒刷新,深入理解JavaScript立即调用函数表达式(IIFE)
  6. java 位运算符赋值_java-运算符(算术、赋值 =、关系、逻辑、三元、位运算符)...
  7. c语言中的typedef struct相当于java的一个类?,C ++中'struct'和'typedef struct'之间的区别?...
  8. android 将SQLite数据库的表格导出为csv格式,并解析csv文件
  9. 深度学习和目标检测系列教程 12-300:常见的opencv的APi和用法总结
  10. ACL 2020 | 基于不同硬件搜索更好的Transformer结构