Mybaties-Plus saveBatch()、自定义批量插入、多线程批量插入性能测试和对比

一.背景

最近在做一个项目的时候,由于涉及到需要将一个系统的基础数据全量同步到另外一个系统中去,结果一看,基础数据有十几万条,作为小白的我,使用单元测试,写了一段代码,直接采用了MP(Mybaties-Plus)自带的saveBatch()方法,将基础数据导入到新的系统中去,但是后面涉及多次修正基础数据的情况,导致,每次重新插入数据或者更新的时候,都需要花费十几分钟的时间,后面想着以下的方案进行了优化。
其实针对自带的saveBatch()方法插入很慢,一般都是由于数据库连接url上没有配置批量操作的属性,只需要在url上加上如下属性即可,如下:

rewriteBatchedStatements=true

在配置数据库连接信息的时候,配置类似如下:

jdbc:mysql://数据库地址/数据库名?useUnicode=true&characterEncoding=UTF8&allowMultiQueries=true&rewriteBatchedStatements=true

加上之后,你就会发现,saveBatch的速度直线提升,效果还是很不错的,一万条数据估计也就在几百毫秒。
接下来的文章都是设置在rewriteBatchedStatements=false情况下,且MP(Mybaties-Plus)为3.5.3.1版本下进行测试的。

二 .优化方法

如果在 rewriteBatchedStatements=false情况下,使用自带的方法,插入几十万数据是比较慢的,我们先讲解自带的方法,再讲解MP给我们自定义空间的自定义方法,然后在加入一些多线程的情况下进行的测试和方案比较。

2.1 Mybaties-plus自带的批量saveBatch()方法

直接上代码
实体类如下:

@Data
@TableName("test_user")
public class TestUser implements Serializable {private String id;private String name;private String managerId;private String salary;private String age;private String departId;private String remark;private String province;
}

Mapper如下:

public interface TestUserMapper extends BaseMapper<TestUser> {
}

Service如下:

public interface ITestUserService extends IService<TestUser> {
}@Service
public class TestUserServiceImpl extends ServiceImpl<TestUserMapper, TestUser> implements ITestUserService {
}

接下来我使用单元测试的方法,构造200000条数据,测试Mybaties-Plus自带的saveBatch()方法,代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = JeecgSystemApplication.class)
public class UserTest {@Autowiredprivate ITestUserService userService;@Testpublic void testInsertBatch(){List<TestUser> userList = new ArrayList<>();for(int i = 0; i < 199999; i++){TestUser user = new TestUser();user.setName("张三");user.setAge("20");user.setProvince("重庆市");user.setSalary("200000");user.setRemark("diitch");userList.add(user);}long s = System.currentTimeMillis();userService.saveBatch(userList);System.out.println("保存200000条数据消耗" + (System.currentTimeMillis() - s) + "ms");}}

测试结果如下,大概需要10s中的时间:
在这里插入图片描述
我们可以跟踪源码,它的实现如下:

  default boolean saveBatch(Collection<T> entityList) {return this.saveBatch(entityList, 1000);}public boolean saveBatch(Collection<T> entityList, int batchSize) {String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE);return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> {sqlSession.insert(sqlStatement, entity);});}public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]);return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> {int size = list.size();int idxLimit = Math.min(batchSize, size);int i = 1;for(Iterator var7 = list.iterator(); var7.hasNext(); ++i) {   ## 循环执行E element = var7.next();consumer.accept(sqlSession, element);if (i == idxLimit) {sqlSession.flushStatements();idxLimit = Math.min(idxLimit + batchSize, size);}}});}
2.2 自定义批量插入或者更新的方法

直接上代码,首先我们自定义一个RootMapper, 继承BaseMapper,自定义自己的批量插入或者更新方法,如下:

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;/*** @author diitich* @param <T>*/
public interface RootMapper<T> extends BaseMapper<T> {/*** 批量新增* @param batchList* @return*/int insertBatch(@Param("list") Collection<T> batchList);/*** 批量跟新* @param batchList* @return*/int updateBatch(@Param("list")Collection<T> batchList);}

定义InsertBatchColumn 继承 AbstractMethod ,下面基本就是一些通用的写法,不同的Mybatis-plus有一点点区别,本文用的版本为3.5.3.1版本,代码如下:

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.util.List;
import java.util.function.Predicate;@Slf4j
public class InsertBatchColumn extends AbstractMethod {@Setter@Accessors(chain = true)private Predicate<TableFieldInfo> predicate;public InsertBatchColumn() {super("insertBatch");}public InsertBatchColumn(Predicate<TableFieldInfo> predicate) {// 此处的名称必须与后续的RootMapper的新增方法名称一致super("insertBatch");this.predicate = predicate;}@SuppressWarnings("Duplicates")@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;SqlMethod sqlMethod = SqlMethod.INSERT_ONE;List<TableFieldInfo> fieldList = tableInfo.getFieldList();String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true,false) +this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true,ENTITY_DOT, false) +this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA);String keyProperty = null;String keyColumn = null;// 表包含主键处理逻辑,如果不包含主键当普通字段处理if (tableInfo.havePK()) {if (tableInfo.getIdType() == IdType.AUTO) {/* 自增主键 */keyGenerator = Jdbc3KeyGenerator.INSTANCE;keyProperty = tableInfo.getKeyProperty();keyColumn = tableInfo.getKeyColumn();} else {if (null != tableInfo.getKeySequence()) {keyGenerator = TableInfoHelper.genKeyGenerator(modelClass.getName(), tableInfo, builderAssistant);keyProperty = tableInfo.getKeyProperty();keyColumn = tableInfo.getKeyColumn();}}}String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);// 注意第三个参数,需要与后续的RootMapper里面新增方法名称要一致,不然会报无法绑定异常return this.addInsertMappedStatement(mapperClass, modelClass, "insertBatch", sqlSource, keyGenerator, keyProperty, keyColumn);}
}

定义 UpdateBatchColumn 继承 AbstractMethod ,代码如下:

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;public class UpdateBatchColumn extends AbstractMethod {public UpdateBatchColumn(String methodName) {super(methodName);}@SuppressWarnings("Duplicates")@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {String sql = "<script>\n<foreach collection=\"list\" item=\"item\" separator=\";\">\nupdate %s %s where %s=#{%s} %s\n</foreach>\n</script>";String additional = tableInfo.isWithVersion() ? tableInfo.getVersionFieldInfo().getVersionOli("item", "item.") : "" + tableInfo.getLogicDeleteSql(true, true);String setSql = sqlSet(tableInfo.isWithLogicDelete(), false, tableInfo, false, "item", "item.");String sqlResult = String.format(sql, tableInfo.getTableName(), setSql, tableInfo.getKeyColumn(), "item." + tableInfo.getKeyProperty(), additional);SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);// 第三个参数必须和RootMapper的自定义方法名一致return this.addUpdateMappedStatement(mapperClass, modelClass, "updateBatch", sqlSource);}

自定义sql注入,MysqlInjector继承DefaultSqlInjector ,代码如下:

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import java.util.List;public class MysqlInjector extends DefaultSqlInjector {@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {List<AbstractMethod> methods = super.getMethodList(mapperClass,tableInfo);// 自定义的insert SQL注入器methods.add(new InsertBatchColumn());// 自定义的update SQL注入器,参数需要与RootMapper的批量update名称一致methods.add(new UpdateBatchColumn("updateBatch"));return methods;}
}

定义MybatiesPlus的配置文件,将 MysqlInjector 注入进去,代码如下:

import org.jeecg.common.sqlinject.MysqlInjector;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatiesPlusConfig {@Beanpublic MysqlInjector sqlInjector(){return new MysqlInjector();}
}

接下来我们还是使用单元测试,构造200000万条数据,当然我们不能一次性插入20万条数据,进行分段插入,代码如下:

public interface TestUserMapper extends RootMapper<TestUser> {
}

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = JeecgSystemApplication.class)
public class UserTest {@Autowiredprivate TestUserMapper testUserMapper;/*** 测试自定义批量新增*/@Testpublic void testInsertBatchCustom(){List<TestUser> userList = new ArrayList<>();int batchSize = 5000; // 每批次插入的数据量long s = System.currentTimeMillis();for(int i = 0; i < 199999; i++){TestUser user = new TestUser();user.setName("张三");user.setAge("20");user.setProvince("重庆市");user.setSalary("200000");user.setRemark("diitch");userList.add(user);// 达到批次大小时进行插入if(userList.size() == batchSize){testUserMapper.insertBatch(userList);userList.clear(); // 清空列表,准备下一批数据}}
// 插入剩余数据if(!userList.isEmpty()){testUserMapper.insertBatch(userList);}System.out.println("保存200000条数据消耗" + (System.currentTimeMillis() - s) + "ms");}}

上面的代码我们设置了一次性批量插入batchSize = 5000,执行结果如下,大概需要4~5秒,batchSize值设置不同,执行效率稍微有点不同:
在这里插入图片描述

2.3 多线程更新 + MP自带saveBatch()方法

上面我们讲了自定义批量插入大概能提升一倍的性能,接下来我们使用多线程方式更新数据,首先我们先测试使用5个线程插入20万条数据,使用Mybaties-plus自带的saveBatch()方法更新,直接上代码:

import org.jeecg.JeecgSystemApplication;
import org.jeecg.modules.demo.test.entity.TestUser;
import org.jeecg.modules.demo.test.mapper.TestUserMapper;
import org.jeecg.modules.demo.test.service.ITestUserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = JeecgSystemApplication.class)
public class UserTest {@Autowiredprivate ITestUserService userService;@Autowiredprivate TestUserMapper testUserMapper;@Testpublic void testInsertBatchMulThreadSaveBatch() throws Exception{int totalRecords = 199999;int batchSize = 5000;int threadCount = 5; // 可以根据实际情况调整线程数量ExecutorService executor = Executors.newFixedThreadPool(threadCount);List<Future<Void>> futures = new ArrayList<>();long s = System.currentTimeMillis();for (int i = 0; i < totalRecords; i += batchSize) {int startIndex = i;int endIndex = Math.min(i + batchSize, totalRecords);List<TestUser> batchList = new ArrayList<>();for (int j = startIndex; j < endIndex; j++) {TestUser user = new TestUser();user.setName("张三");user.setAge("20");user.setProvince("重庆市");user.setSalary("200000");user.setRemark("diitch");batchList.add(user);}Future<Void> future = executor.submit(() -> {userService.saveBatch(batchList);return null;});futures.add(future);}// 等待所有线程执行完成for (Future<Void> future : futures) {future.get();}executor.shutdown();System.out.println("保存200000条数据消耗" + (System.currentTimeMillis() - s) + "ms");}}

执行结果如下,大概需要3s多:
在这里插入图片描述

2.4 多线程 + 自定义批量插入方法

接下来我们还是使用5个线程来插入数据,只是使用我们自己定义的批量插入方法来插入数据,代码如下:

import org.jeecg.JeecgSystemApplication;
import org.jeecg.modules.demo.test.entity.TestUser;
import org.jeecg.modules.demo.test.mapper.TestUserMapper;
import org.jeecg.modules.demo.test.service.ITestUserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = JeecgSystemApplication.class)
public class UserTest {@Autowiredprivate ITestUserService userService;@Autowiredprivate TestUserMapper testUserMapper;@Testpublic voidtestInsertBatchMulThreadCustom() throws Exception{int totalRecords = 199999;int batchSize = 5000;int threadCount = 5;ExecutorService executor = Executors.newFixedThreadPool(threadCount);List<Future<Void>> futures = new ArrayList<>();Set<String> insertedData = Collections.synchronizedSet(new HashSet<>());long s = System.currentTimeMillis();for (int i = 0; i < totalRecords; i += batchSize) {int startIndex = i;int endIndex = Math.min(i + batchSize, totalRecords);List<TestUser> batchList = new ArrayList<>();for (int j = startIndex; j < endIndex; j++) {TestUser user = new TestUser();user.setName("张三");user.setAge("20");user.setProvince("重庆市");user.setSalary("200000");user.setRemark("diitch");batchList.add(user);}List<TestUser> filteredList = batchList.stream().filter(user -> !insertedData.contains(user.getName())).collect(Collectors.toList());Future<Void> future = executor.submit(() -> {testUserMapper.insertBatch(filteredList);filteredList.forEach(user -> insertedData.add(user.getName()));return null;});futures.add(future);}// 等待所有线程执行完成for (Future<Void> future : futures) {future.get();}executor.shutdown();System.out.println("保存200000条数据消耗" + (System.currentTimeMillis() - s) + "ms");}
}

执行结果如下,大概需要2s左右时间
在这里插入图片描述

三.总结

一般我们设置rewriteBatchedStatements=true时,批量插入功能已经相对较快,如果还满足不了需求,我们可以使用多线程进行批量插入,下面是在设置rewriteBatchedStatements=true时,插入20万条数据saveBatch()以及 saveBatch + 多线程的方式的执行结果:
单独的saveBatch()方法,差不多也是4秒多,也达到了我们自定义的批量插入方法性能:
在这里插入图片描述
saveBatch() + 多线程的方法,执行结果如下,大概只需要1秒多,比我们自定义批量插入 + 多线程方法还要快:

在这里插入图片描述

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

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

相关文章

element---tree树形结构(返回的数据与官方的不一样)

项目中要用到属性结构数据&#xff0c;后端返回的数据不是官方默认的数据结构&#xff1a; <el-tree:data"treeData":filter-node-method"filterNode":props"defaultProps"node-click"handleNodeClick"></el-tree>这是文档…

SpringCloudGateway全局过滤器

文章目录 全局过滤器的作用自定义全局过滤器过滤器执行的顺序 上一篇 Gateway理论与实践 介绍的过滤器&#xff0c;网关提供了31种&#xff0c;但每一种过滤器的作用都是固定的。如果我们希望拦截请求&#xff0c;做自己的业务逻辑则没办法实现。 全局过滤器的作用 全局过滤器的…

怎样添加、移除、移动、复制、创建和查找节点?

1&#xff09;创建新节点 createDocumentFragment() //创建一个 DOM 片段createElement() //创建一个具体的元素createTextNode() //创建一个文本节点 2&#xff09;添加、移除、替换、插入 appendChild() //添加removeChild() //移除replaceChild() //替换insertBefore() /…

高级语言讲义2010计专(仅高级语言部分)

1.编写一程序&#xff0c;对输入的正整数&#xff0c;求他的约数和。 如&#xff1a;18的约数和为1236939 #include <stdio.h>int getsum(int n){int i,sum0;for(i1;i<n;i)if(n%i0)sumi;return sum; } int main(){int sum getsum(18);printf("%d",sum); …

JS直接量及其相关对象

什么是直接量 直接量是指不需要创建对象就可以直接使用的变量。ES中的直接量主要有三种类型&#xff1a;表示字符串的string类型、表示数字的number类型和表示true/false的boolean类型。当我们直接将值赋给变量后&#xff0c;ES就会自动判断其类型&#xff0c;而且当参数发生变…

Android14之编译输出system/product/vendor/odm分区(一百九十一)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

机器学习流程—数据预处理 Encoding

机器学习流程—数据预处理 Encoding 在机器学习中,我们经常会遇到分类变量,这些分量变量往往机器学习模型没有办法从中学习,往往有两种,一种是字符型,一种是数值型。通常需要对分类型变量做一些处理,常用的方法有两种:label encoding和one hot encoding。 例如,假设数…

「AI工程师」数据处理与分析-工作指导

工作指导书 一、工作职责 负责数据的收集、清洗、整合和处理,确保数据质量和准确性。进行数据分析和挖掘,提取有价值的信息,为业务决策提供支持。构建和维护数据处理和分析的流程和工具,提高数据处理效率。与其他团队成员合作,共同解决数据处理和分析过程中遇到的问题。二…

深度学习相关概念及术语总结

目录 1.CNN2.RNN3.LSTM4.NLP5.CV6.正向传播7.反向传播8.sigmoid 函数9.ReLU函数10.假设函数11.损失函数12.代价函数 1.CNN CNN 是卷积神经网络&#xff08;Convolutional Neural Network&#xff09;的缩写。卷积神经网络是一种深度学习模型&#xff0c;专门用于处理具有网格状…

kotlin筑基(2)

文章目录 一、internal二、infix三、object四、open五、operator六、sealed class1. 用法一2. 用法二 七、typealias八、when1. 带参数2. 不带参数 一、internal internal 修饰方法&#xff0c;表示这个方法只适合当前 module 使用&#xff0c;同理修饰 class 也是一样。 Syn…

mysql的time_zone导致性能下降

mysql的 time_zone有哪些 MySQL 的 time_zone 变量可以设置为多种不同的时区值。这些值通常由字母和数字组成&#xff0c;代表世界上不同的地理区域和它们的标准时间。以下是一些常见的 time_zone 设置选项&#xff1a; UTC&#xff1a;协调世界时&#xff0c;是国际标准时间…

2024年3月ZZUACM 招新赛题解

2024年3月ZZUACM 招新赛 题号题目A区间次大值B上课签到C魔法森林&#xff08;一&#xff09;D魔法森林&#xff08;二&#xff09;ELOPF跳格子G猜数字H抽卡记录I安达的二维矩阵J安达的数字手术K跳楼梯L前缀和 A 区间次大值—循环/签到题 题目描述 给定一个 n n n的全排列 a i…

顶顶通呼叫中心中间件(基于FreeSWITCH)对接mrcp asr 和 tts配置

介绍 顶顶通呼叫中心中间件通过asrproxy程序对接各种asr和tts,本文主要讲解 asrproxy程序 对接 mrcp v1 和 mrcp v2的配置。 asrproxy.json配置 asr配置 "asr":{"interface": {"mrcp": {"count": 0,"fault_threshold": 0…

外包干了10天,技术退步明显···

先说一下自己的情况&#xff0c;本科生&#xff0c;通过校招进入杭州某软件公司&#xff0c;干了接近3年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&…

【鸿蒙开发】第十七章 Web组件(一)

1 Web概述 Web组件用于在应用程序中显示Web页面内容&#xff0c;为开发者提供页面加载、页面交互、页面调试等能力。 页面加载&#xff1a;Web组件提供基础的前端页面加载的能力&#xff0c;包括&#xff1a;加载网络页面、本地页面、html格式文本数据。 页面交互&#xff1a…

Java必须掌握的clear【】(含面试大厂题和源码)

面试题&#xff1a;“如何在Java中清除一个数组中的所有元素&#xff1f;” 在Java中&#xff0c;清除一个数组中的所有元素并不意味着逐个删除数组的每个元素&#xff0c;因为这在数组中是不可能做到的。数组的大小在初始化之后是固定的&#xff0c;无法改变。因此&#xff0…

IOS面试题object-c 1-10

1、简述Object-C的理解与特性&#xff1f; OC 作为一门 面向对象 的语言&#xff0c;自然具有面向对象的语言特性&#xff1a;封装、继承、多态。 它既具有 静态语言的特性&#xff08;如C&#xff09;&#xff0c;又有 动态语言的效率&#xff08;动态绑定、动态加载等&#…

JAVA使用的工具类-Hutool

文章目录 Hutool工具类简介1&#xff1a;身份证工具类相关方法1.1 身份证脱敏处理 字符串补零处理(此处是JAVA类的方法&#xff0c;并无引用StrUtil)springboot前后端分离&#xff0c;后端返回json字符串带斜杠问题处理 在这里整理一下博主常用的工具类方法-hutool工具类,这里囊…

制定一份完美的测试计划,让您的产品质量更上一层楼!

大家好&#xff0c;我是彭于晏。今天学习测试计划如何书写。 虽然很多人日常工作中都知道测试计划是什么&#xff0c;但是写好测试计划&#xff0c;其实并不容易。今天就来一起学习下测试计划如何书写。 什么是测试计划&#xff1f; 测试计划是一份为软件产品所准备的详细文档…

目标检测——监控下打架检测数据集

一、简述 首先&#xff0c;监控下打架检测是维护公共安全的重要手段。在公共场所、学校、监狱等地方&#xff0c;打架事件往往难以避免。通过安装打架检测监控系统&#xff0c;可以实时监控并准确识别打架事件&#xff0c;及时采取必要的应对措施&#xff0c;有效地减少打架事…