MyBatis动态SQL安全实践:${}与#{}的深度解析与SQL注入防御
1. 项目概述从一道课后习题到企业级安全实践最近在辅导团队新人学习Java EE特别是MyBatis框架时总会遇到第三章关于动态SQL的课后习题。这些习题看似基础但往往藏着许多新手乃至有一定经验的开发者都容易忽略的“坑”。尤其是在当前企业开发环境中安全扫描工具如奇安信等的普及让一个曾经被简单带过的知识点——${}和#{}的区别——变成了可能引发线上事故的“高危漏洞”。这道课后习题远不止是教会你如何拼接SQL字符串它更像是一把钥匙打开了理解MyBatis执行原理、SQL注入防御以及编写健壮数据访问层的大门。如果你正在学习MyBatis或者在工作中被安全报告提示了SQL注入风险却不知如何彻底解决那么这次围绕动态SQL的深度拆解正是为你准备的。我们将从一个典型的课后习题场景出发逐步深入到${}的诱惑与陷阱、#{}的魔法原理并最终给出在企业级开发中安全、高效使用动态SQL的完整方案和避坑指南。这不仅仅是一次语法学习更是一次安全编码思维的建立。2. 动态SQL核心${}与#{}的终极辨析几乎所有MyBatis的入门教程都会提到动态SQL中参数传递要用#{}因为它能防止SQL注入。而${}是字符串拼接有风险。但为什么什么时候非用${}不可安全扫描报的漏洞到底怎么修这一章我们抛开教条从根上弄明白。2.1#{}预编译的守护者#{}的工作原理是MyBatis安全性的基石。它不是简单的字符串替换。当你写下SELECT * FROM user WHERE id #{userId}时MyBatis会创建一个PreparedStatement对象。底层过程解析SQL解析与预编译JDBC驱动会将这句SQL发送给数据库数据库会对其进行解析和编译生成一个执行计划。此时#{userId}被视为一个占位符?而不是具体的值。参数传递当真正执行时MyBatis再将具体的参数值例如123通过setXXX方法如setInt安全地设置到这个占位符上。关键优势因为值是在预编译后才传入的所以无论这个值是什么内容哪怕是包含‘ OR ‘1’‘1这样的恶意字符串数据库都只会把它当作一个普通的参数值来处理而不会将其作为SQL语法的一部分进行解析。这就从根本上杜绝了SQL注入。注意#{}不仅用于WHERE条件它适用于任何需要参数值的地方如INSERT VALUES(#{name})、UPDATE SET column#{value}甚至在IN子句中需结合动态SQL标签处理列表。2.2${}直接的字符串替换器与#{}相反${}的行为简单而“危险”。它是在SQL语句被预编译之前就进行直接的字符串替换。底层过程解析字符串拼接MyBatis在处理SELECT * FROM user WHERE name ‘${userName}’时会直接从传入的参数对象中取出userName属性的值假设为“Alice”。替换生成最终SQLMyBatis会直接进行字符串拼接生成SELECT * FROM user WHERE name ‘Alice’。提交执行这条完整的、拼接好的SQL语句被发送到数据库执行。风险即刻显现如果userName来自不可信的用户输入且值为‘ OR ‘1’‘1’ --那么拼接后的SQL将变成SELECT * FROM user WHERE name ‘’ OR ‘1’‘1’ --‘--是SQL注释符这会导致条件永远为真从而泄露所有用户数据。这就是经典的SQL注入。2.3 为何有时不得不使用${}既然${}这么危险为什么MyBatis还要保留它因为它处理的是“SQL语法部分”而非“参数值”。这是理解其应用场景的关键。必须使用${}的典型场景动态表名或列名SQL的语法部分如表名、列名、ORDER BY字段无法使用预编译占位符。!-- 根据类型查询不同的统计表 -- SELECT * FROM ${tableName} WHERE year #{year}这里的${tableName}可能是“stats_2023”或“stats_2024”。重要前提这个tableName值必须是内部逻辑可控的如从枚举或配置中读取绝不能来自用户前端输入。动态排序ORDER BY同样排序字段和方向是语法。ORDER BY ${sortField} ${sortOrder}安全实践必须对sortField和sortOrder进行严格的白名单校验。例如只允许“create_time”、“amount”等有限的几个字段sortOrder只允许“ASC”或“DESC”。拼接SQL片段在一些极其复杂的动态查询中可能需要根据条件拼接完整的WHERE或JOIN片段。这需要极高的警惕性通常意味着你的数据模型或查询设计可能需要反思。实操心得在我的经验中99%的${}使用场景都可以通过优化设计来避免。例如动态表名可以通过分库分表中间件或数据库视图来屏蔽动态排序可以通过在业务层映射枚举确保传入的只能是白名单值。每次你想用${}时先问自己这个值是否100%来自系统内部、绝对可信如果不是请寻找替代方案。3. 课后习题实战安全漏洞场景还原与修复现在让我们回到常见的课后习题场景看看那些“不经意”的写法如何埋下隐患以及如何修复。3.1 漏洞场景模糊查询与排序的陷阱习题示例实现一个用户搜索功能可以根据用户名模糊搜索并支持按不同字段排序。危险写法常见于初学者select idsearchUsers resultTypeUser SELECT * FROM user WHERE 11 if testusername ! null and username ! ‘’ AND name LIKE ‘%${username}%’ /if ORDER BY ${orderBy} DESC /select漏洞分析LIKE ‘%${username}%’直接拼接用户输入的username存在SQL注入风险。攻击者可以输入“%‘ OR ‘1’‘1’ --”进行攻击。ORDER BY ${orderBy}orderBy参数直接拼接攻击者可输入诸如“1; DROP TABLE user; --”之类的值导致灾难性后果。即使不注入传入不存在的列名也会导致SQL错误。3.2 安全修复方案使用#{}与OGNL函数修复后安全写法select idsearchUsers resultTypeUser SELECT * FROM user WHERE 11 if testusername ! null and username ! ‘’ AND name LIKE CONCAT(‘%’, #{username}, ‘%’) /if ORDER BY choose when test“orderBy ‘createTime’”create_time/when when test“orderBy ‘username’”username/when otherwiseid/otherwise /choose DESC /select修复解析模糊查询使用CONCAT函数将通配符‘%’与安全的参数#{username}在数据库层面进行拼接。这样username的值始终作为参数传入无法破坏SQL结构。不同数据库的CONCAT语法可能略有差异如MySQL是CONCATOracle是||需注意兼容性。动态排序放弃危险的${}改用choose标签进行白名单映射。orderBy参数现在只是一个业务逻辑标识如“createTime”而非直接的列名。MyBatis根据这个标识选择预定义的安全的列名字符串进行拼接。这彻底杜绝了注入和无效列名的风险。3.3 使用bind标签提升可读性与兼容性对于复杂的拼接特别是涉及数据库方言时bind标签是利器。它可以在动态SQL内部创建一个变量这个变量可以在后续使用。改进的模糊查询写法select idsearchUsers resultTypeUser bind name“usernamePattern” value“‘%’ username ‘%’” / SELECT * FROM user WHERE 11 if testusername ! null and username ! ‘’ AND name LIKE #{usernamePattern} /if /select优势usernamePattern是一个在OGNL表达式中计算出的字符串但最终#{usernamePattern}仍以预编译参数的形式传入SQL。它既实现了字符串拼接的逻辑又保证了安全性。将拼接逻辑前置使主要的SQL语句更加清晰。方便处理更复杂的拼接逻辑且与数据库方言解耦拼接发生在MyBatis层而非SQL层。4. 企业级动态SQL最佳实践与安全规范在真实的企业开发中尤其是面临严格安全扫描如奇安信的环境仅知道如何修复单个点是不够的需要建立一套规范和最佳实践。4.1 代码层面强制安全编程规范静态代码扫描集成在项目的CI/CD流水线中集成SonarQube、Checkstyle或Alibaba Java Coding Guidelines等工具并配置规则直接禁止在XML映射文件中使用${}或仅允许在特定模式如“${tableName}”且tableName符合特定正则时使用。让机器在代码合并前就发现问题。Mapper接口参数设计对于排序、分组等参数定义明确的枚举类型作为接口参数而不是传递字符串。public enum SortField { CREATE_TIME(“create_time”), USER_NAME(“user_name”); private final String columnName; // getter... } ListUser searchUsers(Param(“username”) String username, Param(“sortBy”) SortField sortBy);在Service层将前端的字符串参数转换为枚举转换失败则使用默认值。这样Mapper XML中接收到的就是安全的枚举可以直接用于choose标签的判断。集中式SQL片段管理对于必须使用${}的动态表名等场景将其抽象到独立的sql片段中并在片段顶部添加清晰的警告注释说明该片段的安全前提和责任人。!-- 警告此片段使用${}tableNameParam必须为内部可控值禁止传入用户输入 -- sql id“dynamicTable” FROM ${tableNameParam} /sql4.2 MyBatis配置与插件增强防御配置defaultScriptingLanguage为RAW网上有些文章建议通过配置来限制动态SQL。实际上MyBatis的核心防御在于理解并正确使用#{}。更有效的方法是使用插件。自定义拦截器插件可以编写一个MyBatis的Interceptor插件对BoundSql的SQL语句进行解析在运行时检测是否包含非法的${}使用模式例如检测到${后面跟的不是预定义的安全关键字并记录警告或抛出异常。这是一个更高级的防御层。4.3 应对安全扫描报告当奇安信等安全扫描工具报告你的MyBatis XML文件存在SQL注入漏洞时通常是指出了使用${}的位置。排查和修复流程如下定位根据报告提供的文件路径和行号快速定位到具体的Mapper XML文件及SQL语句。分析判断该${}的使用场景。场景A用于传递值如WHERE name ‘${value}’这是高风险必须修复。改为#{value}并检查上下文如LIKE是否需要配合CONCAT或bind。场景B用于动态表名/列名/排序如ORDER BY ${field}这是中风险。修复方案是改为白名单机制使用choose/when或从枚举映射。修复与测试实施上述修复方案。编写或补充单元测试这是关键一步。不仅要测试正常功能还要编写安全测试用例尝试传入各种边缘和恶意参数如包含单引号、分号、SQL关键字的字符串确保SQL执行正确且不会报语法错误或产生非预期结果。可以使用JUnit 内存数据库如H2快速完成。在测试环境中重新部署触发安全扫描确认漏洞已关闭。5. 深度进阶script、Provider与复杂动态SQL对于极其复杂的动态SQL在XML中使用大量的if、choose标签会导致可读性急剧下降。MyBatis提供了另外两种强大的方式。5.1 使用script标签编写内联动态SQL在注解Select、Update等中可以直接编写动态SQL。Select(“script” “SELECT * FROM user ” “WHERE 11 ” “if test‘username ! null’” “ AND name LIKE CONCAT(‘%’, #{username}, ‘%’)” “/if” “if test‘statusList ! null and statusList.size() 0’” “ AND status IN ” “ foreach collection‘statusList’ item‘item’ open‘(’ separator‘,’ close‘)’” “ #{item}” “ /foreach” “/if” “/script”) ListUser findUsersByCriteria(Param(“username”) String username, Param(“statusList”) ListInteger statusList);适用场景SQL逻辑相对简单且开发者希望DAO层接口和SQL定义在一起避免在XML文件中跳转。缺点复杂的SQL会使得注解字符串非常长影响代码美观和编辑体验。5.2 使用SQL Provider类实现极致灵活对于高度动态、条件组合极其复杂的查询SelectProvider、UpdateProvider等注解是终极武器。public class UserSqlProvider { public String searchUsers(MapString, Object params) { String username (String) params.get(“username”); ListInteger statusList (ListInteger) params.get(“statusList”); StringBuilder sql new StringBuilder(“SELECT * FROM user WHERE 11”); if (username ! null !username.isEmpty()) { sql.append(“ AND name LIKE CONCAT(‘%’, #{username}, ‘%’)”); // 注意这里参数名仍需与Mapper接口匹配 } if (statusList ! null !statusList.isEmpty()) { sql.append(“ AND status IN (“); for (int i 0; i statusList.size(); i) { sql.append(“#{statusList[“).append(i).append(“] }”); if (i ! statusList.size() - 1) { sql.append(“,”); } } sql.append(“)”); } sql.append(“ ORDER BY create_time DESC”); return sql.toString(); } } // 在Mapper接口中使用 SelectProvider(type UserSqlProvider.class, method “searchUsers”) ListUser searchUsersByProvider(Param(“username”) String username, Param(“statusList”) ListInteger statusList);优势完全的Java代码控制你可以使用所有Java语言特性循环、条件、字符串操作、工具方法来构建SQL逻辑表达能力远超XML标签。易于调试可以在Provider方法中打日志输出最终拼接的SQL字符串调试非常方便。类型安全虽然参数是Map但你可以通过方法签名和内部强转来获得一定的类型安全。关键注意事项与安全实践警惕手动拼接在Provider中你是在用Java代码拼接SQL字符串。对于值部分必须坚持使用#{paramName}的占位符语法如上例中的#{username}和#{statusList[i]}让MyBatis后续进行预编译处理。绝对不要在Provider里用字符串“”操作符拼接用户输入的值。对于动态表名/列名如果必须在Provider中处理应使用白名单机制。例如从一个预定义的枚举或配置Map中根据传入的key获取真正的表名然后再用字符串拼接进SQL。永远不要将未经校验的用户输入直接拼接到表名、列名的位置。代码可读性复杂的Provider方法可能会变得难以维护请务必添加充足注释并将不同条件的构建逻辑抽取为私有方法。6. 常见问题排查与性能考量6.1 动态SQL使用中的典型“坑”if标签判断整数0失效if test“status ! null”如果status是int类型且值为0这个判断会为false因为MyBatis的OGNL表达式将0视为false。对于基本数据类型应使用其包装类Integer或者使用更精确的判断if test“status ! null and status ! 0”。foreach遍历Mapforeach collection“map” item“value” index“key”注意item是值index是键顺序和遍历List时不同。分页插件兼容性在使用PageHelper等分页插件时复杂的动态SQL特别是带有foreach或嵌套查询可能会影响分页计数查询count语句的生成准确性。务必在集成后对各种动态查询条件组合进行分页测试验证总条数和当前页数据是否正确。${}导致的模糊查询索引失效即使安全地使用${}进行动态列选择也可能带来性能问题。例如WHERE ${dynamicColumn} #{value}如果dynamicColumn变化频繁数据库可能难以针对此查询建立有效的索引命中策略。6.2 性能优化建议避免过度动态化不要为了“灵活”而将每个条件都做成动态的。如果某个条件在业务上几乎总是存在可以将其作为必传参数减少动态标签的解析开销和SQL的复杂度。优先使用where标签where标签会自动处理WHERE关键字以及去除首个条件前的AND/OR比手动写WHERE 11更符合SQL标准一些数据库优化器可能对11这种恒真条件有更好的处理但where是更优雅和安全的做法。批量操作使用foreach的批处理模式对于INSERT INTO ... VALUES (...), (...), ...这种语句foreach拼接大量值会导致SQL语句极长。考虑在foreach内使用bind预编译所有参数或者直接使用MyBatis的ExecutorType.BATCH模式配合循环单条插入在SqlSession层面进行批处理提交性能更高且能有效控制SQL语句长度。监控与日志开启MyBatis的SQL日志配置log4j.logger.org.apache.ibatisDEBUG或使用p6spy等工具观察最终执行的SQL语句。检查动态SQL生成的语句是否最优是否存在不必要的子查询或条件。特别是在Provider模式下打印出构建的SQL字符串进行审查是性能调优的第一步。动态SQL是MyBatis的灵魂特性它赋予了DAO层巨大的灵活性。然而能力越大责任越大。${}和#{}之间的选择本质上是在灵活性与安全性之间做权衡。通过建立严格的安全规范值用#{}语法用白名单、善用bind和Provider等高级特性并辅以完善的测试和安全扫描我们完全可以编写出既灵活高效又坚如磐石的数据访问代码。记住每一次SQL拼接都值得你停下来思考一下这样做安全吗

相关新闻

Windows DNS缓存查看与管理:原理、命令与实战排查指南

Windows DNS缓存查看与管理:原理、命令与实战排查指南

1. 项目概述:为什么需要查看Windows DNS缓存?当你访问一个网站,比如www.example.com,你的电脑并不会直接知道这个域名对应哪台服务器。它需要先问路,这个“问路”的过程就是域名解析,而负责指路的“交通警察…

2026/7/29 4:43:12阅读更多 →
C++/QT实现与MATLAB一致的短时傅里叶变换(STFT)时频分析

C++/QT实现与MATLAB一致的短时傅里叶变换(STFT)时频分析

1. 项目概述与核心价值最近在做一个音频信号分析相关的项目,需要把一段时域信号转换成时频图来观察频率成分随时间的变化。这个需求在语音识别、故障诊断、音乐分析里太常见了,业内标准工具就是短时傅里叶变换。项目初期为了快速验证算法,我直…

2026/7/29 4:43:12阅读更多 →
VMware Horizon 8.0入门④--AD域服务器部署

VMware Horizon 8.0入门④--AD域服务器部署

编辑AD域控制器的配置 注意:为了提升性能,请在部署完毕后再更改配置 关闭虚拟机,点击编辑设置 设置2个CPU和1G内存,点击确定 准备环境 开启远程桌面 弹出光盘 检查防火墙是否关闭 在导航栏输入wf.msc 点击属性关闭域、专用和公用配置文件 设置主机名 右键此电脑->更…

2026/7/29 4:43:12阅读更多 →
全志ARM Linux内核编译实战:从环境搭建到系统启动全流程

全志ARM Linux内核编译实战:从环境搭建到系统启动全流程

1. 项目缘起:为什么我们要从零编译ARM Linux内核?如果你手头有一块全志方案的开发板,比如Orange Pi Zero 2或者Orange Pi 5,并且已经玩腻了官方提供的现成镜像,那么“从零开始编译并启动一个自己定制的Linux内核”这个…

2026/7/29 5:47:35阅读更多 →
国产AI数据大屏生成工具推荐(政企电商+3D展厅)

国产AI数据大屏生成工具推荐(政企电商+3D展厅)

这两年“国产替代”喊得响,我们公司作为一家有国资背景的供应链企业,所有数字化系统都得过“信创”这一关。数据大屏又是公司的门面——既要给领导汇报用,又要放在展厅给客户看,还得满足电商部门的实时作战需求。我花了大半年时间…

2026/7/29 5:47:35阅读更多 →
RK3568裸机显示驱动实战:VOP2与IEP配置详解

RK3568裸机显示驱动实战:VOP2与IEP配置详解

1. 项目概述:在RK3568裸机环境下探索显示与图像增强最近在折腾ROC-RK3568-PC这块板子,目标是在完全脱离操作系统(也就是我们常说的“裸机”或“Bare Metal”)的环境下,把它的显示系统跑起来。项目标题里的“裸机19”大…

2026/7/29 5:47:35阅读更多 →
工业物联网通信方案:LTE Cat 1模块与Cortex-M4 MCU实战

工业物联网通信方案:LTE Cat 1模块与Cortex-M4 MCU实战

1. 工业级物联网通信的核心挑战与解决方案在工业自动化、远程监控和智能设备管理领域,稳定可靠的物联网通信一直是系统设计的核心难点。传统Wi-Fi和蓝牙方案在复杂工业环境中经常面临信号干扰、温度波动和电源不稳定等问题。这正是LARA-R6401D-00B蜂窝通信模块与TM4…

2026/7/29 5:47:35阅读更多 →
树莓派交互式表情显示器:GDW变脸项目全解析

树莓派交互式表情显示器:GDW变脸项目全解析

1. 项目概述:GDW变脸,一个树莓派驱动的交互式表情显示器最近在折腾一个挺有意思的小项目,我把它叫做“GDW变脸”。这个名字听起来有点玄乎,但其实核心就是一个基于树莓派的、能够根据外部触发条件动态切换显示内容的交互式装置。简…

2026/7/29 5:47:35阅读更多 →
DF BEETLE控制器深度解析:ESP32-S3核心在嵌入式物联网中的极致应用

DF BEETLE控制器深度解析:ESP32-S3核心在嵌入式物联网中的极致应用

1. 项目概述:当“小身材”遇上“大作为”在嵌入式开发和物联网项目里,我们总在寻找一个完美的平衡点:性能要足够强劲,能跑得动复杂的逻辑和算法;体积要足够小巧,能塞进各种奇形怪状的壳子里;接口…

2026/7/29 5:45:34阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

🔹 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具,凭借本地离线运行、可视化图形操作和任务自动化三大核心特性,赢得了众多用户的青睐。与普通在线对话AI工具不同,它属于能够直接操控本机软硬件的智能数字员工…

2026/7/28 4:06:39阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接,是用激光束对阀座壳体(通常为不锈钢或铝合金)进行密封焊接,使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/28 2:08:06阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/28 1:38:28阅读更多 →
28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“! 在构建复杂的 Agent 系统时,我们经常会遇到这样的场景:Agent 正在执行一个多步骤的任务,比如“下单购买商品”,但执行到一半时,我们…

2026/7/29 0:01:46阅读更多 →
自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

近日,国际专注开放式技术研发的声学品牌Nank南卡,正式官宣实力艺人曾舜晞担任品牌代言人。消息一经发出便轰动全网。为什么耳机品牌不选择流量明星、老牌歌手?而且是选择曾舜晞?让我们一起来探索一下!比起短期的流量&a…

2026/7/29 0:01:46阅读更多 →
【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

一、本文介绍 🔥本文在RT-DETR多模态融合目标检测中引入RLAB残差线性注意力模块,可在不同模态特征交互阶段进行多次残差细化,使可见光、红外等特征在尺度、语义和空间位置上更好对齐;随后将细化特征与解码器输出拼接并生成Q、K、V,通过线性注意力自适应强化关键通道、目…

2026/7/29 0:01:46阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/28 20:22:24阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/29 4:31:51阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/28 2:35:58阅读更多 →