SpringBoot 实现动态数据源切换

Spring Boot + Mybatis Plus + Druid + MySQL 实现动态数据源切换及动态 SQL 语句执行。

项目默认加载 application.yml 中配置的数据源,只有在调用数据源切换时创建数据连接。

Druid 实现动态数据源切换

相关依赖

 <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency> <dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency> <dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>

application.yml Druid 配置

spring:
#Druid 连接池通用配置
datasource:
url: jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
# sql 校验
validation-query: select count(1) from sys.objects Where type='U' And type_desc='USER_TABLE'
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
max-pool-prepared-statement-per-connection-size: 20
filters: stat # wall 若开启 wall,会把 if 中的 and 判断为注入进行拦截
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 指定当连接超过废弃超时时间时,是否立刻删除该连接
remove-abandoned: true
# 指定连接应该被废弃的时间
remove-abandoned-timeout: 60000
# 是否追踪废弃statement或连接,默认为: false
log-abandoned: false

Druid 配置

package com.demo.utils.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import javax.servlet.Filter;
import javax.servlet.Servlet;
import java.util.HashMap;
import java.util.Map; /**
* @ClassName: DruidConfig.java
* @Description: Druid配置
* @Author: tanyp
* @Date: 2022/2/18 10:29
**/
@Configuration
public class DruidConfig { @Value("${spring.datasource.type}")
private String db_type; // @Value("${spring.datasource.driver-class-name}")
// private String db_driver_name; @Value("${spring.datasource.url}")
private String db_url; @Value("${spring.datasource.username}")
private String db_user; @Value("${spring.datasource.password}")
private String db_pwd; // 连接池初始化大小
@Value("${spring.datasource.druid.initial-size}")
private int initialSize; // 连接池最小值
@Value("${spring.datasource.druid.min-idle}")
private int minIdle; // 连接池最大值
@Value("${spring.datasource.druid.max-active}")
private int maxActive; // 配置获取连接等待超时的时间
@Value("${spring.datasource.druid.max-wait}")
private int maxWait; // 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
@Value("${spring.datasource.druid.time-between-eviction-runs-millis}")
private int timeBetweenEvictionRunsMillis; // 配置一个连接在池中最小生存的时间,单位是毫秒
@Value("${spring.datasource.druid.min-evictable-idle-time-millis}")
private int minEvictableIdleTimeMillis; // 用来验证数据库连接的查询语句,这个查询语句必须是至少返回一条数据的SELECT语句
@Value("${spring.datasource.druid.validation-query}")
private String validationQuery; // 检测连接是否有效
@Value("${spring.datasource.druid.test-while-idle}")
private boolean testWhileIdle; // 申请连接时执行validationQuery检测连接是否有效。做了这个配置会降低性能。
@Value("${spring.datasource.druid.test-on-borrow}")
private boolean testOnBorrow; // 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
@Value("${spring.datasource.druid.test-on-return}")
private boolean testOnReturn; // 是否缓存preparedStatement,也就是PSCache。
@Value("${spring.datasource.druid.pool-prepared-statements}")
private boolean poolPreparedStatements; // 指定每个连接上PSCache的大小。
@Value("${spring.datasource.druid.max-pool-prepared-statement-per-connection-size}")
private int maxPoolPreparedStatementPerConnectionSize; // 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
@Value("${spring.datasource.druid.filters}")
private String filters; // 通过connectProperties属性来打开mergeSql功能;慢SQL记录
@Value("${spring.datasource.druid.connect-properties}")
private String connectionProperties; // 指定当连接超过废弃超时时间时,是否立刻删除该连接
@Value("${spring.datasource.druid.remove-abandoned}")
private boolean removeAbandoned; // 指定连接应该被废弃的时间
@Value("${spring.datasource.druid.remove-abandoned-timeout}")
private int removeAbandonedTimeout; // 使用DBCP connection pool,是否追踪废弃statement或连接,默认为: false
@Value("${spring.datasource.druid.log-abandoned}")
private boolean logAbandoned; @Bean
public DynamicDataSource druidDataSource() {
Map<Object, Object> map = new HashMap<>();
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance(); DruidDataSource defaultDataSource = new DruidDataSource();
// defaultDataSource.setDriverClassName(db_driver_name);
defaultDataSource.setUrl(db_url);
defaultDataSource.setUsername(db_user);
defaultDataSource.setPassword(db_pwd);
defaultDataSource.setInitialSize(initialSize);
defaultDataSource.setMinIdle(minIdle);
defaultDataSource.setMaxActive(maxActive);
defaultDataSource.setMaxWait(maxWait);
defaultDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
defaultDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
defaultDataSource.setValidationQuery(validationQuery);
defaultDataSource.setTestWhileIdle(testWhileIdle);
defaultDataSource.setTestOnBorrow(testOnBorrow);
defaultDataSource.setTestOnReturn(testOnReturn);
defaultDataSource.setPoolPreparedStatements(poolPreparedStatements);
defaultDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
defaultDataSource.setRemoveAbandoned(removeAbandoned);
defaultDataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
defaultDataSource.setLogAbandoned(logAbandoned);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource); map.put("default", defaultDataSource);
dynamicDataSource.setTargetDataSources(map);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
} @Bean
public ServletRegistrationBean<Servlet> druid() {
// 现在要进行druid监控的配置处理操作
ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 白名单,多个用逗号分割, 如果allow没有配置或者为空,则允许所有访问
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
// 黑名单,多个用逗号分割 (共同存在时,deny优先于allow)
//servletRegistrationBean.addInitParameter("deny", "127.0.0.1");
// 控制台管理用户名
servletRegistrationBean.addInitParameter("loginUsername", "admin");
// 控制台管理密码
servletRegistrationBean.addInitParameter("loginPassword", "admin");
// 是否可以重置数据源,禁用HTML页面上的“Reset All”功能
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
} @Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new WebStatFilter());
// 所有请求进行监控处理
filterRegistrationBean.addUrlPatterns("/*");
// 添加不需要忽略的格式信息
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.css,/druid/*");
return filterRegistrationBean;
} }

数据源上下文

package com.demo.utils.datasource;

/**
* @ClassName: DataSourceContextHolder.java
* @Description: 数据源上下文
* @Author: tanyp
* @Date: 2022/2/18 10:04
**/
public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); /**
* @MonthName: setDBType
* @Description: 设置当前线程持有的数据源
* @Author: tanyp
* @Date: 2022/2/18 10:07
* @Param: [dbType]
* @return: void
**/
public static synchronized void setDBType(String dbType) {
contextHolder.set(dbType);
} /**
* @MonthName: getDBType
* @Description: 获取当前线程持有的数据源
* @Author: tanyp
* @Date: 2022/2/18 10:07
* @Param: []
* @return: java.lang.String
**/
public static String getDBType() {
return contextHolder.get();
} /**
* @MonthName: clearDBType
* @Description: 清除当前线程持有的数据源
* @Author: tanyp
* @Date: 2022/2/18 10:07
* @Param: []
* @return: void
**/
public static void clearDBType() {
contextHolder.remove();
} }

数据源信息

package com.demo.utils.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.HashMap;
import java.util.Map; /**
* @ClassName: DynamicDataSource.java
* @Description: 数据源信息
* @Author: tanyp
* @Date: 2022/2/18 10:26
**/
public class DynamicDataSource extends AbstractRoutingDataSource { private static DynamicDataSource instance; private static byte[] lock = new byte[0]; private static Map<Object, Object> dataSourceMap = new HashMap<>(); @Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceMap.putAll(targetDataSources);
super.afterPropertiesSet();
} public Map<Object, Object> getDataSourceMap() {
return dataSourceMap;
} public static synchronized DynamicDataSource getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new DynamicDataSource();
}
}
}
return instance;
} @Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDBType();
} }

切换数据源

以数据库 ip + 端口 + 数据库名作为 key 和数据库连接的映射关系。

package com.demo.utils;

import com.alibaba.druid.pool.DruidDataSource;
import com.demo.utils.datasource.DataSourceContextHolder;
import com.demo.utils.datasource.DynamicDataSource;
import lombok.extern.slf4j.Slf4j; import java.util.Map;
import java.util.Objects; /**
* @ClassName: DruidDataSourceUtil.java
* @Description: 用于查找并切换数据源
* @Author: tanyp
* @Date: 2022/2/18 10:34
**/
@Slf4j
public class DruidDataSourceUtil { /**
* @MonthName: addOrChangeDataSource
* @Description: 切换数据源
* @Author: tanyp
* @Date: 2022/2/18 10:38
* @Param: dbip:IP地址
* dbport:端口号
* dbname:数据库名称
* dbuser:用户名称
* dbpwd:密码
* @return: void
**/
public static void addOrChangeDataSource(String dbip, String dbport, String dbname, String dbuser, String dbpwd) {
try {
DataSourceContextHolder.setDBType("default"); // 数据库连接key:ip + 端口 + 数据库名
String key = "db" + dbip + dbport + dbname; // 创建动态数据源
Map<Object, Object> dataSourceMap = DynamicDataSource.getInstance().getDataSourceMap();
if (!dataSourceMap.containsKey(key + "master") && Objects.nonNull(key)) {
String url = "jdbc:mysql://" + dbip + ":" + dbport + "/" + dbname + "?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&useSSL=false";
log.info("插入新数据库连接信息为:{}", url); DruidDataSource dynamicDataSource = new DruidDataSource();
// dynamicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
dynamicDataSource.setUsername(dbuser);
dynamicDataSource.setUrl(url);
dynamicDataSource.setPassword(dbpwd);
dynamicDataSource.setInitialSize(50);
dynamicDataSource.setMinIdle(5);
dynamicDataSource.setMaxActive(1000);
dynamicDataSource.setMaxWait(500); // 如果失败,当前的请求可以返回
dynamicDataSource.setTimeBetweenEvictionRunsMillis(60000);
dynamicDataSource.setMinEvictableIdleTimeMillis(300000);
dynamicDataSource.setValidationQuery("SELECT 1 FROM DUAL");
dynamicDataSource.setTestWhileIdle(true);
dynamicDataSource.setTestOnBorrow(false);
dynamicDataSource.setTestOnReturn(false);
dynamicDataSource.setPoolPreparedStatements(true);
dynamicDataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
dynamicDataSource.setRemoveAbandoned(true);
dynamicDataSource.setRemoveAbandonedTimeout(180);
dynamicDataSource.setLogAbandoned(true);
dynamicDataSource.setConnectionErrorRetryAttempts(0); // 失败后重连的次数
dynamicDataSource.setBreakAfterAcquireFailure(true); // 请求失败之后中断 dataSourceMap.put(key + "master", dynamicDataSource); DynamicDataSource.getInstance().setTargetDataSources(dataSourceMap);
// 切换为动态数据源实例
DataSourceContextHolder.setDBType(key + "master");
} else {
// 切换为动态数据源实例
DataSourceContextHolder.setDBType(key + "master");
}
} catch (Exception e) {
log.error("=====创建据库连接异常:{}", e);
}
} }

以上动态数据源加载及切换已完成。

使用 MyBatis Plus 动态执行 SQL 语句

加载动态数据源执行 SQL (增、删、改、查)

package com.demo.service.impl;

import com.demo.constants.Constants;
import com.demo.mapper.DynamicSqlMapper;
import com.demo.service.DynamicDataSourceService;
import com.demo.utils.DataUtils;
import com.demo.utils.DruidDataSourceUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import java.util.HashMap;
import java.util.Map; /**
* @ClassName: DynamicDataSourceServiceImpl.java
* @Description: 动态数据源
* @Author: tanyp
* @Date: 2022/2/18 10:43
**/
@Slf4j
@Service("dynamicDataSourceService")
public class DynamicDataSourceServiceImpl implements DynamicDataSourceService { @Autowired
private DynamicSqlMapper dynamicSqlMapper; /**
* @MonthName: dynamicExecutive
* @Description: 加载动态数据源执行SQL
* @Author: tanyp
* @Date: 2022/2/28 10:46
* @Param: {
* "dbip":"IP地址",
* "dbport":"端口号",
* "dbname":"数据库名称",
* "dbuser":"用户名称",
* "dbpwd":"密码",
* "type":"执行类型:SELECT、INSERT、UPDATE、DELETE",
* "paramSQL":"需要执行的SQL",
* "param":{} // SQL中的参数
* }
* @return: java.util.Map<java.lang.String, java.lang.Object>
**/
@Override
public Map<String, Object> dynamicExecutive(Map<String, Object> params) {
Map<String, Object> result = null;
try {
DruidDataSourceUtil.addOrChangeDataSource(
String.valueOf(params.get("dbip")),
String.valueOf(params.get("dbport")),
String.valueOf(params.get("dbname")),
String.valueOf(params.get("dbuser")),
String.valueOf(params.get("dbpwd"))
);
} catch (Exception e) {
log.error("=====创建据库连接异常:{}", e);
result.put("data", "创建据库连接异常,请检查连接信息是否有误!");
} try {
// 执行动态SQL
Object data = null;
String type = String.valueOf(params.get("type"));
String paramSQL = String.valueOf(params.get("paramSQL"));
Map<String, Object> param = (HashMap) params.get("param"); // 参数替换
String sql = DataUtils.strRreplace(paramSQL, param); log.info("======请求SQL语句:{}======", sql); switch (type) {
case Constants.SELECT:
data = dynamicSqlMapper.dynamicsSelect(sql);
break;
case Constants.INSERT:
data = dynamicSqlMapper.dynamicsInsert(sql);
break;
case Constants.UPDATE:
data = dynamicSqlMapper.dynamicsUpdate(sql);
break;
case Constants.DELETE:
data = dynamicSqlMapper.dynamicsDelete(sql);
break;
default:
data = "请求参数【type】有误,请核查!";
break;
} result = new HashMap<>();
result.put("data", data);
} catch (Exception e) {
log.error("=====执行SQL异常:{}", e);
result.put("data", "执行SQL异常,请检查SQL语句是否有误!");
}
return result;
} }

动态 SQL 执行器

package com.demo.mapper;

import org.apache.ibatis.annotations.*;

import java.util.List;
import java.util.Map; /**
* @ClassName: DynamicSqlMapper.java
* @Description: 动态SQL执行器
* @Author: tanyp
* @Date: 2022/2/28 10:21
**/
@Mapper
public interface DynamicSqlMapper { @Select({"${sql}"})
@ResultType(Object.class)
List<Map<String, Object>> dynamicsSelect(@Param("sql") String sql); @Insert({"${sql}"})
@ResultType(Integer.class)
Integer dynamicsInsert(@Param("sql") String sql); @Update({"${sql}"})
@ResultType(Integer.class)
Integer dynamicsUpdate(@Param("sql") String sql); @Delete({"${sql}"})
@ResultType(Integer.class)
Integer dynamicsDelete(@Param("sql") String sql); }

SQL 占位符处理

package com.demo.utils;

import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern; /**
* @ClassName: DataUtils.java
* @Description: 数据处理
* @Author: tanyp
* @Date: 2022/2/28 9:21
**/
@Slf4j
public class DataUtils { private static final Pattern pattern = Pattern.compile("\\#\\{(.*?)\\}");
private static Matcher matcher; /**
* @MonthName: strRreplace
* @Description: 字符串站位处理
* @Author: tanyp
* @Date: 2022/2/28 9:21
* @Param: [content, param]
* @return: java.lang.String
**/
public static String strRreplace(String content, Map<String, Object> param) {
if (Objects.isNull(param)) {
return null;
}
try {
matcher = pattern.matcher(content);
while (matcher.find()) {
String key = matcher.group();
String keyclone = key.substring(2, key.length() - 1).trim();
boolean containsKey = param.containsKey(keyclone);
if (containsKey && Objects.nonNull(param.get(keyclone))) {
String value = "'" + param.get(keyclone) + "'";
content = content.replace(key, value);
}
}
return content;
} catch (Exception e) {
log.error("字符串站位处理:{}", e);
return null;
}
} }

测试

POST 请求接口

http://127.0.0.1:8001/dynamicExecutive

请求参数

{
"dbip":"127.0.0.1",
"dbport":"3306",
"dbname":"demo",
"dbuser":"root",
"dbpwd":"root",
"type":"SELECT",
"paramSQL":"SELECT id, code, name, path, message, status, classify, params, icon, update_time, create_time FROM component where id = #{id}",
"param":{
"id":"611fb3e553371b9d42f8583391cc8478"
}
}

正常返回值

{
"code": 200,
"message": "操作成功",
"result": {
"code": 200,
"message": "操作成功!",
"result": {
"data": [
{
"path": "127.0.0.1",
"classify": "8ab3f21e1607a0374fb2d82f7fcaee98",
"update_time": "2022-03-08 17:59:11",
"code": "dynamicDataSourceService",
"create_time": "2022-03-07 14:51:15",
"name": "动态数据源",
"icon": "Rank",
"id": "611fb3e553371b9d42f8583391cc8478",
"message": "加载动态数据源执行SQL",
"status": 0
}
]
},
"dateTime": "2022-03-11T09:56:31.87"
}
}

SpringBoot 动态数据源的更多相关文章

  1. SpringBoot动态数据源

    1.原理图 2.创建枚举类 /** * 存数据源key值 */ public enum DataSourceKey { master,salve,migration } 3.创建自定义注解类 /** ...

  2. spring boot动态数据源方案

    动态数据源 1.背景 动态数据源在实际的业务场景下需求很多,而且想要沟通多数据库确实需要封装这种工具,针对于bi工具可能涉及到从不同的业务库或者数据仓库中获取数据,动态数据源就更加有意义. 2.依赖 ...

  3. springboot动态多数据源

    参考文章:https://www.cnblogs.com/hehehaha/p/6147096.html 前言 目标是springboot工程支持多个MySQL数据源,在代码层面上,同一个SQL(Ma ...

  4. springboot 双数据源+aop动态切换

    # springboot-double-dataspringboot-double-data 应用场景 项目需要同时连接两个不同的数据库A, B,并且它们都为主从架构,一台写库,多台读库. 多数据源 ...

  5. springboot动态多数据源切换

    application-test.properties #datasource -- mysql multiple.datasource.master.url=jdbc:mysql://localho ...

  6. SpringBoot(十一)-- 动态数据源

    SpringBoot中使用动态数据源可以实现分布式中的分库技术,比如查询用户 就在用户库中查询,查询订单 就在订单库中查询. 一.配置文件application.properties # 默认数据源 ...

  7. SpringBoot和Mycat动态数据源项目整合

    SpringBoot项目整合动态数据源(读写分离) 1.配置多个数据源,根据业务需求访问不同的数据,指定对应的策略:增加,删除,修改操作访问对应数据,查询访问对应数据,不同数据库做好的数据一致性的处理 ...

  8. SpringBoot整合MyBatisPlus配置动态数据源

    目录 SpringBoot整合MyBatisPlus配置动态数据源 SpringBoot整合MyBatisPlus配置动态数据源 推文:2018开源中国最受欢迎的中国软件MyBatis-Plus My ...

  9. SpringBoot集成Mybatis配置动态数据源

    很多人在项目里边都会用到多个数据源,下面记录一次SpringBoot集成Mybatis配置多数据源的过程. pom.xml <?xml version="1.0" encod ...

  10. SpringBoot多数据源动态切换数据源

    1.配置多数据源 spring: datasource: master: password: erp_test@abc url: jdbc:mysql://127.0.0.1:3306/M201911 ...

随机推荐

  1. VO、DTO、Entity的区别

    只能说从实际用法的一般习惯上进行区分: 1.entity 里的每一个字段,与数据库相对应,注意:entity与对应的实际数据库表的字段 没有强制要求但是一般建议保持一致(包括字段数据类型),当然,从实 ...

  2. [GDOIpj222A] 点指兵兵

    第一题 点指兵兵 提交文件: bing.cpp 输入文件: bing.in 输出文件: bing.out 时间空间限制: 1 秒, 256 MB 你一定有过在两个物品之间犹豫不决的时候,想要借助一些方 ...

  3. LeetCode331:验证二叉树的前序序列化(递归)

    解题思路:把所有元素存成数组,设置一个全局下标next,表示当前节点如果要继续遍历应当从数组的哪个位置开始,然后从下标 0 开始DFS.如果DFS返回真并且next下标等于数组的长度,说明元素已经全部 ...

  4. RV1126 分区教程

    一.前言 期初我是想弄一个分区存放自己的 APP 程序,如果需要更改应用的时候,只需要烧写独立的分区即可,就不需要重新烧写 rootfs.这是一个简单的操作,为啥还需要记录了,因为我在里面遇到了一些坑 ...

  5. 解密视频魔法:将ExternalOES纹理转化为TEXTURE_2D纹理

    在使用OpenGL ES进行图形图像开发时,我们常使用GL_TEXTURE_2D纹理类型,它提供了对标准2D图像的处理能力.这种纹理类型适用于大多数场景,可以用于展示静态贴图.渲染2D图形和进行图像处 ...

  6. hello Flask最简单的Flask项目

    # 1.导包 from flask import Flask # 2.实例化Flask对象.一般变量名都叫app,大家都是这样用,很多扩展插件的文档也是叫app,所以统一都叫app. # __name ...

  7. Centos7 Zabbix3.2安装(yum)

    http://repo.zabbix.com/zabbix/3.2/rhel/7/x86_64/  #官网下载地址(只包含zabbix的应用包) ftp://47.104.78.123/zabbix/ ...

  8. 手写spring的ioc的流程截图(笔记-1)

    spring ioc是什么? IoC 容器是 Spring 的核心,也可以称为 Spring 容器.Spring 通过 IoC 容器来管理对象的实例化和初始化,以及对象从创建到销毁的整个生命周期. S ...

  9. idea配置tomcat热部署

    idea配置tomcat热部署,点击+添加一个local的tomcat服务 点击部署tab 添加Artifact...选择 一定要选择exploded,否则没有热部署选项!!! 一定要选择explod ...

  10. Tailscale 基础教程:Headscale 的部署方法和使用教程

    Tailscale 是一种基于 WireGuard 的虚拟组网工具,它在用户态实现了 WireGuard 协议,相比于内核态 WireGuard 性能会有所损失,但在功能和易用性上下了很大功夫: 开箱 ...