news 2026/9/15 20:31:38

Spring Data Sort 与 QueryDSL 排序转换:从原理到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Data Sort 与 QueryDSL 排序转换:从原理到工程实践

1. 混合使用 Spring Data Sort 与 QueryDSL 时的排序痛点

最近在重构一个订单查询接口时,遇到一个非常典型的问题:接口入参是 Spring Data 体系的Pageable,前端通过?sort=createdAt,desc&sort=user.name,asc这样的参数传递排序条件,但查询部分已经全面切换到 QueryDSL,用的是JPAQueryFactory。结果就是 Pageable 里的Sort在 QueryDSL 这边根本没法直接用,只能眼睁睁看着 Sorting 失效。

1.1 两边排序体系的设计差异

先说清楚这两个东西的本质区别。Spring Data 的Sort是一个与实现技术无关的排序描述对象,它只表达语义:按什么属性排序、升序还是降序、是否忽略大小写、空值排前还是排后。真正执行排序时,由 Spring Data JPA 的Querydsl工具类把它翻译成 JPA Criteria 或 JPQL。

而 QueryDSL 的OrderSpecifier是 QueryDSL 类型体系里的排序表达式,它直接持有一个Expression对象,表达的是"对某个具体路径表达式应用某个排序方向"。差异点在于:Spring Data Sort 的属性是字符串,比如"user.name",是运行期拼接的;OrderSpecifier 的属性路径通常来自 Q 类,是编译期生成的类型安全路径。

这两个体系各有用武之地,但一混用就难受。Spring Data Sort 的好处是能和Pageable无缝配合,Controller 层可以直接绑定;OrderSpecifier 的好处是在手写 JPAQuery 时能确保类型安全、不会打错字段名。实际情况往往是:想保留 Pageable 的便捷,又不想放弃 QueryDSL 的动态查询能力,于是就需要在中间做一层转换。

1.2 典型场景:手写 JPAQuery 却收到 Pageable 的 Sort

我重构的这个订单查询接口,查询条件非常多,状态、时间范围、关键字、金额区间,加起来有十几个参数。用 Spring Data JPA 的JpaSpecificationExecutor写是可以,但 Predicate 的构造逻辑在复杂场景下很痛苦,团队后来统一改成了 QueryDSL。

Controller 层的代码长这样:

@GetMapping("/orders") public Page<OrderDTO> search(OrderSearchRequest request, Pageable pageable) { return orderService.search(request, pageable); }

Service 层拿到Pageable后,传给 Repository 实现类。Repository 实现类里是手写的JPAQuery

JPAQuery<Order> query = queryFactory.selectFrom(order) .leftJoin(order.user, user) .fetchJoin(); // 各种 where 条件拼接 if (StringUtils.hasText(request.getOrderNo())) { query.where(order.orderNo.contains(request.getOrderNo())); } // 排序怎么办?pageable.getSort() 不能直接塞进来

在网上搜过一圈,最常见的做法是手动解析 Sort 里的每个Sort.Order,然后逐个 new 一个OrderSpecifier。思路没问题,但网上很多代码写得都比较简陋,要么没处理ignoreCase,要么没处理NULLS FIRST/LAST,要么嵌套属性解析有问题。用在一个简单接口上没问题,一旦排序字段涉及关联对象属性、或者前端传了带点分路径的排序字段,就暴露出各种边界问题。

1.3 为什么直接沿用 Spring Data 的转换实现不满足需求

有人可能会问:Spring Data JPA 内部不也是做类似转换吗?直接把它的实现复制过来不就行了?

Spring Data JPA 的Querydsl类里确实有一个toOrderSpecifier方法,但它依赖一个重要的前提:需要把 Sort 的属性名和实体类的 Q 类绑定起来。它在SimpleEntityPathResolver内部查找实体路径,再结合PathInformation去解析属性。这套机制绑定的是 Spring Data JPA 的 Repository 代理逻辑,复用起来耦合很重,不是拿过来就能作为独立工具类用的。

另一个问题是,Spring Data JPA 对 Sort 属性的合法性校验、JpaSort.unsafe() 的特殊处理,都假设排序最终在 Spring Data 自己的查询执行链路上走。而我们手写 JPAQuery 的场景,排序要交给 QueryDSL 执行,两套语义在"属性不存在时怎么报错""空值默认策略是什么"上都可能不一致。

所以更务实的方案是写一个独立、通用、可控的转换器,只依赖 QueryDSL 自身的 API,不依赖 Spring Data JPA 的实现细节。这也是下面要展开的核心内容。

2. 通用转换器实现:PathBuilder 桥接两种排序

2.1 PathBuilder 为什么适合做这个桥

QueryDSL 给动态查询提供了一个关键类:PathBuilder。它允许你在运行期根据字符串属性名解析出对应的Path表达式。比如:

PathBuilder<Order> pathBuilder = new PathBuilder<>(Order.class, "order"); Path<Object> createdAtPath = pathBuilder.get("createdAt");

createdAtPathQOrder.order.createdAt在最终的 JPQL 生成上作用类似。有了这个机制,Spring Data Sort 里的字符串属性名就能被翻译成 QueryDSL 能识别的路径表达式。

PathBuilder 尤其适合干这个事的原因有三个:

  1. 它不依赖具体的 Q 类。只要传入实体类型和变量名,就能动态解析任意属性路径。
  2. 它支持点分嵌套路径。pathBuilder.get("user.name")能生成order.user.name这样的嵌套属性表达式。
  3. 它不是 QueryDSL 代码生成器的产物,本身就是为运行期场景设计的,通用性和扩展性都更好。

当然,类型安全上它就比不过 Q 类了。属性写错不会在编译期报错,只能运行期暴露,这点后面章节会专门讲怎么防守。

2.2 完整的 Sort2QuerydslConverter 代码

先写一个最基础的版本,把骨架搭起来:

public class Sort2QuerydslConverter { public static List<OrderSpecifier<?>> convert(Sort sort, PathBuilder<?> pathBuilder) { if (sort == null || sort.isUnsorted()) { return Collections.emptyList(); } List<OrderSpecifier<?>> orderSpecifiers = new ArrayList<>(); for (Sort.Order order : sort) { OrderSpecifier<?> specifier = convertOrder(order, pathBuilder); if (specifier != null) { orderSpecifiers.add(specifier); } } return orderSpecifiers; } private static OrderSpecifier<?> convertOrder(Sort.Order order, PathBuilder<?> pathBuilder) { String property = order.getProperty(); if (!StringUtils.hasText(property)) { return null; } boolean ascending = order.isAscending(); Order direction = ascending ? Order.ASC : Order.DESC; // 构造目标路径:支持 order.user.name 这种点分嵌套 Path<Object> targetPath = pathBuilder.get(property); OrderSpecifier<?> specifier = new OrderSpecifier<>(direction, targetPath); // 空值处理:对齐 Spring Data 的 NullHandling 语义 if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) { specifier = specifier.nullsFirst(); } else if (order.getNullHandling() == Sort.NullHandling.NULLS_LAST) { specifier = specifier.nullsLast(); } return specifier; } }

注意这里有个类型细节:pathBuilder.get(property)的返回值是Path<Object>,泛型是 Object。在 QueryDSL 里OrderSpecifier要求第一个参数是Order枚举,第二个参数是Expression<T>,用Path<Object>完全可以构造OrderSpecifier<Object>。在最终生成 JPQL 时,QueryDSL 会根据路径元数据感知到实际字段类型,这里不需要过于纠结泛型类型对不对。

2.3 在 Repository 实现类中怎么挂接

有了转换器,业务代码就干净了。以订单查询为例:

@Override public Page<Order> search(OrderSearchRequest request, Pageable pageable) { QOrder order = QOrder.order; QUser user = QUser.user; JPAQuery<Order> query = queryFactory .selectFrom(order) .leftJoin(order.user, user) .fetchJoin(); // 动态条件拼接 if (StringUtils.hasText(request.getOrderNo())) { query.where(order.orderNo.contains(request.getOrderNo())); } if (StringUtils.hasText(request.getUserName())) { query.where(user.name.contains(request.getUserName())); } // 排序:把 Pageable 的 Sort 转成 OrderSpecifier if (pageable.getSort().isSorted()) { PathBuilder<Order> pathBuilder = new PathBuilder<>(Order.class, "order"); List<OrderSpecifier<?>> orderSpecifiers = Sort2QuerydslConverter.convert(pageable.getSort(), pathBuilder); query.orderBy(orderSpecifiers.toArray(new OrderSpecifier[0])); } // 分页 long total = query.fetchCount(); List<Order> content = query .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); return new PageImpl<>(content, pageable, total); }

这里有两点要特别说清楚。

第一,new PathBuilder<>(Order.class, "order")的第二个参数"order"是变量名。它必须和QOrder.order的变量名一致,否则生成的 JPQL 里别名匹配不上,运行期直接报错。最稳妥的写法是:

PathBuilder<Order> pathBuilder = new PathBuilder<>( QOrder.order.getType(), QOrder.order.getMetadata() );

不过日常为了可读性,直接用new PathBuilder<>(Order.class, "order")也够,前提是你知道 Q 类的变量名是什么。

第二,count 查询和 fetch 查询用的是同一个 JPAQuery 实例。QueryDSL 的fetchCount()会把原查询包装成select count(*),如果查询里带了fetchJoin(),生成的 count SQL 有时会不合法。这个坑后面章节详细说,这里先记住:如果 count 有问题,单独写一个不含 fetchJoin 的 count 查询,别省那几行代码。

3. 边界处理:ignoreCase、NullHandling 与嵌套属性

基础版转换器能用,但离"通用"还有距离。排序场景里常见的边界情况,至少有这么几个:忽略大小写、空值排序策略、嵌套属性解析。

3.1 ignoreCase 只对字符串有效,否则会踩类型坑

Spring Data 的Sort.Order有一个ignoreCase()方法,语义是排序时忽略大小写。在数据库层面,JPA 会生成order by lower(column)之类的表达式。

QueryDSL 里对字符串路径做忽略大小写排序,要用StringExpression.lower()

if (order.isIgnoreCase()) { StringPath stringPath = pathBuilder.getString(property); if (ascending) { return stringPath.lower().asc(); } else { return stringPath.lower().desc(); } }

但这里有个隐含风险:getString(property)会强制把属性解析为 String 类型。如果前端传的排序属性实际是数值类型或日期类型,运行期就会抛类型转换异常。所以不能无脑对任意属性调用getString

我在实际项目里的处理方式是:把 ignoreCase 的处理放到一个独立的方法里,由调用方显式传入"哪些排序属性需要忽略大小写"的集合;如果属性不在这个集合里,即使order.isIgnoreCase()为 true,也按普通排序处理。这样既不会破坏接口兼容性,也避免了对所有字段做字符串强转。

改进后的代码:

public static List<OrderSpecifier<?>> convert(Sort sort, PathBuilder<?> pathBuilder, Set<String> caseInsensitiveProperties) { // ... for (Sort.Order order : sort) { if (order.isIgnoreCase() && caseInsensitiveProperties.contains(order.getProperty())) { // 走忽略大小写分支 } else { // 走普通分支 } } }

或者更简单一点:在项目规范里约定,需要忽略大小写的排序字段必须显式在配置类中声明,而不是依赖前端传参。毕竟排序字段通常就那么几个,一百个字段都能排序本身也是一种设计失误。

3.2 nullsFirst 和 nullsLast 不是所有数据库都支持

Sort.NullHandling有三种取值:NATIVENULLS_FIRSTNULLS_LAST。默认是NATIVE,即交给数据库自己决定空值默认排前面还是排后面。

QueryDSL 的OrderSpecifier也提供了.nullsFirst().nullsLast()方法,映射到 JPQL 里就是nulls firstnulls last。转换逻辑本身不复杂,上面基础版代码已经写了。

真正的坑在数据库方言上。PostgreSQL、Oracle、H2 支持NULLS FIRST/LAST语法,但 MySQL 不支持。如果生产环境是 MySQL,前端传了nullsFirst的排序,生成的 SQL 拿到 MySQL 上执行会直接语法报错。

我在一个项目里就踩过这个坑:本地开发用的 H2 数据库测试一切正常,部署到生产 MySQL 后,某个带空值排序的接口 500 了,排查了半天才发现是排序语法不兼容。

解决方案有两个方向:

  1. 在转换器里加一个开关,根据数据库方言决定是否应用 nullsFirst/nullsLast 逻辑。如果是 MySQL 系,就忽略NullHandling设置,退回默认行为。
  2. 数据库层面处理,比如 MySQL 下用order by (column is null), column这样的表达式,但 QueryDSL 的 OrderSpecifier 不太好表达这种混合逻辑,不如直接在转换层规避。

我的建议是第一种,简单直接,把兼容性问题挡在转换层之外。毕竟排序语义和数据库能力发生冲突时,业务上的排序往往是"有最好,没有也能接受"。

3.3 点分嵌套路径的正确姿势与安全校验

PathBuilder 的get(String property)方法支持点分路径吗?答案是支持。pathBuilder.get("user.name")在 QueryDSL 内部会解析为一个嵌套路径order.user.name,在最终 JPQL 里表现为order by order.user.name

这功能在测试环境看起来一切正常,因为 Spring Data Sort 也支持传user.name这种点分属性。但有个隐患:如果user是集合属性(比如用户有多个订单),在order by里访问集合关联属性是 JPQL 规范不允许的,运行期会抛异常。单值关联(@ManyToOne@OneToOne)则没问题,会自动隐式 join。

为了确保转换器的健壮性,我通常会对外提供一个白名单校验方法,在转换之前先检查排序属性是否是允许的:

public static boolean isValidSortProperty(Sort sort, Set<String> allowedProperties) { for (Sort.Order order : sort) { if (!allowedProperties.contains(order.getProperty())) { return false; } } return true; }

这个白名单机制看起来保守,实际价值很大。它同时解决了三个问题:避免前端传任意字段导致运行期报错、避免排序字段涉及多值关联导致查询异常、避免恶意传参导致数据库压力过大。

4. 实际踩过的坑:从排序结果异常到数据库报错

这一节把我在真实项目里遇到过的排序转换相关坑逐一梳理下,每个都有完整的排查链路。排序功能看起来简单,出错时的表现却往往很隐蔽。

4.1 count 查询与排序 JOIN 不一致导致的重复数据

现象:列表页刷新后偶现重复数据,但总条数不变,翻页混乱。从接口返回看,content 里的 id 有重复,totalElements 却是对的。

排查过程大概走了这么几条线:

第一步,先怀疑 SQL 本身。把 QueryDSL 打印出来的 SQL 拿到了数据库里执行,发现排序生效了,没有重复行。这就说明在数据库层面查询结果是对的。

第二步,怀疑 fetchJoin。打印 SQL 后发现,排序字段涉及order.user.name时,QueryDSL 会生成一个隐式 join。而查询里已经有一个显式的leftJoin(order.user, user),两个 join 语义不同:隐式 join 是 inner join(如果字段路径是单值关联属性),显式 join 是 left join。当排序字段的隐式 join 过滤掉了user为 null 的订单后,查询结果的行数和只做 leftJoin 时不同,但 Pageable 的 count 查询是单独执行的,它聚合逻辑不一样,于是 totalElements 和 content 就出现了错位。

真正修复的关键是让排序不产生额外的隐式 join。做法是在查询里显式声明这个 join:

JPAQuery<Order> query = queryFactory .selectFrom(order) .leftJoin(order.user, user) .fetchJoin();

然后转换器生成的排序表达式,基于显式 join 过的路径来构造,而不是让 QueryDSL 重新解析一个隐式 join。实际操作时,因为PathBuilder.get("user.name")会生成新的路径,无法直接复用 QUser.user.name,所以最稳妥的方案是把排序属性映射到已经存在的 join 路径上。

这也是为什么后面会讲"自定义属性映射"扩展。通用转换器解决 80% 场景,剩下的 20% 复杂查询需要能够手动指定排序表达式。

4.2 PathBuilder 别名与 Q 类变量不一致

现象:接口在联调环境正常,部署到测试环境后报unknown property之类的异常,或者直接 SQL 解析失败。

这类问题多半是 PathBuilder 的变量名和 Q 类的变量名不一致导致的。QueryDSL 的 JPQL 生成时,变量名必须唯一且和 from 子句一致。如果 Q 类的变量名改了,PathBuilder 没跟着改,生成的 JPQL 就会出现 "order 找不到" 的异常。

排查方法很直接:把最终 SQL 打印出来看。QueryDSL 的 JPAQuery 可以通过toString()拿到 JPQL 字符串:

log.debug("query sql: {}", query);

你会发现 JPQL 里的 from 子句是Order order,而 order by 子句却引用了另一个变量名,自然就报错了。

修复方式上面说过,最靠谱的是从 Q 类元数据构造 PathBuilder,而不是手写字符串变量名:

PathBuilder<Order> pathBuilder = new PathBuilder<>( QOrder.order.getType(), QOrder.order.getMetadata() );

getMetadata()返回的 PathMetadata 里带着正确的变量名,这样无论 Q 类怎么生成,都不会对不上。

4.3 空值排序在 MySQL 上的兼容性故障

现象:开发环境(H2)里排序正常,生产环境(MySQL 8.0)某个列表接口直接 500。错误日志明确显示是语法错误,位置在 order by 子句附近。

完整的排查链路是:先看 querydsl 打印的 SQL,发现是order by order.created_at desc nulls last。这个语法 MySQL 8.0 不支持。H2 支持,所以本地测不出来。

当时临时修复是直接把转换器里的 nullsFirst/nullsLast 逻辑去掉,但这么做等于放弃了排序空值策略,不够优雅。后来在转换器里加了数据库方言判断:

// 在启动或第一次使用时判断当前数据库类型 if (databaseType != DatabaseType.MYSQL) { // 只有支持 nulls first/last 语法的数据库才应用 NullHandling }

实现上可以注入 DataSource 或者直接读数据库连接的 metadata 来判定。这个判断只做一次,性能影响可以忽略。

这个坑想强调一个经验:用 QueryDSL 这类动态 SQL 生成框架时,不能假设 JPQL 语法在所有数据库方言上都一样。凡是和排序、null 处理、分页相关的特性,最好在目标数据库上做一轮完整的集成测试,而不是只在 H2 上自测。

5. 为了团队共用,做了一层小扩展

5.1 字段白名单:别让前端随意排序任意列

通用转换器本身是"无脑"的,给什么属性名就解析什么路径。但生产环境里,排序字段的值来自前端,这就存在两个问题:一是排序字段名合法性问题,字段不存在时 PathBuilder 要到执行阶段才报错,影响接口稳定性;二是性能问题,前端可以传任意排序,包括一些没有索引的文本列,排序耗时会直线上升。

团队内部后来在通用转换器外面包了一层:

public class OrderSearchSupport { private static final Map<Class<?>, Set<String>> WHITE_LIST = new ConcurrentHashMap<>(); public static List<OrderSpecifier<?>> convertSafe(Sort sort, PathBuilder<?> pathBuilder, Class<?> entityClass) { if (sort == null || sort.isUnsorted()) { return Collections.emptyList(); } Set<String> allowedFields = WHITE_LIST.computeIfAbsent(entityClass, OrderSearchSupport::loadConfig); for (Sort.Order order : sort) { if (!allowedFields.contains(order.getProperty())) { throw new IllegalArgumentException( "排序字段不允许: " + order.getProperty()); } } return Sort2QuerydslConverter.convert(sort, pathBuilder); } }

白名单的来源可以是配置文件、注解、或者一个显式的资源表。实际用下来,白名单机制的价值比想象中大得多,因为它把排序字段变成一个显式受控的接口约定,而不是开发者单方面相信"前端不会乱传"。

5.2 自定义属性映射:覆盖复杂 JOIN 场景

前文提到的隐式 JOIN 问题,终极解法是允许调用方为某些排序字段指定一个具体的表达式映射。这就是自定义属性映射函数:

@FunctionalInterface public interface SortFieldMapper { Expression<?> map(String property); }

转换器里增加一个重载方法:

public static List<OrderSpecifier<?>> convert(Sort sort, Function<String, Expression<?>> propertyResolver) { List<OrderSpecifier<?>> orderSpecifiers = new ArrayList<>(); for (Sort.Order order : sort) { Expression<?> expression = propertyResolver.apply(order.getProperty()); if (expression == null) { continue; } OrderSpecifier<?> specifier = new OrderSpecifier<>( order.isAscending() ? Order.ASC : Order.DESC, expression ); orderSpecifiers.add(specifier); } return orderSpecifiers; }

业务侧的用法:

Map<String, Expression<?>> sortMapping = new HashMap<>(); sortMapping.put("userName", QOrder.order.user.name); sortMapping.put("orderTime", QOrder.order.createdAt); List<OrderSpecifier<?>> specifiers = Sort2QuerydslConverter.convert(pageable.getSort(), sortMapping::get);

这样既能保留 Pageable 的接口便利性,又能把复杂 JOIN 场景的排序表达式牢牢控制在开发者手里。通用 PathBuilder 负责处理简单属性排序,自定义映射负责处理复杂业务排序,两个互不干扰。

5.3 排序字段解析错误的快速定位

最后分享一个排查排序问题的通用思路。QueryDSL 排序出错时,报错位置往往离真正问题根源比较远,比如在底层 SQL 执行时才暴露。这时候最快的方法永远是看最终生成的 JPQL/SQL。

在实际项目里,我会在转换器里加一个 debug 日志,把每个 Sort.Order 被解析成了什么表达式打印出来:

log.debug("Sort property [{}] -> target expression [{}]", order.getProperty(), targetPath);

同时把整个 JPAQuery 的 toString() 输出:

log.debug("Generated JPQL: {}", query);

这两行日志在平时看没什么用,真出问题的时候能大幅缩短排查时间。尤其是那种"开发环境正常、测试环境报错"的问题,基本靠对比 SQL 就能快速定位。

排查时还有个技巧:把报错信息里的异常栈往上看,如果是IllegalArgumentException: Unsupported expression或类似的路径解析异常,基本可以断定是 PathBuilder 解析路径的问题,优先检查属性名拼写和点分路径。如果是数据库语法错误,优先检查 nullsFirst/nullsLast、字符串排序等方言差异。捋清楚这两个方向,排序问题其实不难排查。

从最开始只处理 ASC/DESC 的基础转换器,到现在带白名单校验、方言兼容、自定义映射的完整工具类,这个转换器在团队内部迭代了快一年。回过头看,排序这件事看着简单,真要做得通用、稳当,涉及的边界情况一点也不少。最后再补充一个建议:如果你也在维护类似的基础工具类,尽量把日志打全一点,按实体类分类维护白名单,并且把它当成一个独立小模块持续迭代,别指望一次性写出适用于所有场景的终极代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 20:30:53

深度强化学习驱动的交通信号灯控制:DDPG建模与仿真实践

简介&#xff1a;面向智能交通与强化学习领域初学者及进阶开发者&#xff0c;提供一套基于深度确定性策略梯度&#xff08;DDPG&#xff09;的交通信号灯控制完整源码包。项目包括详细论文与Python实现&#xff0c;涵盖环境模拟、Agent交互、神经网络构建等核心环节&#xff0c…

作者头像 李华
网站建设 2026/9/15 20:30:50

美容整形网站建设:搞定域名服务器,用免费工具优化SEO

美容整形网站建设:搞定域名服务器,用免费工具优化SEO 域名服务器搞不懂,是很多医美机构老板建站时的第一道坎。选错服务器,网站打开慢如蜗牛,客户看两眼就走了;域名没备案,直接打不开,钱白花。别慌,今天这篇干货,专门拆解美容整形网站建设里的坑,教你用 免费工具 把SEO做扎实,让网站真正带来客流。…

作者头像 李华
网站建设 2026/9/15 20:29:43

基于ASP.NET MVC的物业报修系统源码设计与部署实战

简介&#xff1a;一套基于ASP与MVC框架的小区物业报修管理系统源码&#xff0c;面向从新手到有一定经验的开发人员&#xff0c;可解决物业报修工单管理中的提交、派单、处理与状态跟踪等实际问题。项目采用Visual Studio作为开发工具&#xff0c;SQL Server作为后台数据库&…

作者头像 李华
网站建设 2026/9/15 20:27:49

变分模态分解VMD实战:MATLAB脚本参数详解与信号处理应用

简介&#xff1a;围绕变分模态分解&#xff08;VMD&#xff09;的MATLAB实现资源包&#xff0c;面向需要分析非线性、非平稳信号的科研与工程人员。压缩包内为单个VMD.m脚本&#xff0c;体积仅2KB&#xff0c;轻量易用。该算法由Dietz和Steiner于2011年提出&#xff0c;能够将实…

作者头像 李华
网站建设 2026/9/15 20:25:35

2026年高性价比显卡牌子推荐与选购攻略

我注意到你的消息中还没有填入具体的项目信息&#xff08;项目标题、项目正文、关键词、摘要描述&#xff09;。你提供的内容似乎只有模板和要求&#xff0c;并没有实际的项目数据。如果你是想让我根据示例标题"2026年显卡推荐什么牌子好性价比高&#xff1f;&#xff08;…

作者头像 李华