Java SpringBoot 动态数据源

1. 原理

动态数据源,本质上是把多个数据源存储在一个 Map 中,当需要使用某一个数据源时,使用 key 获取指定数据源进行处理。而在 Spring 中已提供了抽象类 AbstractRoutingDataSource 来实现此功能,继承 AbstractRoutingDataSource 类并覆写其 determineCurrentLookupKey() 方法监听获取 key 即可,该方法只需要返回数据源 key 即可,也就是存放数据源的 Mapkey

因此,我们在实现动态数据源的,只需要继承它,实现自己的获取数据源逻辑即可。AbstractRoutingDataSource 顶级继承了 DataSource,所以它也是可以做为数据源对象,因此项目中使用它作为主数据源。

1.1. AbstractRoutingDataSource 源码解析

![[Pasted image 20240321103621.png]]

        public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {// 目标数据源 map 集合,存储将要切换的多数据源 bean 信息,可以通过 setTargetDataSource(Map<Object, Object> mp) 设置@Nullableprivate Map<Object, Object> targetDataSources;// 未指定数据源时的默认数据源对象,可以通过 setDefaultTargetDataSouce(Object obj) 设置@Nullableprivate Object defaultTargetDataSource;...// 数据源查找接口,通过该接口的 getDataSource(String dataSourceName) 获取数据源信息private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();//解析 targetDataSources 之后的 DataSource 的 map 集合@Nullableprivate Map<Object, DataSource> resolvedDataSources;@Nullableprivate DataSource resolvedDefaultDataSource;//将 targetDataSources 的内容转化一下放到 resolvedDataSources 中,将 defaultTargetDataSource 转为 DataSource 赋值给 resolvedDefaultDataSourcepublic void afterPropertiesSet() {//如果目标数据源为空,会抛出异常,在系统配置时应至少传入一个数据源if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");} else {//初始化 resolvedDataSources 的大小this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());//遍历目标数据源信息 map 集合,对其中的 key,value 进行解析this.targetDataSources.forEach((key, value) -> {// resolveSpecifiedLookupKey 方法没有做任何处理,只是将 key 继续返回Object lookupKey = this.resolveSpecifiedLookupKey(key);// 将目标数据源 map 集合中的 value 值(Druid 数据源信息)转为 DataSource 类型DataSource dataSource = this.resolveSpecifiedDataSource(value);// 将解析之后的 key,value 放入 resolvedDataSources 集合中this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {// 将默认目标数据源信息解析并赋值给 resolvedDefaultDataSourcethis.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);}}}protected Object resolveSpecifiedLookupKey(Object lookupKey) {return lookupKey;}protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {if (dataSource instanceof DataSource) {return (DataSource)dataSource;} else if (dataSource instanceof String) {return this.dataSourceLookup.getDataSource((String)dataSource);} else {throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);}}// 因为 AbstractRoutingDataSource 继承 AbstractDataSource,而 AbstractDataSource 实现了 DataSource 接口,所有存在获取数据源连接的方法public Connection getConnection() throws SQLException {return this.determineTargetDataSource().getConnection();}public Connection getConnection(String username, String password) throws SQLException {return this.determineTargetDataSource().getConnection(username, password);}// 最重要的一个方法,也是 DynamicDataSource 需要实现的方法protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");// 调用实现类中重写的 determineCurrentLookupKey 方法拿到当前线程要使用的数据源的名称Object lookupKey = this.determineCurrentLookupKey();// 去解析之后的数据源信息集合中查询该数据源是否存在,如果没有拿到则使用默认数据源 resolvedDefaultDataSourceDataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");} else {return dataSource;}}@Nullableprotected abstract Object determineCurrentLookupKey();}

1.2. 关键类说明

忽略掉 controller/service/entity/mapper/xml介绍。

  • application.yml:数据源配置文件。但是如果数据源比较多的话,根据实际使用,最佳的配置方式还是独立配置比较好。
  • DynamicDataSourceRegister:动态数据源注册配置文件
  • DynamicDataSource:动态数据源配置类,继承自 AbstractRoutingDataSource
  • TargetDataSource:动态数据源注解,切换当前线程的数据源
  • DynamicDataSourceAspect:动态数据源设置切面,环绕通知,切换当前线程数据源,方法注解优先
  • DynamicDataSourceContextHolder:动态数据源上下文管理器,保存当前数据源的 key,默认数据源名,所有数据源 key

1.3. 开发流程

  1. 添加配置文件,设置默认数据源配置,和其他数据源配置
  2. 编写 DynamicDataSource 类,继承 AbstractRoutingDataSource 类,并实现 determineCurrentLookupKey() 方法
  3. 编写 DynamicDataSourceHolder 上下文管理类,管理当前线程的使用的数据源,及所有数据源的 key
  4. 编写 DynamicDataSourceRegister 类通过读取配置文件动态注册多数据源,并在启动类上导入(@Import)该类
  5. 自定义数据源切换注解 TargetDataSource,并实现相应的切面,环绕通知切换当前线程数据源,注解优先级(DynamicDataSourceHolder.setDynamicDataSourceKey() > Method > Class

2. 实现

2.1. 引入 Maven 依赖

<!-- web 模块依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring 核心 aop 模块依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Druid 数据源连接池依赖 -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.8</version>
</dependency>
<!-- mybatis 依赖 -->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version>
</dependency>
<!-- mysql驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.24</version>
</dependency>
<!-- lombok 模块依赖 -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-text</artifactId><version>1.10.0</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>

2.2. application.yml 配置文件

spring:datasource:type: com.alibaba.druid.pool.DruidDataSourceurl: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding-utf8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: root
custom:datasource:names: ds1,ds2ds1:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/content_center?useUnicodeusername: rootpassword: rootds2:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/trade?useUnicodeusername: rootpassword: root

2.3. 创建 DynamicDataSource 继承 AbstractRoutingDataSource 类

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;/*** @Description: 继承Spring AbstractRoutingDataSource 实现路由切换*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 决定当前线程使用哪种数据源* @return 数据源 key*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getDataSourceType();}
}

2.4. 编写 DynamicDataSourceHolder 类,管理 DynamicDataSource 上下文

import java.util.ArrayList;
import java.util.List;/*** @Description: 动态数据源上下文管理*/
public class DynamicDataSourceHolder {// 存放当前线程使用的数据源类型信息private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<String>();// 存放数据源 keyprivate static final List<String> DATASOURCE_KEYS = new ArrayList<String>();// 默认数据源 keypublic static final String DEFAULT_DATESOURCE_KEY = "master";//设置数据源public static void setDynamicDataSourceType(String key) {DYNAMIC_DATASOURCE_KEY.set(key);}//获取数据源public static String getDynamicDataSourceType() {return DYNAMIC_DATASOURCE_KEY.get();}//清除数据源public static void removeDynamicDataSourceType() {DYNAMIC_DATASOURCE_KEY.remove();}public static void addDataSourceKey(String key) {DATASOURCE_KEYS.add(key)}/*** 判断指定 key 当前是否存在** @param key* @return boolean*/public static boolean containsDataSource(String key){return DATASOURCE_KEYS.contains(key);}
}

2.5. 编写 DynamicDataSourceRegister 读取配置文件注册多数据源

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import java.util.Objects;/*** @Description: 注册动态数据源* 初始化数据源和提供了执行动态切换数据源的工具类* EnvironmentAware(获取配置文件配置的属性值)*/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSourceRegister.class);// 指定默认数据源类型 (springboot2.0 默认数据源是 hikari 如何想使用其他数据源可以自己配置)// private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";private static final String DEFAULT_DATASOURCE_TYPE = "com.alibaba.druid.pool.DruidDataSource";// 默认数据源private DataSource defaultDataSource;// 用户自定义数据源private Map<String, DataSource> customDataSources  = new HashMap<>();/*** 加载多数据源配置* @param env 当前环境*/@Overridepublic void setEnvironment(Environment env) {initDefaultDataSource(env);initCustomDataSources(env);}/*** 初始化主数据源* @param env*/private void initDefaultDataSource(Environment env) {// 读取主数据源Map<String, Object> dsMap = new HashMap<>();dsMap.put("type", env.getProperty("spring.datasource.type", DEFAULT_DATASOURCE_TYPE));dsMap.put("driver", env.getProperty("spring.datasource.driver-class-name"));dsMap.put("url", env.getProperty("spring.datasource.url"));dsMap.put("username", env.getProperty("spring.datasource.username"));dsMap.put("password", env.getProperty("spring.datasource.password"));defaultDataSource = buildDataSource(dsMap);}/*** 初始化更多数据源* @param env*/private void initCustomDataSources(Environment env) {// 读取配置文件获取更多数据源String dsPrefixs = env.getProperty("custom.datasource.names");if (!StringUtils.isBlank(dsPrefixs)) {for (String dsPrefix : dsPrefixs.split(",")) {dsPrefix = fsPrefix.trim()if (!StringUtils.isBlank(dsPrefix)) {Map<String, Object> dsMap = new HashMap<>();dsMap.put("type", env.getProperty("custom.datasource." + dsPrefix + ".type", DEFAULT_DATASOURCE_TYPE));dsMap.put("driver", env.getProperty("custom.datasource." + dsPrefix + ".driver-class-name"));dsMap.put("url", env.getProperty("custom.datasource." + dsPrefix + ".url"));dsMap.put("username", env.getProperty("custom.datasource." + dsPrefix + ".username"));dsMap.put("password", env.getProperty("custom.datasource." + dsPrefix + ".password"));DataSource ds = buildDataSource(dsMap);customDataSources.put(dsPrefix, ds);}}}}@Overridepublic void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {Map<Object, Object> targetDataSources = new HashMap<Object, Object>();// 将主数据源添加到更多数据源中targetDataSources.put(DynamicDataSourceHolder.DEFAULT_DATASOURCE_KEY, defaultDataSource);DynamicDataSourceHolder.addDataSourceKey(DynamicDataSourceHolder.DEFAULT_DATASOURCE_KEY);// 添加更多数据源targetDataSources.putAll(customDataSources);for (String key : customDataSources.keySet()) {DynamicDataSourceContextHolder.addDataSourceKey(key);}// 创建 DynamicDataSourceGenericBeanDefinition beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(DynamicDataSource.class);beanDefinition.setSynthetic(true);MutablePropertyValues mpv = beanDefinition.getPropertyValues();mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);mpv.addPropertyValue("targetDataSources", targetDataSources);registry.registerBeanDefinition("dataSource", beanDefinition); // 注册到 Spring 容器中LOGGER.info("Dynamic DataSource Registry");}/*** 创建 DataSource* @param dsMap 数据库配置参数* @return DataSource*/public DataSource buildDataSource(Map<String, Object> dsMap) {try {Object type = dsMap.get("type");if (type == null)type = DEFAULT_DATASOURCE_TYPE;// 默认DataSourceClass<? extends DataSource> dataSourceType = (Class<? extends DataSource>)Class.forName((String)type);String driverClassName = String.valueOf(dsMap.get("driver"));String url = String.valueOf(dsMap.get("url"));String username = String.valueOf(dsMap.get("username"));String password = String.valueOf(dsMap.get("password"));// 自定义 DataSource 配置DataSourceBuilder<? extends DataSource> factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url).username(username).password(password).type(dataSourceType);return factory.build();}catch (ClassNotFoundException e) {e.printStackTrace();}}
}

2.6. 在启动器类上添加 @Import,导入 register 类

// 注册动态多数据源
@Import({ DynamicDataSourceRegister.class })
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

2.7. 自定义注解 @TargetDataSource

/*** 自定义多数据源切换注解* 优先级:DynamicDataSourceHolder.setDynamicDataSourceKey() > Method > Class*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{/*** 切换数据源名称*/public String value() default DynamicDataSourceHolder.DEFAULT_DATESOURCE_KEY;
}

2.8. 定义切面拦截 @TargetDataSource

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;@Aspect
// 保证在 @Transactional 等注解前面执行
@Order(-1)
@Component
public class DataSourceAspect {// 设置 DataSource 注解的切点表达式@Pointcut("@annotation(com.ayi.config.datasource.DynamicDataSource)")public void dynamicDataSourcePointCut(){}//环绕通知@Around("dynamicDataSourcePointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable{String key = getDefineAnnotation(joinPoint).value();if (!DynamicDataSourceHolder.containsDataSource(key)) {LOGGER.error("数据源[{}]不存在,使用默认数据源[{}]", key, DynamicDataSourceHolder.DEFAULT_DATESOURCE_KEY)key = DynamicDataSourceHolder.DEFAULT_DATESOURCE_KEY;}DynamicDataSourceHolder.setDynamicDataSourceKey(key);try {return joinPoint.proceed();} finally {DynamicDataSourceHolder.removeDynamicDataSourceKey();}}/*** 先判断方法的注解,后判断类的注解,以方法的注解为准* @param joinPoint 切点* @return TargetDataSource*/private TargetDataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();TargetDataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(TargetDataSource.class);if (Objects.nonNull(methodSignature)) {return dataSourceAnnotation;} else {Class<?> dsClass = joinPoint.getTarget().getClass();return dsClass.getAnnotation(TargetDataSource.class);}}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/10087.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

跨境电商行业蓬勃发展,武汉星起航引领卖家孵化新潮流

近年来&#xff0c;我国跨境电商行业在政府的大力扶持下呈现出强劲的发展势头。随着国内制造业结构的加速调整与居民消费需求升级态势的持续凸显&#xff0c;跨境出口规模占比稳步提升&#xff0c;跨境进口规模同样不断扩大&#xff0c;行业市场规模持续增长。在这一背景下&…

系列介绍:《创意代码:Processing艺术编程之旅》

系列介绍&#xff1a;《创意代码&#xff1a;Processing艺术编程之旅》 标题创意&#xff1a; “代码绘梦&#xff1a;Processing艺术编程入门”“数字画布&#xff1a;用Processing创造视觉奇迹”“编程美学&#xff1a;Processing艺术创作指南”“创意编程&#xff1a;Proc…

nginx开启目录索引搭建文件服务器

目录索引 ngx_http_autoindex_module 模块可以支持目录列表浏览&#xff0c;开启方式 location / {autoindex on; }开启后就可以通过浏览器访问目录下的文件列表并且可以下载&#xff0c;像很多镜像资源站一样。可以文件浏览下载。这时候如果对某个目录不是所有用户可以访问下…

QT学习(4)——自定义控件

目录 引出自定义一个控件自定义控件定义方法函数widget窗口调用函数 总结 引出 QT学习&#xff08;4&#xff09;——自定义控件 自定义一个控件 自定义控件定义方法函数 #include "smallwid.h" #include "ui_smallwid.h"SmallWid::SmallWid(QWidget *par…

redis抖动问题导致延迟或者断开的处理方案

目录&#xff1a; 1、使用背景2、redis重试机制3、redis重连机制4、其他一些解决redis抖动问题方案 1、使用背景 客户反馈文件偶现打不开&#xff0c;报错现象是session not exist&#xff0c;最终定位是redis抖动导致的延迟/断开的现象&#xff0c;最终研发团方案是加入redis…

C# 集合(三) —— Stack/BitArray类

总目录 C# 语法总目录 集合三 Stack/BitArray 1. Stack2. BitArray 1. Stack 栈&#xff0c;先进后出 Stack<string> strArr new Stack<string>(); strArr.Push("tom"); strArr.Push("jerry"); strArr.Push("lily"); Console.Wri…

Mapreduce | 案例

根据提供的数据文件【test.log】 数据文件格式&#xff1a;姓名,语文成绩,数学成绩,英语成绩 完成如下2个案例&#xff1a; &#xff08;1&#xff09;求每个学科的平均成绩 &#xff08;2&#xff09;将三门课程中任意一门不及格的学生过滤出来 &#xff08;1&#xff09;求每…

Navicat安装配置(注册码)连接MySQL

下载资源 博主给你打包好了安装包&#xff0c;在网盘里&#xff0c;防止你下载到钓鱼软件 快说谢谢博主&#xff08;然后心甘情愿的点个赞~&#x1f60a;&#xff09; navicatformysql.zip_免费高速下载|百度网盘-分享无限制 (baidu.com) 安装流程 ①下载好压缩包后并解压 ② …

【JavaEE精炼宝库】多线程1(认识线程 | 创建线程 | Thread 类)

目录 一、认识线程 1.1 线程的概念&#xff1a; 1.2 为什么需要线程&#xff1a; 1.3 面试题.谈谈进程和线程的区别&#xff1a; 1.4 Java的线程和操作系统线程的关系&#xff1a; 二、创建线程 2.1 创建线程的5种写法&#xff1a; 2.1.1 写法1.继承 Thread 类&#xf…

【redis】Redis五种常用数据类型和内部编码,以及对String字符串类型的总结

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…

827. 最大人工岛

827. 最大人工岛 题目链接&#xff1a;827. 最大人工岛 代码如下&#xff1a; class Solution { public:int largestIsland(vector<vector<int>>& grid) {unordered_map<int,int> gridNum;int ngrid.size(), m grid[0].size();bool isAllGridtrue;//…

C++ | Leetcode C++题解之第80题删除有序数组中的重复项II

题目&#xff1a; 题解&#xff1a; class Solution { public:int removeDuplicates(vector<int>& nums) {int n nums.size();if (n < 2) {return n;}int slow 2, fast 2;while (fast < n) {if (nums[slow - 2] ! nums[fast]) {nums[slow] nums[fast];slo…

【doghead】mac与wsl2联通

mbp 设置为发送端,那么要能与windows上 wsl2的ubutnu通信。 mbp的 uv 构建ok zhangbin@zhangbin-mbp-2  ~/tet/Fargo/zhb-bifrost/Bifrost-202403/worker/third_party/libuv   main clion使用lldb cmake构建 更新git2.45.0啊

【八股系列】Loader和Plugin的区别是什么?

Loader 是一个转换器&#xff0c;它将源代码从一种格式转换成另一种格式。例如&#xff0c;你可以使用 Loader 将 TypeScript 代码转换成 JavaScript 代码。Loader 通常在 module.rules 配置中指定。 常用的一些 Loader&#xff1a; babel-loader: 用于将 ES6 代码转换成 ES5 …

盘点自动驾驶的技术发展趋势

自动驾驶技术在不断发展变快&#xff0c;我们之前提过算法岗如今越来越卷&#xff0c;从今年的就业局势看&#xff0c;前年还属于蓝海行业的自动驾驶&#xff0c;今年就已经满满关上了招揽之门——呈红海之势。作为在这个行业中摸爬滚打的一以子&#xff0c;我们到底该如何纵观…

会员卡积分收银源码系统 支持多门店系统 带完整的安装代码包以及安装搭建教程

在数字化浪潮的推动下&#xff0c;传统零售行业面临着巨大的转型压力。为了满足现代消费者多样化的需求&#xff0c;提高门店管理效率和顾客满意度&#xff0c;小编给大家分享一款会员卡积分收银源码系统——支持多门店系统&#xff0c;并附带了完整的安装代码包以及安装搭建教…

君正T31移植电源IC—CW2015芯片简介

CW2015芯片简介 CW2015 是一款超紧凑、低成本、主机侧/电池组侧、无传感电阻器的电量计量系统 IC&#xff0c;适用于手持和便携式设备中的锂离子 (Li) 电池。CW2015 包括一个14 位Sigma-Delta ADC、一个精密电压基准和内置准确温度传感器。该 IC 允许最终用户消除占用大量电路板…

【Vue3进阶】- Pinia

什么是Pinia Pinia 是 Vue 的专属状态管理库&#xff0c;它允许你跨组件或页面共享状态。它提供了类似于 Vuex 的功能&#xff0c;但比 Vuex 更加简单和直观。 需要在多个组件之间共享状态或数据时使用 Pinia 的 store&#xff0c;这样可以避免 props 和 eventBus 等传统方法…

【stm32笔记】DSP库调用

参考&#xff1a;DSP库调用 , __CC_ARM,__TARGET_FPU_VFP, __FPU_PRESENT1U, ARM_MATH_CM4 ,USE_HAL_DRIVER,STM32F407xx,ARM_MATH_CM4,__FPU_USED1U,__FPU_PRESENT1U,__CC_ARM,ARM_MATH_MATRIX_CHECK,ARM_MATH_ROUNDING把需要的库复制出来单独用&#xff0c;方便移植

KEIL declaration may not appear after executable statement in block

KEIL declaration may not appear after executable statement in block 这个问题也是比较经典&#xff0c;就是不允许你的变量定义位置不允许在下边的代码区域&#xff0c;只允许在最上方 ‍ 修改编码模式为C99解决 ‍ ​​