Java对象通用比对工具

目录

背景

思路

实现


背景

  前段时间的任务中,遇到了需要识别两个对象不同属性的场景,如果使用传统的一个个属性比对equals方法,会存在大量的重复工作,而且为对象新增了属性后,比对方法也需要同步修改,不方便维护,于是便打算构造一个通用的比对方法,需要支持对象嵌套,并支持过滤比对逻辑的策略,适用大部分比对场景

思路

  一个java对象,他的属性是多样的,会有集合类型(如Collection,Map,Array)基本类型(如String,int,double,long等认为是最基本对象的比较),或者对象类型,本质上是一个树状的结构

怎么去遍历对象树

常用的方法就是递归,那怎么定义递归的终点?基于上面三种类型,定为基本类型的比对为递归的终点,对于集合类型,那就再对每一个元素进行比较,对于对象类型,遍历属性进行比较

为了达成通用目的,这里使用java反射实现,基于约定和性能考虑,只通过无参的getter方法获取属性进行比较,另外如何记录当前属性所在的路径?这里参考spel表达式,相对轻量直观

实现

首先定义递归过程记录的上下文信息

这里记录了根对象,当前处理的对象,以及反射获取的对象信息

protected static class CompareContext {//对比结果private List<CompareResult.CompareInfo> resCollect;//当前遍历的对象信息,用于定位private ArrayDeque<String> pathMessageStack;protected Object rootSourceObj;protected Object rootOtherObj;protected Object sourceObj;protected Object otherObj;protected Object sourceInvokeVal;protected Object otherInvokeVal;//过滤耗费时间private long filterCost;private String checkCycle() {Set<String> set = new LinkedHashSet<>();//出现重复退出while (set.add(pathMessageStack.removeLast())) {}String[] elements = new String[set.size()];Iterator<String> iterator = set.iterator();int index = 0;while (iterator.hasNext()) {elements[set.size() - 1 - index++] = iterator.next();}return getPath(elements);}protected String getPath(String[] elements) {Object obj = this.rootSourceObj == null ? this.rootOtherObj : this.rootSourceObj;String simpleName = obj.getClass().getSimpleName();StringBuilder builder = new StringBuilder(simpleName);if (elements == null) {elements = this.pathMessageStack.toArray(new String[0]);}for (int i = elements.length - 1; i >= 0; i--) {String cur = elements[i];String value = cur.substring(2);if (cur.startsWith(FIELD_SIGN)) {builder.append(".").append(value);} else {builder.append("[").append(value).append("]");}}return builder.toString();}}

基于这个上下文,定义过滤接口,因为有些属性可能不用比对,如果命中过滤接口,则跳过

@FunctionalInterface
public interface MethodFilter {boolean isSkipCompare(Method method, String methodKey, CompareUtil.CompareContext compareContext);
}

比对结果类,主要存放每个比对不通过属性的值及其路径信息

@Getter
@ToString
public class CompareResult {protected boolean same;protected Object source;protected Object other;protected List<CompareInfo> diffList;//=====额外信息,可以不关注====//整体对比耗时private long totalCostMill;//方法过滤耗时protected long methodFilterCostMill;protected void genCost(long start) {this.totalCostMill = System.currentTimeMillis() - start;}@Getterpublic static class CompareInfo {protected Object sourceVal;protected Object otherVal;protected String path;protected boolean isShow() {if (sourceVal == null || otherVal == null) {return true;}if (CompareUtil.getObjType(sourceVal) != Object.class) {return true;}if (sourceVal instanceof Collection) {return ((Collection<?>) sourceVal).size() != ((Collection<?>) otherVal).size();}if (sourceVal instanceof Map) {return ((Map<?, ?>) sourceVal).size() != ((Map<?, ?>) otherVal).size();}if (sourceVal.getClass().isArray()) {return ((Object[]) sourceVal).length != ((Object[]) otherVal).length;}return false;}@Overridepublic String toString() {return String.format("path:%s,source:[%s],other:[%s]", path, sourceVal, otherVal);}}public String getBaseObjDiffInfo() {return getBaseObjDiffInfo("\n");}/*** 过滤出为空的父对象或基本对象,原生的diffList会包含父对象,不方便查看** @param seperator* @return*/public String getBaseObjDiffInfo(String seperator) {if (!same) {StringBuilder builder = new StringBuilder();diffList.stream().filter(CompareInfo::isShow).forEach(v -> builder.append(v).append(seperator));return builder.toString();}return "";}}

随后定义入口方法

  public static <T> CompareResult compareObjByGetter(T source, T other, MethodFilter... methodFilters) {List<CompareResult.CompareInfo> diffList = new ArrayList<>();CompareContext compareContext = new CompareContext();compareContext.resCollect = diffList;compareContext.pathMessageStack = new ArrayDeque<>(MAX_DEEP_SIZE);compareContext.rootSourceObj = source;compareContext.rootOtherObj = other;long start = System.currentTimeMillis();boolean res = compareObjByGetter(source, other, compareContext, methodFilters);CompareResult compareResult = new CompareResult();compareResult.genCost(start);compareResult.diffList = diffList;compareResult.same = res;compareResult.other = other;compareResult.source = source;compareResult.methodFilterCostMill = compareContext.filterCost;return compareResult;}

主流程方法,流程如下

  1. 校验
    1. 路径长度校验
    2. 类校验
  2. 递归终点,对于基本类型,直接走比对方法,并生成路径
  3. 对于复合类型,递归进行比对逻辑
  4. 对于对象类型,采用反射获取具体值,并通过方法过滤进行比较
    1. 首先判断是否有public的无参数getter方法
    2. 有的话采用调用该方法获取两边的值,设置上下文信息
    3. 过滤器过滤无需比对的字段
    4. 如果任一方为空,构造结果
    5. 对于集合类型的属性,需要进一步获取里面的元素进行递归处理
    6. 对于对象类型或基本类型,重复上述步骤

主流程代码如下

private static <T> boolean compareObjByGetter(T source, T other, CompareContext compareContext, MethodFilter... methodFilters) {//对比路径太深或出现循环引用不支持if (compareContext.pathMessageStack.size() > MAX_DEEP_SIZE) {String path = compareContext.checkCycle();if (!StringUtils.isEmpty(path)) {//路径仅供参考,不一定准确throw new IllegalStateException(String.format("reference cycle happen,please check your object,path:%s", path));}throw new IllegalStateException(String.format("compare path over max size:%s,please check your object", MAX_DEEP_SIZE));}if(source==other){return true;}if (source == null || other == null) {generateResult(source, other, compareContext);return false;}if (!source.getClass().equals(other.getClass())) {throw new IllegalArgumentException(String.format("not the same object,class source:%s,class other:%s,path:%s", source.getClass(), other.getClass(),compareContext.getPath(null)));}//基本类型不再对比getter方法if (getObjType(source) != Object.class) {boolean isSame = compareBaseObj(source, other);if (!isSame) {generateResult(source, other, compareContext);}return isSame;}//复合类型if (isCollectionOrMapOrArray(source)) {return dealWithCMA(source, other, compareContext, methodFilters);}//对象类型,遍历对应的方法final boolean[] val = new boolean[]{true};ReflectionUtils.doWithMethods(source.getClass(), new ReflectionUtils.MethodCallback() {@Overridepublic void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {String name = method.getName();if (method.getModifiers() == Modifier.PUBLIC && (name.startsWith("get") || name.startsWith("is"))) {//有入参的getter不处理if (method.getParameterTypes().length != 0) {return;}String methodKey = method.getDeclaringClass().getName() + "#" + method.getName();Object sourceInvokeVal = null;Object otherInvokeVal = null;try {sourceInvokeVal = method.invoke(source);otherInvokeVal = method.invoke(other);} catch (Exception e) {throw new RuntimeException(e);}compareContext.otherObj = other;compareContext.otherInvokeVal = otherInvokeVal;compareContext.sourceObj = source;compareContext.sourceInvokeVal = sourceInvokeVal;//过滤,methodFilter是||关系long start = System.currentTimeMillis();try {for (MethodFilter methodFilter : methodFilters) {if (methodFilter.isSkipCompare(method, methodKey, compareContext)) {return;}}} finally {compareContext.filterCost += System.currentTimeMillis() - start;}if (sourceInvokeVal == null && otherInvokeVal == null) {return;}compareContext.pathMessageStack.push(String.format("%s%s", FIELD_SIGN, ColumnSelector.getFieldName(method.getName(), method.getDeclaringClass().getName())));if (sourceInvokeVal == null || otherInvokeVal == null) {generateResult(sourceInvokeVal, otherInvokeVal, compareContext);val[0] = false;compareContext.pathMessageStack.pop();return;}if (isCollectionOrMapOrArray(sourceInvokeVal)) {val[0] &= dealWithCMA(sourceInvokeVal, otherInvokeVal, compareContext, methodFilters);} else {//对象类型 or 基本类型val[0] &= compareObjByGetter(sourceInvokeVal, otherInvokeVal, compareContext, methodFilters);}compareContext.pathMessageStack.pop();}}});return val[0];}

对于集合类型的处理如下

主要根据各自的特性取出元素,再给到上面的主流程去执行

 private static boolean dealWithCMA(Object sourceObj, Object otherObj, CompareContext compareContext, MethodFilter... methodFilters) {if(sourceObj==otherObj){return true;}boolean isDiff = true;if (sourceObj instanceof Collection) {Collection<?> sourceCollection = ((Collection<?>) sourceObj);Collection<?> otherCollection = ((Collection<?>) otherObj);//要求顺序,这里不做排序if (sourceCollection.size() != otherCollection.size()) {isDiff = false;} else {Iterator<?> sourceI = sourceCollection.iterator();Iterator<?> otherI = otherCollection.iterator();int index = 0;while (sourceI.hasNext()) {Object sourceElement = sourceI.next();Object otherElement = otherI.next();//下一层不匹配的值compareContext.pathMessageStack.push(String.format("%s%s", COLLECTION_SIGN, index++));isDiff &= compareObjByGetter(sourceElement, otherElement, compareContext, methodFilters);compareContext.pathMessageStack.pop();}}}if (sourceObj.getClass().isArray()) {Object[] sourceArray = (Object[]) sourceObj;Object[] otherArray = (Object[]) otherObj;if (sourceArray.length != otherArray.length) {isDiff = false;} else {for (int i = 0; i < sourceArray.length; i++) {Object sourceElement = sourceArray[i];Object otherElement = otherArray[i];compareContext.pathMessageStack.push(String.format("%s%s", ARRAY_SIGN, i));isDiff &= compareObjByGetter(sourceElement, otherElement, compareContext, methodFilters);compareContext.pathMessageStack.pop();}}}if (sourceObj instanceof Map) {Map<?, ?> sourceMap = (Map) sourceObj;Map<?, ?> otherMap = (Map) otherObj;if (sourceMap.size() != otherMap.size()) {isDiff = false;} else {HashSet<?> otherKeySet = new HashSet<>(otherMap.keySet());for (Map.Entry<?, ?> entry : sourceMap.entrySet()) {Object sourceKey = entry.getKey();Object otherVal = otherMap.get(sourceKey);otherKeySet.remove(sourceKey);compareContext.pathMessageStack.push(String.format("%s%s", KEY_SIGN, sourceKey));isDiff &= compareObjByGetter(entry.getValue(), otherVal, compareContext, methodFilters);compareContext.pathMessageStack.pop();}if (!otherKeySet.isEmpty()) {for (Object otherKey : otherKeySet) {compareContext.pathMessageStack.push(String.format("%s%s", KEY_SIGN, otherKey));isDiff &= compareObjByGetter(null, otherMap.get(otherKey), compareContext, methodFilters);compareContext.pathMessageStack.pop();}}}}if (!isDiff) {generateResult(sourceObj, otherObj, compareContext);}return isDiff;}

对于基本对象判断,如果返回object类型需要进一步解析判断,后续如果有其他基本对象,比如业务自定义的基本对象也可以往里面加,或者再进行优化,通过本地线程的方式动态指定基本对象类型

/*** 后续比对有其他基本类型可以往里加* @param obj* @return*/protected static Class<?> getObjType(Object obj) {if (obj instanceof Integer) {return Integer.class;} else if (obj instanceof String) {return String.class;} else if (obj instanceof BigDecimal) {return BigDecimal.class;} else if (obj instanceof Long) {return Long.class;} else if (obj instanceof Enum) {return Enum.class;} else if (obj instanceof Double) {return Double.class;} else if (obj instanceof Boolean) {return Boolean.class;}return Object.class;}

对于基本类型的比较方法

private static <T> boolean compareBaseObj(T obj, T other) {//同一个对象直接通过if(obj==other){return true;}//数字类型if (obj instanceof Number && obj instanceof Comparable) {return ((Comparable<T>) obj).compareTo(other) == 0;}//其他类型return Objects.equals(obj, other);}

效果

Java对象比对功能实现大概如此,来看看效果

定义了比对对象

@Getterpublic static class ComposeObj {private String id;private Object[] arrays;private List<Object> list;private Map<String, Object> map;private ComposeObj son;private int idLength;public ComposeObj(String id) {this.id = id;this.idLength = id.length();}}

测试代码

 ComposeObj sourceObj = new ComposeObj("source");ComposeObj arrayObj = new ComposeObj("A1");sourceObj.arrays = new Object[]{arrayObj, new ComposeObj("A22")};sourceObj.list = Arrays.asList(new ComposeObj("C1"), new ComposeObj("C11"));Map<String, Object> map = new HashMap<>();map.put("test", "test");map.put("test2", "test2");sourceObj.map = map;ComposeObj  otherObj = new ComposeObj("other");ComposeObj arrayObj2 = new ComposeObj("A2");otherObj.arrays = new Object[]{arrayObj2, new ComposeObj("A22")};otherObj.list = Arrays.asList(new ComposeObj("C2"), new ComposeObj("C11"));Map<String, Object> map2 = new HashMap<>();map2.put("test", "test2");map2.put("test2", "test22");otherObj.map = map2;

测试方法

  CompareResult compareResult = CompareUtil.compareObjByGetter(sourceObj, otherObj);assert checkPath(compareResult);assert compareResult.getDiffList().size() == 9;log.info(compareResult.getBaseObjDiffInfo());

结果

默认的支持的表达式过滤器如下

/*** 通过简单的表达式* "*abc|xxx" ->标识以abc结尾或xxx的属性忽略,不做比对,*代表通配符*/
public class RegexFieldFilter implements MethodFilter {private static final Cache<String, Pattern> REGEX_MAP = CacheBuilder.newBuilder().softValues().maximumSize(2048).build();private final List<String> rules;private static final String WILDCARD = "*";//是否缓存结果,如果调用次数较多(比如属性为list,且实际场景数量较多)可以用启用private boolean cacheResult = false;//方法keyprivate Cache<String, Boolean> resultMap;private final Set<String> equalsField = new HashSet<>();private final Map<String, String> prefixFieldMap = new HashMap<>();private final Map<String, String> suffixFieldMap = new HashMap<>();private RegexFieldFilter(String regex, boolean cacheResult) {this.cacheResult = cacheResult;rules = Splitter.on("|").splitToList(regex);for (String rule : rules) {//普通比对if (!rule.contains(WILDCARD)) {equalsField.add(rule);}//首尾通配符特殊逻辑if (onlyOneWildcard(rule)) {if (rule.startsWith(WILDCARD)) {suffixFieldMap.put(rule, rule.substring(1));}if (rule.endsWith(WILDCARD)) {prefixFieldMap.put(rule, rule.substring(0, rule.length() - 1));}}}if (cacheResult) {resultMap = CacheBuilder.newBuilder().softValues().maximumSize(1024).build();}}public static RegexFieldFilter of(String regex, boolean cacheResult) {return new RegexFieldFilter(regex, cacheResult);}public static RegexFieldFilter of(String regex) {return of(regex, false);}private boolean canSkip(String rule, String fieldName) {if (rule.contains(WILDCARD)) {if (suffixFieldMap.containsKey(rule)) {return fieldName.endsWith(suffixFieldMap.get(rule));}if (prefixFieldMap.containsKey(rule)) {return fieldName.startsWith(prefixFieldMap.get(rule));}//在中间或多个通配符String replace = StringUtils.replace(rule, WILDCARD, ".*");Pattern pattern = REGEX_MAP.asMap().computeIfAbsent(replace, Pattern::compile);return pattern.matcher(fieldName).matches();}return equalsField.contains(fieldName);}private boolean onlyOneWildcard(String rule) {if (!rule.contains(WILDCARD)) {return false;}return rule.indexOf(WILDCARD, rule.indexOf(WILDCARD) + 1) == -1;}@Overridepublic boolean isSkipCompare(Method method, String methodKey, CompareUtil.CompareContext context) {return cacheResult ? resultMap.asMap().computeIfAbsent(methodKey, s -> judgeSkip(method)) : judgeSkip(method);}private boolean judgeSkip(Method method) {if (!CollectionUtils.isEmpty(rules)) {String name = method.getName();String fieldName = CompareUtil.getFieldName(name, method.getDeclaringClass().getCanonicalName());for (String rule : rules) {if (canSkip(rule, fieldName)) {return true;}}}return false;}}

测试代码如下,定义这个过滤会把属性名为arrays,带有'l',或者ma开头的属性都过滤掉,不参与比对逻辑

  boolean cacheResult = false;RegexFieldFilter of = RegexFieldFilter.of("arrays|*l*|ma*", cacheResult);CompareResult compareResult = CompareUtil.compareObjByGetter(source, other, of);assert checkPath(compareResult);assert compareResult.getDiffList().size() == 15;

ComopareUtil完整代码

https://github.com/97lele/redis-aux/tree/dev/common/src/main/java/com/xl/redisaux/common/utils/compare

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

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

相关文章

百度、谷歌、必应收录个人博客网站

主要是给各个搜索引擎提交你的sitemap文件&#xff0c;让别人能搜到你博客的内容。 主题使用的Butterfly。 生成sitemap 安装自动生成sitemap插件。 npm install hexo-generator-sitemap --save npm install hexo-generator-baidu-sitemap --save在站点配置文件_config.yml…

自动化测试报告pytest-html样式美化

最近我将 pytest-html 样式优化了 一版 先看优化前&#xff1a; 优化后&#xff1a; 优化内容包括&#xff1a; 删除部分多余字段新增echart图表部分字体大小、行间距、颜色做了美化调整运行环境信息移至报告最后部分字段做了汉化处理&#xff08;没全部翻译是因为&#xf…

力扣爆刷第161天之TOP100五连刷71-75(搜索二叉树、二维矩阵、路径总和)

力扣爆刷第161天之TOP100五连刷71-75&#xff08;搜索二叉树、二维矩阵、路径总和&#xff09; 文章目录 力扣爆刷第161天之TOP100五连刷71-75&#xff08;搜索二叉树、二维矩阵、路径总和&#xff09;一、98. 验证二叉搜索树二、394. 字符串解码三、34. 在排序数组中查找元素的…

分享20个python学习要点

1 类型 在python中&#xff0c;一切都是对象&#xff0c;每个对象都有一个类型。 检查对象类型的常用方式。 <type> type(<el>) 或 <el>.__class__&#xff1a; 这两行代码都是获取对象的类型。type(<el>)会返回<el>的类型&#xff0c;而&l…

如何将资源前端通过 Docker 部署到远程服务器

作为一个程序员&#xff0c;在开发过程中&#xff0c;经常会遇到项目部署的问题&#xff0c;在现在本就不稳定的大环境下&#xff0c;前端开发也需要掌握部署技能&#xff0c;来提高自己的生存力&#xff0c;今天就详细说一下如何把一个前端资源放到远程服务器上面通过docker部…

C++入门(C语言过渡)

文章目录 前言一、C关键字二、命名空间三、C输入&输出四、缺省参数五、函数重载六、引用七、inline八、nullptr总结 前言 C是一种通用的、高级的、静态类型的编程语言&#xff0c;它在20世纪80年代由丹尼斯里奇创建的C语言基础上发展而来。以下是C发展的一些重要里程碑。 1…

kafka 生产者

生产者 生产者负责创建消息&#xff0c;然后将其投递到Kafka中。 负载均衡 轮询策略。随机策略。按照 key 进行hash。 Kafka 的默认分区策略&#xff1a;如果指定了 key&#xff0c;key 相同的消息会发送到同一个分区&#xff08;分区有序&#xff09;&#xff1b;如果没有…

const 修饰不同内容区分

1.修饰局部变量 const int a 1;int const a 1; 这两种是一样的 注意&#xff1a; const int b; 该情况下编译器会报错&#xff1a;常量变量"b”需要初始值设定项 将一个变量没有赋初始值直接const修饰后&#xff0c;在以后时无法更改内容的。 2.修饰常量字符串 a.…

【密码学基础】基于LWE(Learning with Errors)的全同态加密方案

学习资源&#xff1a; 全同态加密I&#xff1a;理论与基础&#xff08;上海交通大学 郁昱老师&#xff09; 全同态加密II&#xff1a;全同态加密的理论与构造&#xff08;Xiang Xie老师&#xff09; 现在第二代&#xff08;如BGV和BFV&#xff09;和第三代全同态加密方案都是基…

第10章:网络与信息安全

目录 第10章&#xff1a;网络与信息安全 网络概述 计算机网络概念 计算机网络的分类 网络的拓扑结构 ISO/OSI网络体系结构 网络互联硬件 物理层互联设备 数据链路层互联设备 网络层互联设备 应用层互联设备 网络的协议与标准 网络标准 TCP/IP协议族 网络接口层协…

浅谈反射机制

1. 何为反射&#xff1f; 反射&#xff08;Reflection&#xff09;机制指的是程序在运行的时候能够获取自身的信息。具体来说&#xff0c;反射允许程序在运行时获取关于自己代码的各种信息。如果知道一个类的名称或者它的一个实例对象&#xff0c; 就能把这个类的所有方法和变…

PyQt5显示QImage并将QImage转换为PIL图像保存到缓存

PyQt5显示QImage并将QImage转换为PIL图像保存到缓存 1、效果图 2、流程 1、获取摄像头资源,打开摄像头 2、截取图像 3、opencv读的通道是BGR,要转成RGB 4、往显示视频的Label里显示QImage 5、将QImage转换为PIL图像,并保存到缓存 6、获取图像中人脸信息3、代码 # -*- codin…

python-22-零基础自学python-数据分析基础 打开文件 读取文件信息

学习内容&#xff1a;《python编程&#xff1a;从入门到实践》第二版 知识点&#xff1a; 读取文件 、逐行读取文件信息等 练习内容&#xff1a; 练习10-1:Python学习笔记 在文本编辑器中新建一个文件&#xff0c;写几句话来总结一下你至此学到的Python知识&#xff0c;其中…

Docker-11☆ Docker Compose部署RuoYi-Cloud

一、环境准备 1.安装Docker 附:Docker-02-01☆ Docker在线下载安装与配置(linux) 2.安装Docker Compose 附:Docker-10☆ Docker Compose 二、源码下载 若依官网:RuoYi 若依官方网站 鼠标放到"源码地址"上,点击"RuoYi-Cloud 微服务版"。 跳转至G…

vue对axios进行请求响应封装

一、原因 像是在一些业务逻辑上&#xff0c;比如需要在请求之前展示loading效果&#xff0c;或者在登录的时候判断身份信息&#xff08;token&#xff09;等信息有没有过期&#xff0c;再者根据服务器响应回来的code码进行相应的提示信息。等等在请求之前&#xff0c;之后做的一…

创建react的脚手架

Create React App 中文文档 (bootcss.com) 网址&#xff1a;creat-react-app.bootcss.com 主流的脚手架&#xff1a;creat-react-app 创建脚手架的方法&#xff1a; 方法一&#xff08;JS默认&#xff09;&#xff1a; 1. npx create-react-app my-app 2. cd my-app 3. …

【深度学习练习】心脏病预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、什么是RNN RNN与传统神经网络最大的区别在于&#xff0c;每次都会将前一次的输出结果&#xff0c;带到下一隐藏层中一起训练。如下图所示&#xff1a; …

comsol随机材料参数赋值

comsol随机材料参数赋值 在comsol中定义外部matlab函数 在comsol中定义外部matlab函数 首选项&#xff0c;安全性&#xff0c;允许 材料中&#xff0c;将杨氏模量更改为变量函数 计算 应力有波动&#xff0c;可见赋值成功 也可以看到赋值的材料参数&#xff1a;

CnosDB:深入理解时序数据修复函数

CnosDB是一个专注于时序数据处理的数据库。CnosDB针对时序数据的特点设计并实现了三个强大的数据修复函数&#xff1a; timestamp_repair – 对时间戳列进行有效修复&#xff0c;支持插入、删除、不变等操作。value_repair – 对值列进行智能修复&#xff0c;根据时间戳间隔和…

Unity | Shader基础知识(第十七集:学习Stencil并做出透视效果)

目录 一、前言 二、了解unity预制的材质 三、什么是Stencil 四、UGUI如何使用Stencil&#xff08;无代码&#xff09; 1.Canvas中Image使用Stencil制作透视效果 2.学习Stencil 3.分析透视效果的需求 五、模型如何使用Stencil 1.shader准备 2.渲染顺序 3.Stencil代码语…