当前位置: 首页 > news >正文

基于注解切换、Hikari实现的SpringBoot动态数据源(支持JNDI)

实现效果

先说效果,要实现方法级别注解切换当前数据源,不设置注解时走默认数据源,同时支持JNDI源。

总体思路

Spring框架中存在一个抽象类AbstractRoutingDataSource,他是一个可以动态选择当前DataSource的路由类,我们就是要从这里入手,重新实现数据源的切换选择逻辑。然后借助注解和切面,将当前需要的数据源名称放在ThreadLocal中,需要时从当前线程取得即可完成数据源的切换。
注解部分比较简单不再详说,看AbstractRoutingDataSource。该类文档写的非常全面,自行翻译一下就可以看懂。主要看其中的几个关键方法。

setTargetDataSources

类中存在一个成员变量targetDataSources,结合之后的setTargetDataSources方法可知,这里用来保存目标数据源。
根据注释我们可以知道,targetDataSources的key可以是数据源的名字,value是相应数据源的实例。
当然这里也可是使用其他的保存方式,然后自行改写用来查找数据源的determineCurrentLookupKey方法,默认场景就足够我们使用了。所以我们要构建一个Map出来,其中key用来区分数据源的名字,value放入对应数据源的实例,有几个数据源就放几个进去。

	@Nullableprivate Map<Object, Object> targetDataSources;/*** Specify the map of target DataSources, with the lookup key as key.* The mapped value can either be a corresponding {@link javax.sql.DataSource}* instance or a data source name String (to be resolved via a* {@link #setDataSourceLookup DataSourceLookup}).* <p>The key can be of arbitrary type; this class implements the* generic lookup process only. The concrete key representation will* be handled by {@link #resolveSpecifiedLookupKey(Object)} and* {@link #determineCurrentLookupKey()}.*/public void setTargetDataSources(Map<Object, Object> targetDataSources) {this.targetDataSources = targetDataSources;}

setDefaultTargetDataSource

上面说了如何设置当前数据源,那如果在开发的时候每一个方法都要声明一下使用哪个源就太麻烦了,所以Spring提供了一个方法用来设置默认的数据源,没啥可说的,传入DataSource实例就好了。

	@Nullableprivate Object defaultTargetDataSource;/*** Specify the default target DataSource, if any.* <p>The mapped value can either be a corresponding {@link javax.sql.DataSource}* instance or a data source name String (to be resolved via a* {@link #setDataSourceLookup DataSourceLookup}).* <p>This DataSource will be used as target if none of the keyed* {@link #setTargetDataSources targetDataSources} match the* {@link #determineCurrentLookupKey()} current lookup key.*/public void setDefaultTargetDataSource(Object defaultTargetDataSource) {this.defaultTargetDataSource = defaultTargetDataSource;}

determineCurrentLookupKey

在设置好数据源之后,接下来这几个寻路方法则是能实现动态数据源切换的重点。afterPropertiesSet方法对我们以配置的数据源进行校验;如果我们在第一步配置数据源map的时候对key有特殊处理则要自己实现抽象方法resolveSpecifiedLookupKey,告诉Spring应该怎么解析这个key值;determineTargetDataSource则最终确定要使用哪一个数据源,其中有一个方法determineCurrentLookupKey需要关注,这个方法会返回当前要使用的数据源名字,但他是个抽象方法,所以我们需要给他重写一下,改为从当前线程获取数据源名称

	@Overridepublic void afterPropertiesSet() {if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");}this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());this.targetDataSources.forEach((key, value) -> {Object lookupKey = resolveSpecifiedLookupKey(key);DataSource dataSource = resolveSpecifiedDataSource(value);this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);}}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return determineTargetDataSource().getConnection(username, password);}/*** Retrieve the current target DataSource. Determines the* {@link #determineCurrentLookupKey() current lookup key}, performs* a lookup in the {@link #setTargetDataSources targetDataSources} map,* falls back to the specified* {@link #setDefaultTargetDataSource default target DataSource} if necessary.* @see #determineCurrentLookupKey()*/protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");Object lookupKey = determineCurrentLookupKey();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 + "]");}return dataSource;}/*** Determine the current lookup key. This will typically be* implemented to check a thread-bound transaction context.* <p>Allows for arbitrary keys. The returned key needs* to match the stored lookup key type, as resolved by the* {@link #resolveSpecifiedLookupKey} method.*/@Nullableprotected abstract Object determineCurrentLookupKey();

代码实现

思路理顺了,代码写起来就比较快,直接贴最后代码,部分地方保留了注释。

数据源切换注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 数据源切换注解,默认为primary*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {DataSourceEnum value() default DataSourceEnum.PRIMARY;
}

数据源切换切面

这里需要特别提醒一下,事务注解@Transactional默认处于切面代理的最后一个,所以我们需要保证数据源切换注解优先级要高于事务注解

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;/*** 数据源切换切面*/
@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {private final static Logger log = LoggerFactory.getLogger(DynamicDataSourceAspect.class);@Before(value = "@annotation(targetDataSource)")public void beforePointCut(TargetDataSource targetDataSource) {log.debug("数据源切换为 " + targetDataSource.value().getDataSourceName());DynamicDataSourceContextHolder.setDataSource(targetDataSource.value().getDataSourceName());}@After(value = "@annotation(targetDataSource)")public void afterPointCut(TargetDataSource targetDataSource) {log.debug("数据源恢复为 " + DataSourceEnum.PRIMARY.getDataSourceName());DynamicDataSourceContextHolder.clearDataSource();}
}

数据源枚举类

/*** 数据源枚举类*/
public enum DataSourceEnum {PRIMARY("primary"), SECONDARY("secondary");private final String dataSourceName;public String getDataSourceName() {return dataSourceName;}DataSourceEnum(String dataSourceName) {this.dataSourceName = dataSourceName;}
}

数据源上下文保持类

/*** 数据源上下文线程持有类*/
public class DynamicDataSourceContextHolder {/*** 存放当前线程使用的数据源类型信息*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();public static void setDataSource(String dataSourceType) {CONTEXT_HOLDER.set(dataSourceType);}public static String getDataSource() {return CONTEXT_HOLDER.get();}public static void clearDataSource() {CONTEXT_HOLDER.remove();}
}

AbstractRoutingDataSource自定义实现

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;
import java.util.Map;/*** 动态数据源切换类** @author liuenqi*/
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getDataSource();}public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {// 默认数据源super.setDefaultTargetDataSource(defaultTargetDataSource);// 所有目标数据源super.setTargetDataSources(targetDataSources);// 后处理super.afterPropertiesSet();}
}

数据源注册

注意使用jndi源的时候需要加一个特定前缀。

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
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 java.util.Objects;/*** 多数据源注册类*/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {private DataSource primaryDataSource;private DataSource secondaryDataSource;@Overridepublic void setEnvironment(Environment environment) {initPrimaryDataSource(environment);initSecondaryDataSource(environment);}/*** 组装主数据源参数,兼容jdbc-url与jndi** @param env Environment*/private void initPrimaryDataSource(Environment env) {Map<String, String> paramMap = new HashMap<>(4);if (StringUtils.isNotBlank(env.getProperty("spring.datasource.primary.url"))) {paramMap.put("url", env.getProperty("spring.datasource.primary.url"));paramMap.put("userName", env.getProperty("spring.datasource.primary.username"));paramMap.put("password", env.getProperty("spring.datasource.primary.password"));paramMap.put("driverClassName", env.getProperty("spring.datasource.primary.driver-class-name"));} else {paramMap.put("jndi", env.getProperty("spring.datasource.primary.jndi-name"));}primaryDataSource = buildDataSource(paramMap);}/*** 组装辅数据源参数,兼容jdbc-url与jndi** @param env Environment*/private void initSecondaryDataSource(Environment env) {if (StringUtils.isNotBlank(env.getProperty("spring.datasource.secondary.url"))) {Map<String, String> paramMap = new HashMap<>(4);paramMap.put("url", env.getProperty("spring.datasource.secondary.url"));paramMap.put("userName", env.getProperty("spring.datasource.secondary.username"));paramMap.put("password", env.getProperty("spring.datasource.secondary.password"));paramMap.put("driverClassName", env.getProperty("spring.datasource.secondary.driver-class-name"));secondaryDataSource = buildDataSource(paramMap);} else if (StringUtils.isNotBlank(env.getProperty("spring.datasource.secondary.jndi-name"))) {Map<String, String> paramMap = new HashMap<>(2);paramMap.put("jndi", env.getProperty("spring.datasource.secondary.jndi-name"));secondaryDataSource = buildDataSource(paramMap);}}@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {Map<Object, Object> targetDataSource = new HashMap<>(2);targetDataSource.put("primary", primaryDataSource);if (Objects.nonNull(secondaryDataSource)) {targetDataSource.put("secondary", secondaryDataSource);}// 为DynamicDataSource构造参数,注意参数顺序ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();constructorArgumentValues.addGenericArgumentValue(primaryDataSource);constructorArgumentValues.addGenericArgumentValue(targetDataSource);// 构造bean放入IOCGenericBeanDefinition beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(DynamicDataSource.class);beanDefinition.setConstructorArgumentValues(constructorArgumentValues);beanDefinition.setSynthetic(true);registry.registerBeanDefinition("dataSource", beanDefinition);}/*** 使用HikariDataSource** @param paramMap {"url":"JDBC-URL","userName":"数据库用户名","password":"密码","driverClassName":"驱动名","jndi":"jndi源"}* @return HikariDataSource*/private DataSource buildDataSource(Map<String, String> paramMap) {HikariConfig hikariConfig = new HikariConfig();if (paramMap.containsKey("url")) {hikariConfig.setJdbcUrl(paramMap.get("url"));hikariConfig.setUsername(paramMap.get("userName"));hikariConfig.setPassword(paramMap.get("password"));hikariConfig.setDriverClassName(paramMap.get("driverClassName"));} else {hikariConfig.setDataSourceJNDI("java:comp/env/" + paramMap.get("jndi"));}return new HikariDataSource(hikariConfig);}
}

application.yml配置

spring:datasource:primary:url: jdbc:mysql://xxxxxusername: xxxxpassword: xxxxxdriver-class-name: com.mysql.cj.jdbc.Driversecondary: jndi-name: jdbc/db

相关文章:

基于注解切换、Hikari实现的SpringBoot动态数据源(支持JNDI)

实现效果 先说效果&#xff0c;要实现方法级别注解切换当前数据源&#xff0c;不设置注解时走默认数据源&#xff0c;同时支持JNDI源。 总体思路 Spring框架中存在一个抽象类AbstractRoutingDataSource&#xff0c;他是一个可以动态选择当前DataSource的路由类&#xff0c;我…...

Java中的动态链接VS操作系统动态链接

在操作系统OS中为了优化内存的使用会采用一种动态链接方式&#xff0c;一个文件想要在操作系统中运行必须经过编译、汇编译、链接、装载等步骤。可以参考Java程序是怎么跑起来的。本篇主要讲解Java栈帧中动态链接部分与操作系统的的动态链接的区别与联系 操纵系统为什么需要动态…...

深入理解Linux虚拟内存管理(七)

系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核 Linux 设备驱动程序 Linux设备驱动开发详解 深入理解Linux虚拟内存管理&#xff08;一&#xff09; 深入理解Linux虚拟内存管理&#xff08;二&#xff09; 深入理解Linux虚拟内存管理&#xff08;三&#xff09; 深入理…...

GSR II 智能速度辅助系统的型式认证和系统作为独立技术单元的型式认证测试流程和技术要求

智能速度辅助系统ISA的型式认证和系统作为独立技术单元的型式认证测试流程和技术要求 补充欧洲议会和欧洲理事会第2019/2144号条例,为机动车智能速度辅助系统的型式认证和这些系统作为独立技术单元的型式认证规定了详细的测试程序和技术要求,并修订该条例的附件二 (1)(EU…...

工厂方法模式(五)

过气的&#xff0c;终究是过气了 上一章简单介绍了工厂模式(四), 如果没有看过,请观看上一章 一.工厂方法模式 工厂方法模式&#xff0c;通过定义工厂父类负责定义创建对象的公共接口&#xff0c;而子类则负责生成具体的对象。 将类的实例化&#xff08;具体产品的创建&…...

力扣笔记(每日随机一题)——最佳买卖股票时机含冷冻期

问题&#xff08;中等&#xff09; 给定一个整数数组prices&#xff0c;其中第 prices[i] 表示第 i 天的股票价格 。​ 设计一个算法计算出最大利润。在满足以下约束条件下&#xff0c;你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#xff09;: 卖出股票后&a…...

yolov5 6.1 关于 tensorrt 加速的使用以及问题说明

文章目录 1. 参考连接2. 使用说明2.1 导出加速模型2.1 使用加速模型2.2 加速参数对比 3. 问题说明3.1 在 Tensorrt 8.4.1.5 版本上使用 export.py 导出失败的问题3.2 把模型文件由 best.pt 更换成加速后的 best.engine 后&#xff0c;执行推理时标注的类别名不正确的问题3.3 导…...

SVR(支持向量机)用法介绍

一、SVR回归介绍 SVR(Support Vector Regression)是支持向量机(SVM)在回归问题中的应用。与SVM分类模型相似&#xff0c;SVR也是一种非概率性算法&#xff0c;通过使用核函数将数据映射到高维空间&#xff0c;并在该空间上寻找最优的超平面与训练数据之间的间隔最大化&#xf…...

是面试官放水,还是公司实在是太缺人?这都没挂,腾讯原来这么容易进···

本人211非科班&#xff0c;之前在字节和腾讯实习过&#xff0c;这次其实没抱着什么特别大的希望投递&#xff0c;没想到腾讯可以再给我一次机会&#xff0c;还是挺开心的。 本来以为有个机会就不错啦&#xff01;没想到能成功上岸&#xff0c;在这里要特别感谢帮我内推的同学&…...

算法模板(5):数学(1):数学知识(1)

数论 整数的整除性 [x]表示不超过x的最大整数&#xff0c;叫做取整函数或高斯函数。设整数a&#xff0c;b不同时为零&#xff0c;则存在一对整数m&#xff0c;n&#xff0c;使得 ( a , b ) a m b n (a, b) am bn (a,b)ambn。注&#xff1a;a和b的最大公因数会写成 (a, b)…...

电子行业 K 公司对接 Nexperia EDI 项目案例

项目背景 Nexperia 是一家全球领先的半导体制造商&#xff0c;专注于提供高性能、高可靠性和创新性的半导体解决方案。公司成立于2017年&#xff0c;是前飞思卡尔半导体业务的一部分&#xff0c;并在全球范围内拥有多个设计、研发和生产基地。 Nexperia 使用 EDI&#xff08;…...

chatgpt赋能python:Python如何将英文转化为中文的最佳方法

Python如何将英文转化为中文的最佳方法 介绍 在现代全球化社会中&#xff0c;国与国之间的交流越来越频繁&#xff0c;相应的语言翻译工具的需求也愈发迫切。Python是一种易于学习、快速上手的编程语言&#xff0c;适合初学者和经验丰富的程序员使用&#xff0c;在语言翻译方…...

知道这些英文文档翻译的方式吗

在工作中&#xff0c;大家有没有遇到领导交给你一份外语的文档&#xff0c;要你去观看和理解&#xff0c;但是我们看不太懂或者没啥时间去一点点翻译怎么办呢&#xff1f;我们就需要有工具来将文档翻译&#xff0c;它是一项非常实用和便捷的功能&#xff0c;它可以将文档中的文…...

供应链安全

供应链安全 目录 文章目录 供应链安全目录本节实战可信任软件供应链概述构建镜像Dockerfile文件优化镜像漏洞扫描工具&#xff1a;Trivy检查YAML文件安全配置&#xff1a;kubesec准入控制器&#xff1a; Admission Webhook准入控制器&#xff1a; ImagePolicyWebhook关于我最后…...

华硕天选4原装Windows11系统带ASUSRECOVERY恢复工厂模式安装

华硕工厂恢复系统 &#xff0c;安装结束后带隐藏分区以及机器所有驱动软件,奥创Myasus Recovery 文件地址https://pan.baidu.com/s/1Pq09oDzmFI6hXVdf8Vqjqw?pwd3fs8 提取码:3fs8 文件格式&#xff1a;5个底包(HDI KIT COM MCAFEE EDN) 1个引导工具TLK 支持ASUSRECOVERY型…...

数据库期末复习(8)并发控制

笔记 数据库DBMS并发控制(1)_旅僧的博客-CSDN博客 数据库 并发控制(2)死锁和意向锁_旅僧的博客-CSDN博客 同一个对象不能既有slock又有xlock; 冲突可串行化和锁 怎么判断是否可以进行冲突可串行化:简便的方法是优先图 只有不同对象和同一对象都是读才不能发生非串行化调…...

一文说透:低代码开发平台和零代码平台区别是什么?

低代码开发平台和零代码平台区别是什么&#xff1f; 一个简单的例子就可以解释清楚。 假设你想入住一套新房&#xff0c;回看住房变迁史&#xff1a; 最原始方式是&#xff1a;自己建造往后一点&#xff0c;交付“毛坯房”&#xff1a;开发商统一建小区&#xff0c;不需要自…...

4.将图神经网络应用于大规模图数据(Cluster-GCN)

到目前为止&#xff0c;我们已经为节点分类任务单独以全批方式训练了图神经网络。特别是&#xff0c;这意味着每个节点的隐藏表示都是并行计算的&#xff0c;并且可以在下一层中重复使用。 然而&#xff0c;一旦我们想在更大的图上操作&#xff0c;由于内存消耗爆炸&#xff0c…...

pymongo更新数据

使用 PyMongo&#xff0c;可以通过以下步骤将查询到的记录进行更新&#xff1a; 下面是一个简单的示例代码片段&#xff0c;展示如何向名为users的集合中的所有文档添加一个新字段age。 import pymongo # 连接 MongoDB client pymongo.MongoClient("mongodb://localh…...

手机软件测试规范(含具体用例)

菜单基本功能测试规范一、短消息功能测试规范测试选项操作方法观察与判断结果创建、编辑短消息并发送书写短消息1、分别使用菜单或快捷方式进入书写短消息是否有异常&#xff1b; 2、输入0个字符&#xff0c;选择、输入号码发送&#xff0c;应成功&#xff1b; 3、输入1个中文…...

Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务

通过akshare库&#xff0c;获取股票数据&#xff0c;并生成TabPFN这个模型 可以识别、处理的格式&#xff0c;写一个完整的预处理示例&#xff0c;并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务&#xff0c;进行预测并输…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业

6月9日&#xff0c;国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解&#xff0c;“超级…...

R语言速释制剂QBD解决方案之三

本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...

消防一体化安全管控平台:构建消防“一张图”和APP统一管理

在城市的某个角落&#xff0c;一场突如其来的火灾打破了平静。熊熊烈火迅速蔓延&#xff0c;滚滚浓烟弥漫开来&#xff0c;周围群众的生命财产安全受到严重威胁。就在这千钧一发之际&#xff0c;消防救援队伍迅速行动&#xff0c;而豪越科技消防一体化安全管控平台构建的消防“…...

从物理机到云原生:全面解析计算虚拟化技术的演进与应用

前言&#xff1a;我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM&#xff08;Java Virtual Machine&#xff09;让"一次编写&#xff0c;到处运行"成为可能。这个软件层面的虚拟化让我着迷&#xff0c;但直到后来接触VMware和Doc…...

加密通信 + 行为分析:运营商行业安全防御体系重构

在数字经济蓬勃发展的时代&#xff0c;运营商作为信息通信网络的核心枢纽&#xff0c;承载着海量用户数据与关键业务传输&#xff0c;其安全防御体系的可靠性直接关乎国家安全、社会稳定与企业发展。随着网络攻击手段的不断升级&#xff0c;传统安全防护体系逐渐暴露出局限性&a…...

书籍“之“字形打印矩阵(8)0609

题目 给定一个矩阵matrix&#xff0c;按照"之"字形的方式打印这个矩阵&#xff0c;例如&#xff1a; 1 2 3 4 5 6 7 8 9 10 11 12 ”之“字形打印的结果为&#xff1a;1&#xff0c;…...

李沐--动手学深度学习--GRU

1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...

OPENCV图形计算面积、弧长API讲解(1)

一.OPENCV图形面积、弧长计算的API介绍 之前我们已经把图形轮廓的检测、画框等功能讲解了一遍。那今天我们主要结合轮廓检测的API去计算图形的面积&#xff0c;这些面积可以是矩形、圆形等等。图形面积计算和弧长计算常用于车辆识别、桥梁识别等重要功能&#xff0c;常用的API…...