版本更新说明
本文记录 jkit 各版本的用户可见变更。每个版本号只出现一次,按新到旧排列。
未发版的改动追加到当前版本小节(现在是 2.0.2);发版后冻结该小节,在上方新建下一版本。不要改写已冻结的历史版本,也不要为同一版本再开 unreleased 标题。
2.0.2 - 2026-09-19
新增
jkit-notify
MarkdownTheme:Markdown → HTML 的 13 套渲染主题(经典 / 蓝 / 橙心 / 彩虹 / 兰青 / 嫩黄 / 碧蓝 / Vue 绿 / 绿意 / 麦色 / 墨黑 / 姹紫 / 博客),前 12 套命名与观感对齐 doocs/md,blog复刻 alianga.com(Halo · LIlGG_Sakura)正文:珊瑚红链接、暖黄行内码、深色 One Dark 代码块、¶/# 标题装饰、列表虚线圆角框。MarkdownTheme.of("lark")按 id 解析,忽略大小写与-/_,未知值回退经典主题。MarkdownRenderOptions与Markdown.toHtml(md, options)/toDocument(md, options):一次指定主题、代码高亮、内联样式与响应式。inlineStyle(true)把样式写进每个标签的style属性,兼容会剥离<head><style>的 Outlook / 部分企业邮箱与微信粘贴。Markdown升为公开入口;NotifyUtils.markdownToHtml(md)/markdownToDocument(md, boolean)/wrapHtmlDocument(fragment, boolean)保留并标过期。- 代码块语法高亮(默认开启,
.highlight(false)关闭):自研零依赖词法扫描,覆盖 java / js / ts / go / python / sql / shell / yaml / properties / json / xml / html,输出内联<span style>,两种模式下都可见;不认识的语言退化为纯转义。 SmtpChannel.markdownTheme(theme)/inlineMarkdownStyle(boolean):给 SMTP 渠道固定 Markdown 渲染主题(默认经典 +<style>模式)。- 本地图片内嵌:
MarkdownRenderOptions.imageBaseDir(dir)(对应SmtpChannel.markdownImageBaseDir(dir))设置基准目录后,Markdown 里引用本地相对路径的图片会转成data:image/...;base64,...写进src,正文可脱离原文件独立展示。只处理本地路径,http(s)://////data:/cid:原样保留;支持./、../、绝对路径与file:前缀。文件不存在、非图片(按后缀与文件头识别 MIME)或超过maxInlineImageBytes(默认 2MB)时保留原src,不抛异常。 MarkdownRenderOptions新增.inlineImage(boolean)与.maxInlineImageBytes(long),控制图片内嵌开关与单张体积上限。
jkit-sql
JdbcUrlUtils:解析 JDBC URL(主机 / 集群节点 / 库名 / schema / 参数),fromUrl推断SqlDialect,getDbType返回类型短名,driverForUrl/getDriverClassName猜测驱动类。覆盖 MySQL 复制与负载、PostgreSQL HA、Oracle SID/Service/RAC、SQL Server、H2、Gauss/openGauss、达梦等。PostgreSQL 系从currentSchema取 schema(缺省public)。SQL.inject/SqlInjectConfig/SqlInject/SqlRewrites.inject:行级条件注入(列名自定,不限租户)。下钻 UNION / 子查询 / CTE;JOIN 按别名限定;INSERT 补列;MERGE 补 ON。启动时SQL.injectConfig配表白名单和列;SqlInject.setCurrent覆盖本线程;SqlInjectValue每次 inject 再取值。可一次注入多列。字符串值按 SQL 单引号转义。SQL.replaceSelectItem/SQL.replaceSelectItems/SQL.expandStar:列级脱敏。整树替换 SELECT 投影(保留输出列名);replaceSelectItems一次 clone、一次遍历替换多列;expandStar按表列清单把*/t.*展开后再裁列或改写成掩码表达式。解析不到的星号保持原样。SqlWallConfig:denyTables/allowTables/requireWhereColumns/maxTables。违规码deny-table、allow-table、missing-where-column、too-many-tables。恒真再拦LIKE '%'与XOR 1=1。DATE_FORMAT跨方言转换会改写常见格式符:%Y-%m-%d %H:%i:%s→ PG/OracleTO_CHAR(..., 'YYYY-MM-DD HH24:MI:SS'),SQLitestrftime会交换参数并把%i改成%M。对不上的格式符保留并SEMANTIC_RISK。- 复杂业务 SQL 1200 条回归(
sqls/complex-sql/,四方言各 300):L1 parse 1200/1200;L2 parse→format→parse 结构保真 1200/1200;L3 主矩阵 1500 次转换后再 parse 1500/1500。DATE(col)回写不再误成类型字面量。转换补GETDATE/DATEADD/LEAST/GREATEST、CURRENT_DATE→SQL ServerCAST(GETDATE() AS DATE)、Oracle 日期间隔用数字加减、递归 CTE 去RECURSIVE并补列清单。真库代表题 001/022/211 原文与 MySQL→Oracle12/SQL Server 转换后均可执行(tools-testComplexSqlExecutionIT)。 - 复杂 SQL 方言切片 L2/L4 harness(Oracle):
ComplexSqlOracleSliceL2*(jkit-sql)与ComplexSqlOracleNativeExecute*(tools-test);001–300 在 oracle19c 本库原生执行 300/300(parseFail=0 / execFail=0),报告target/complex-sql-reports/oracle-l2|l4-*。 - 复杂 SQL 方言切片 L2/L4 harness(MySQL):
ComplexSqlMysqlSliceL2*(jkit-sql)与ComplexSqlMysqlNativeExecute*(tools-test);001–300 在 MySQL3308/test_db本库原生执行 300/300(parseFail=0 / execFail=0),报告target/complex-sql-reports/mysql-l2|l4-*。 - 复杂 SQL 方言切片 L2/L4 harness(PostgreSQL):
ComplexSqlPostgresSliceL2*(jkit-sql)与ComplexSqlPostgresNativeExecute*(tools-test);001–300 在jkit_complex(5532)本库原生执行 294/300(parseFail=0;6 条语料round(float8,int)需显式 cast),报告target/complex-sql-reports/postgres-l2|l4-*。 - 复杂 SQL 方言切片 L2/L4 harness(SQL Server):
ComplexSqlSqlServerSliceL2*(jkit-sql)与ComplexSqlSqlServerNativeExecute*(tools-test);001–300 在jkit_ss_test(1433)本库原生执行 300/300(parseFail=0 / execFail=0),报告target/complex-sql-reports/sqlserver-l2|l4-*。 com.alianga.jkit.sql.entity.Comment:本模块自有的表 / 字段注释注解(TYPE+FIELD,value())。与 Hibernate@Comment、@SqlTable(comment)/@SqlColumn(comment)并列;扫描仍按简单名Comment识别,供jkit-sql-model等模块解析字段注释时选用。SQL.bind/SQL.bindNamed:把?/:name换成字面量或公式,并识别模板占位 IDENT(@name@/#{table}/${*}/{{*}}/<*>等)。表名位置写成标识符(必要时加方言引号,防注入);表达式位置与:name相同。字符串只加倍单引号;公式传SqlExpr。IN ?可填集合。布尔:Oracle / 达梦 / SQL Server / SQLite / DB2 / MySQL / Hive 写1/0;PG / H2 / ANSI / Presto / ClickHouse 写TRUE/FALSE。未传命名值时不扫描标识符。SqlPlaceholders.mybatis()/hashBrace()/dollarBrace():内置 MyBatis#{property}、${property},解析支持#{id,jdbcType=VARCHAR}/#{item.name};bind 按逗号前的属性名取值。SqlGuardedStatement:T-SQL 控制流守卫IF <expr> <stmt> [ELSE <stmt>](如 SQL Server init 幂等删表前置IF OBJECT_ID('t','U') IS NOT NULL DROP TABLE t;)。SqlParser.parseIfGuard解析 condition 原文与内层 body 并包成SqlGuardedStatement;其type()委托给 body,故IF…DROP对外仍是 DROP,下游格式化 / 跨方言转换可正确识别。解析器在语句起始处的IF一律按控制流守卫处理(MySQL 里IF是函数,但语句起始位置不冲突)。SqlFormatter/SqlAstCloner/SqlSchemaConverter均已支持——sqlserver_init.sql整文件parseAll不再因IF抛 unsupported,其 51 处守卫识别为 DROP,跨方言转换 0 失败。
jkit-core
AESCryptGCM 认证加密:encryptGcm(data, key)/decryptGcm(data, key)走AES/GCM/NoPadding,每次随机 12 字节 IV 并拼在密文前(IV || ciphertext),解密自动拆分,无需调用方保管 IV;四参版本encryptGcm(data, key, iv, aad)/decryptGcm(data, key, iv, aad)支持自定义 IV 与 AAD(附加认证数据参与完整性校验但不加密,适合绑定用户 ID / 业务单号)。另有generateIv()、GCM_CIPHER_ALGORITHM/GCM_IV_LENGTH/GCM_TAG_BITS常量。相比 ECB / CBC,密文被篡改或密钥不对时解密直接抛异常,而不是解出乱码继续往下传。EncryptUtils.RSAOAEP 填充:encryptOaep(byte[], PublicKey)/decryptOaep(byte[], PrivateKey)及 base64 字符串便捷方法,填充为RSA/ECB/OAEPWithSHA-256AndMGF1Padding;新增buildKeyPair(int keySize)可显式指定密钥长度,常量DEFAULT_KEY_SIZE。ULID:common.idgenerate下的 26 字符 Crockford Base32 有序 ID。next()/nextMonotonic()(同毫秒内+1,单 JVM 严格递增)、parse()取时间戳与随机部分、toBytes/fromBytes16 字节紧凑存储、isUlid()校验。解析按 Crockford 容错(I/L→1、O→0、大小写不敏感),含U等非法字符拒绝。按字符串排序即按时间排序,比 UUIDv4 更适合做索引主键。UUIDv7:RFC 9562 的时间有序 UUID,毫秒时间戳在高 48 位。next()/nextMonotonic()(12 位计数器,一毫秒用满则推进逻辑时钟 1ms,保证不回退)、nextString()、timestamp(UUID)取回毫秒时间戳、isV7(UUID)判定。形态仍是标准 UUID,可直接存uuid列。IdGenerator.ulid()/uuidV7()/uuidV7String():与雪花并列的门面入口。DesensitizeUtils:常用数据脱敏。phone/idCard/bankCard/name/email/address/carNo/ip/password语义方法,通用mask(value, keepHead, keepTail[, maskChar])与maskAll,配置驱动用desensitize(value, Type)(Type枚举十类)。null与空串原样返回不抛异常;keepHead + keepTail覆盖全文时只保留首字符而非原样返回;password固定输出 6 个掩码,不泄漏密码长度。HttpClient:可实例化 HTTP 客户端(next-plan 第 4 节)。HttpUtils是进程级全局门面,所有setXxx改全局默认,多线程下互相覆盖污染;HttpClient每个实例持有独立配置(超时 / 代理 / SSL / CookieJar / 引擎 / 拦截器 / fakeIp / 默认 Content-Type),多实例与多线程互不污染。builder()从当前全局默认起算、链式覆盖;shared()取全局默认单例,与HttpUtils静态方法等价。隔离通过 ThreadLocal 把实例配置下发到发送链路实现,无需全局加锁。HttpConfig新增copy()供实例取独立配置副本。JwtUtils:JSON Web Token 签发与校验(next-plan 第 4 节「JWT + 加密默认值」项中 JWT 一半)。仅依赖 JDK(java.util.Base64的 url 变体 +javax.crypto的 HMAC / RSA-SHA256),零第三方依赖。createHs256(secret, payload)/createRs256(keyPair, payload)签发,parseHs256(secret, token)/parseRs256(publicKey, token)校验并回吐 payload(自动采用Map上下文);withExp(minutes)/withNbf(minutes)/withIssuer/withAudience等声明快捷构造,exp/nbf/iat走毫秒或秒两种单位(expAt(Instant)/expSeconds(long))。校验严格:签名错误、密钥不符、过期、未生效、载荷被篡改一律拒绝;显式禁止alg=none与算法混淆(HS256 令牌不会被 RS256 公钥解);base64url 无填充、载荷支持任意 JSON 对象(含嵌套与标准注册声明)。JwtUtilsTest16 例覆盖 HS256 / RS256 往返、错误密钥、篡改、过期、未生效、声明保留、alg=none拒绝、base64url 边界等。JSONPatch:JSON Patch(RFC 6902)应用工具(next-plan 第 4 节「JSON Patch / Schemarequired别名」项中 Patch 一半;required别名已在 2.0.2 早些时候落地)。apply(String doc, String patch)自行解析与序列化,apply(Object, List)对已解析的Map/List原地修改并返回;支持add/remove/replace/move/copy/test六种操作,路径采用 JSON Pointer(RFC 6901,~1→/、~0→~解码),test按值比较数字(1与1.0相等)。非法path、下标越界、删除/替换不存在的成员或test不成立时抛出JSONPatchException(JSONException子类)。JSONPatchTest32 例覆盖各操作、数组插入与末尾-、根替换、指针转义、数字相等与各类非法输入。Retryer/CircuitBreaker:通用(与 HTTP 解耦)的韧性抽包(next-plan 第 4 节「韧性抽包」项)。Retryer.retry(Callable, RetryConfig)按指数退避 + 抖动重试有限次,默认重试所有非Error异常,retryOn(Predicate)/retryOn(Class)收窄范围;CircuitBreaker状态机CLOSED→OPEN→HALF_OPEN,连续失败达阈值熔断、冷却后半开探测、成功恢复,内部状态用原子变量、可多线程共用。两者均纯 JDK、零依赖。RetryerTest(8 例)+CircuitBreakerTest(7 例)覆盖首次成功、重试后成功、耗尽抛最后异常、谓词过滤、不重试Error、退避上限、熔断阈值/OPEN 直接拒绝/冷却后半开/半开恢复与重开/成功重置等。com.alianga.jkit.html轻量 HTML 解析与 CSS 选择器(next-plan 第 4 节「轻量 HTML」项,替代已移除的 Jsoup),零依赖:Html.parse解析为Document;DOM 有Element/TextNode/Comment/Elements,元素支持select/selectFirst/text/attr/innerHtml/outerHtml/children等;选择器支持标签、#id、.class(含.a.b)、属性[attr]与=/^=/$=/*=/~=/|=、后代与子代组合符、逗号分组、:first-child/:last-child/:only-child/:root/:empty/:not()/:nth-child(an+b);另有Html.escape/Html.unescape实体处理。解析器对不规范 HTML 容错(隐式关闭未闭合标签、void 元素不进栈、<div/等残缺标签安全终止、script/style 按原文处理)。HtmlTest14 例覆盖解析结构、各选择器、伪类、void 元素序列化、文本抽取、容错与实体转义。com.alianga.jkit.html保真度增强(以 Jsoup 1.18.1 为对照做差分验证,93 组「HTML + 选择器」用例 86 组结果一致):解析端新增隐式html/head/body骨架、省略结束标签按 HTML5 作用域自动闭合(p/li/dt+dd/td+th+tr/option/rt+rp/thead+tbody+tfoot,含被行内元素埋住的p)、table内隐式补tbody、无引号属性值支持/与&(href=/x?a=1&b=2不再被截断)、新增DataNode使script/style原文序列化不转义;选择器端新增相邻兄弟+与通用兄弟~组合符、:nth-last-child/:nth-of-type/:nth-last-of-type/:first-of-type/:last-of-type/:only-of-type/:contains/:containsOwn/:matches/:matchesOwn、属性[attr!=v],伪类名大小写不敏感,且非法选择器(如h1 @ p、p ~ + p、p:not())改为立即抛SelectorException——此前遇到未知字符会死循环;文本端text()与 Jsoup 对齐(块级元素间补空格、空白折叠、裁剪首尾含 ),新增ownText()/nextElementSibling()/previousElementSibling()/elementSiblingIndex(),nodeText()改为返回未归一化原文;实体端新增Entities收录约 200 个高频命名实体(©—…等)。HtmlTest扩充至 32 例,并新增非法选择器必须抛异常而非挂死的回归用例。com.alianga.jkit.html性能优化(同机与 Jsoup 1.18.1 对比:预热 3 轮、计时 7 轮取中位数,-Xms1g -Xmx2g,Jsoup 仅用于基准、未进入项目依赖。内存侧Element属性表由LinkedHashMap改为并行String[](键 / 值 / 计数三件套),子节点表与属性表均懒创建,新增Names驻留表复用标签名与属性名字符串——200 份 300 KB 页面 DOM 常驻堆 506.9 MB → 268.0 MB(Jsoup 241.5 MB),由高出 110% 收窄到高出 11%。解析侧开放元素栈的 head / body 成员判断由 O(栈深) 线性查找改为下标缓存,起始标签的属性写入复用缓冲区(不再每个标签分配 Map),Character.isLetter/isWhitespace/isLetterOrDigit换成本地 ASCII 判断,trim().isEmpty()改为手写扫描——300 KB 页面解析 2.161 ms → 1.775 ms,由慢 12% 变为快 3%–28%。选择器侧遍历由「先allElements再LinkedHashSet去重」改为直接递归(不建中间列表、不分配迭代器),Compound.matches按下标遍历,属性选择器由「hasAttr+attr两次扫描」合并为一次,~=去掉正则split,标签名在解析期归一后由equalsIgnoreCase退化为equals,并新增上限 256 条、按访问序淘汰的选择器解析缓存(与 Jsoup 一样缓存解析结果)——中等页面(30 KB)选择器由慢 5 倍变为快 1.6–4.1 倍,大页面(300 KB)选择器基本持平(0.91–1.15 倍)。优化后HtmlTest仍 32 例全过,与 Jsoup 的差分验证仍是 112 组一致 / 7 组已知差异,与优化前逐条相同,无行为回退。com.alianga.jkit.html选择器索引:Document上新增惰性 id / class / 标签索引(键前缀t:/#:/.:,首次查询时建,之后复用)。doc.select(...)/doc.selectFirst(...)不再每次全树深度优先遍历,而是「取候选 + 逐个校验」;键的优先级是 id > class > 标签名,div.foo取.foo、a[href^=/p/]取a(标签名也算索引键,所以带属性的查询一样受益),只有纯属性 / 伪类查询(如[data-x]、:empty)才退回全树遍历。查询根是子元素时(如article.selectFirst("a.post-title"))仍走遍历——候选集以整份文档为范围,逐个判断是否落在子树内反而更慢,所以抽取类代码里能提到文档级的查询尽量提到文档级。索引敢这么用,是因为本模块 DOM 解析完即不可变(没有 setter、没有增删子节点的入口),建一次就再也不会失效,不需要任何失效逻辑。实测(合成页面,成对交替计时):选择器由 1.3x–2.2x 提升到中页面 4.6x–80x、大页面 8.6x–749x,端到端 1.89x;真实站点 alianga.com 首页解析 1.74x、解析 + 抽 10 篇全部字段 1.73x、4 个抽取选择器 4.5x–37.1x。常驻堆只解析仍为 202.6 MB(不查询就不建索引,与建索引前一模一样),解析 + 一次查询 209.1 MB vs Jsoup 181.0 MB,索引约占一份 DOM 的 3.5%。HtmlTest由 34 例扩充到 39 例,新增indexAndTraversalReturnTheSameElements(同一批查询分别在文档级走索引、在元素级走遍历,断言元素序列逐个assertSame)、indexKeepsDocumentOrder、repeatedIdAndMultiClassAreIndexedCompletely、missKeyYieldsEmptyInsteadOfCrash、indexIsBuiltOnceAndReused;与 Jsoup 的差分验证仍是 112 组一致 / 7 组已知差异,真实站点 10 篇文章 5 个字段逐条一致。jkit-sql-auto:配置绑定补齐。foreign-keys/auto-increment/postgres-identity-style此前只有 Java 链式 API 能设,jkit.sql.auto.*配置与 Spring 绑定都到不了(docs 推荐的 OpenGauss / DuckDB 规避手段对 Spring 用户实际不可用);现在两个入口都支持。Spring starter 的additional-spring-configuration-metadata.json补全全部 24 个配置键,IDE 里jkit.sql.auto.*有完整补全与说明。jkit-sql-auto:多实例并发保护与审计三件套。lock: true(默认)执行前在当前连接取元数据锁(MySQLGET_LOCK/ PGpg_advisory_lock,其它方言自动跳过),多实例同时冷启动串行化,避免「都读到表不存在、都去 CREATE」的竞态;history: true把实际执行的 DDL 写入历史表jkit_schema_history(自动创建,行含时间 / 主机 / 模式 / 语句,空计划不产生行);export: 路径把本次计划落盘(UTF-8,每条一行分号结尾),配合dry-run即纯 schema 生成器。
变更
jkit-core:EncryptUtils.RSA.buildKeyPair()默认密钥长度由 1024 位上调到 2048 位。1024 位 RSA 已不满足当前安全基线(NIST / PCI-DSS 均要求 ≥2048);确需沿用旧长度改为显式调用buildKeyPair(1024),小于 512 位抛IllegalArgumentException。jkit-core:DESCrypt与EncryptUtils.DES标@Deprecated(56 位有效密钥可被暴力破解,ECB / CBC 不防篡改)。方法仍可用,仅用于解密历史数据;新代码用AESCrypt.encryptGcm。jkit-notify:Markdown 渲染入口迁到Markdown(toHtml/toDocument/wrapDocument)。NotifyUtils上 2.0.1 的markdownToHtml(md)/markdownToDocument(md, boolean)/wrapHtmlDocument(fragment, boolean)保留并@Deprecated。toDocument/wrapDocument按主题生成样式表(此前是写死的一段 CSS),默认观感略调(正文 16px、行高 1.75、标题与表格样式更完整),并默认开启代码高亮。jkit-notify:修复 macOS 窗口风格代码块(碧蓝 / Vue 绿 / 墨黑)顶部三个圆点与首行代码重叠——样式表与内联样式都改为顶部留 36px(此前只写padding-top:34px,内联模式与移动端媒体查询的简写padding都会把它覆盖掉)。jkit-notify:移动端媒体查询改为带!important(内联模式下行内style优先级更高,不带则整段适配不生效),并补上标题字号收敛、表格单元格内边距、图片宽度与表格惯性滚动;移动端不再用简写padding覆盖代码块上内边距。jkit-notify:带引号装饰的引用(姹紫 / 兰青 / 橙心)改为把引号放进左侧 2.6em 留白区,不再压在正文上;内联模式下的引用内边距与样式表保持一致(此前竖条与填充两种风格产出完全相同)。jkit-notify:正文排版改挂到自带的<div class="jkit-md">容器上(此前写在body上)。Gmail、QQ 邮箱等客户端会剥掉<html>/<head>/<body>,body上的max-width/padding会一同失效,表现为正文铺满读信区。现在宽度、内边距、字体、颜色、行高全部内联在这层 div 上。jkit-notify:修复完整 HTML 文档在浏览器中正文栏贴左边——body不再带max-width(文档壳给 body 写了内联margin:0以铺满底色,限宽会留下、居中外边距被盖掉)。栏宽与margin:0 auto只挂在div.jkit-md上,移动端媒体查询也只收这层容器的内边距。jkit-notify:正文栏宽度默认 720 → 820px,桌面端左右内边距 32 → 16px、上下 24px,移动端改为12px 8px(左右基本不留白)。容器宽度取主题自身的maxWidth令牌,不再在媒体查询里硬编码。jkit-notify:图片改为居中并带主题圆角(display:block;margin:1.4em auto),与段落、代码块、表格同宽——此前单独收窄图片会让图片与文字对不齐;桌面端代码块左右内边距 18px、行高 1.7。jkit-notify:原文里的 HTML<img>(公众号稿常用<img src="..." width="100%" />)消毒后透传,不再转义成<img>。只保留 src/alt/title/width/height/class/loading;onerror等事件丢掉,javascript:的 src 降为#。其他 HTML 标签仍转义。jkit-sql:行级注入实现类由SqlTenantRewriter更名为SqlInjectRewriter(2.0.2 未发版,不保留旧名)。租户只是一种场景,类名/方法名不再带 tenant。jkit-sql:复杂 SQL 回归门禁收紧——L1 parse / L3 转换后 parse 由「≥95% / ≥90%」收到 100%;L3 由 5 个方向对扩到四方言 4×3 全矩阵(3600 次转换,补上 PostgreSQL 作为源);L2 新增「有效括号不减少」硬断言。8 个方言切片 L2 测试收敛到AbstractComplexSqlSliceL2Test,子类只声明方言与编号区间(净减约 1100 行重复代码)。jkit-sql:SqlIdentifier支持逐段引号标记(新增markQuotedPart/isPartQuoted/quotedParts)。此前只有一个整体quoted,c."LEVEL"会被回写成"c"."LEVEL"——引号扩散到表别名,Oracle 里"c"与别名C不匹配而报ORA-00904(1200 条语料真库全量对照中 15 条中招)。SqlParser按段打标记、SqlAstCloner复制位图、SqlFormatter按段输出;无逐段信息时(改写器构造的标识符)回退到整体quoted,行为不变。jkit-sql:L2 结构对比由语句级 6 项扩到深结构——ComplexSqlReports.deepStructureDrift递归 select 树比对列数 / WHERE / GROUP BY / HAVING / ORDER BY / FROM / DISTINCT / UNION / LIMIT,L2 与 8 个方言切片共用。此前列被吃掉、WHERE 整段丢失都检测不到。jkit-sql:SQL.bind/bindNamed字符串入口不再对 parse 结果二次 clone;访问者热路径分发提前;绑定值少分配(位置参数走数组下标、无引号字符串快路径、小整数原文缓存)。AST 入口仍 clone-then-mutate。jkit-sql-auto:SqlAutoDialects.fromUrl/driverForUrl委托JdbcUrlUtils(覆盖 Gauss / Kingbase / Hive / ClickHouse / Trino 等更多 URL)。jkit-sql-auto:已有表对照实体注释。DatabaseMetaData.REMARKS读入活表/列;实体注释非空且与库不一致时发出COMMENT ON/ALTER TABLE … COMMENT/ MySQLMODIFY … COMMENT。实体未写注释时不覆盖库里已有注释。com.alianga.jkit.html真实站点验证(Halo 1.4.5 博客,首页 70 KB,含 24 个script):抽最近 10 篇文章的链接、标题、懒加载图data-src、占位图src、发布日期,5 个字段 10 篇逐条与 Jsoup 一致;3 篇详情页的pre code命中数与内容同样一致。效率上首页解析 0.179 ms vs Jsoup 0.295 ms(1.65x),端到端(解析 + 抽 10 篇全部字段)0.310 ms vs 0.381 ms(1.23x)。jkit-sql-auto:Spring Boot starter 执行阶段默认改为phase: eager——在上下文刷新期(DataSource 就绪后、Web 端口开放前)完成表结构同步,fail-fast失败时应用在接收流量前就退出(此前挂在 ApplicationReadyEvent,端口已绑定);恢复 2.0.1 行为配phase: ready。同时,没有 DataSource bean 且未配置jkit.sql.auto.url时改为跳过并打 warn 日志,不再让应用启动失败。
文档
docs/sql.md/docs/en/sql.md:实体表 / 列注释补充本模块com.alianga.jkit.sql.entity.Comment。docs/sql.md/docs/en/sql.md:经典 Oracle ROWNUM 包装补充 UNIONORDER BY先外包再分页,以及toSqlString必须带目标方言。docs/sql.md/docs/en/sql.md:跨方言转换去掉「进行中」口径;补齐inject/expandStar/replaceSelectItem/bind/ Wall 表策略 /DATE_FORMAT格式符。英文转换章节与中文对齐。sql-auto补充已有表注释同步。docs/sql.md/docs/en/sql.md「业务场景」补bind/inject/expandStar+replaceSelectItems/addComment+方言引号 / MyBatis#{}/ ${}可复制示例;模板占位符节增加 parse+bind 常用写法。样例与SqlBusinessScenarioTest对齐。docs/sql.md/docs/en/sql.md业务场景扩到 18 类(2.0.2):多数据源方言识别(JdbcUrlUtils)、报表DATE_FORMAT跨方言、动态表名安全绑定、低代码查询沙箱(Wall 表白名单 / WHERE 必含列 / 表数上限)。场景 4 补恒真LIKE '%'/XOR。docs/toolkit.md:crypto 章节补 AES-GCM(IV 布局 / AAD / 参数校验 / 与 ECB 的差异)、RSA(OAEP 与 PKCS#1 v1.5 选型表、密钥长度、单块上限)、DES 废弃说明;新增「ID 生成」章节对比雪花 / ULID / UUIDv7 的适用场景与单调模式语义,新增「脱敏」章节说明各类型保留位数与mask的兜底策略。docs/json.md/docs/en/json.md:Schema 关键字表补required(标准数组写法)与must(自有布尔写法)的区别,新增「必填:required 与 must 选哪个」小节。docs/csv.md/docs/en/csv.md:补完此前 0 字节的空文档,覆盖CSVUtils轻量读写、CSV/CSVTable带表头与 POJO 映射、CSVObjectWriter流式写对象、流式readStream、字符集与解析细节、异常;从srcExclude移除并加入中英文侧边栏。docs/expression.md/docs/en/expression.md:补完此前 0 字节的空文档,覆盖算术/逻辑、Map与 JavaBean 上下文、位置参数p0/p1、@内置函数、自定义函数与静态方法注册、renderTemplate模板渲染、CacheableExpression缓存说明与异常;同包加入侧边栏,toolkit.md新增「表达式引擎」指针小节。docs/html.md/docs/en/html.md:§9 性能数据全部重测刷新,计时改为成对交替(A→B→B→A 各测两轮、各取较小值)。此前固定「先 jkit 后 jsoup」会让先跑的一方吃亏——JIT 编译、分支预测、缓存预热都压在前几轮——大页面.post因此报出 0.60x 这种并不存在的「jkit 更慢」,.post交替后稳定在 1.3x。刷新后:解析 1.92x / 2.06x / 2.02x(小 / 中 / 大页面),解析 +text()1.67x,选择器中页面 1.55x–2.04x、大页面 1.28x–1.77x,端到端 2.01x,150 份大页面 DOM 常驻堆 202.1 MB vs Jsoup 179.7 MB(0.89x);真实站点 alianga.com 首页解析 0.203 ms vs 0.381 ms(1.88x)、解析 + 抽 10 篇全部字段 0.247 ms vs 0.428 ms(1.74x)。压测代码迁到tools-test项目(com.alianga.test.html.HtmlParseBenchTest/HtmlRealSiteBenchTest),报告归档在reports/,§9 新增「复现方式」小节写明命令与断言口径。docs/html.md/docs/en/html.md:§9 再次刷新为建索引后的数据,新增「索引的使用范围」小节(只对文档级查询生效、键的取法、候选仍是超集故结果与遍历逐一相同),并把优化手段清单由四处扩到五处——前四处加起来选择器只有 1.3x–2.2x,加上惰性索引才是 4.4x–35x。docs/en/sql.md:跨方言类型转换章节补全到与中文全等——Normal Form 框架与设计文档入口、Phase 0–1 完整 import 代码、SqlDataTypeRegistry校验、RegistryValidationTest、Phase 2–3 整句SQL.convert入口与SqlSchemaConvertOptions/generateOracleSequence、完整函数改写清单(含CONVERT USING charset不误映射、DATEADD/GETDATE/FROM_UNIXTIME、UCASE/LPAD/CEIL/YEAR等)、SQL.convertBatch与ALTER处理、真库回归命令块、SqlDialectSpec扩展、ConversionResult.sqlWithExtras()与SqlSchemaConverterProvider.registerFunctions、类型别名/函数扩展指引。docs/html.md/docs/en/html.md:随 HTML 修复与内存反超全面刷新——§4 补:not()选择器列表与含组合器的复杂选择器示例、Elements.select的去重语义;§5 补超 BMP 数字实体生成代理对;§6 补<!-->/<!--->空注释与<html>属性保留;§7 差异清单移除「:not()内嵌组合符」(已支持);§8 补齐畸形选择器解析期拒绝的完整清单;§9 数字按最新 tools-test 报告刷新(JDK 17),常驻内存结论由「高 13%」改为「低 15%」,优化清单由五处扩到六处(新增解析期字符串驻留缓存)。docs/resilience.md/docs/en/resilience.md:HALF_OPEN状态说明改为「同一时刻只放行一个探测请求,未达成功阈值会继续放行下一个探测」。docs/json.md/docs/en/json.md:JSONPatch 小节补数组下标的 RFC 6901 严格规则(0之外无前导零、不允许+)与test的BigDecimal精确比较语义(大整数不再被double截断成假相等)。docs/toolkit.md/docs/en/toolkit.md:ULID 小节补parse的 48 位时间戳上限校验与往返保证。docs/http.md/docs/en/http.md:实例作用域改为「保存并恢复进入前的旧值」,补拦截器嵌套调用其它实例后外层配置不变的语义。
修复
jkit-core:com.alianga.jkit.html的script/style/textarea结束标签查找是 O(标签数 × 文档长度)——每次查找都对整篇文档做一次toLowerCase()再indexOf。合成页面只有 1 个script时看不出来,真实页面(含 24 个script的 70 KB 首页)解析耗时 3.929 ms,比 Jsoup 慢 14 倍。改为逐字符扫描 +regionMatches忽略大小写比较,零分配;同一页面 3.929 ms → 0.179 ms(快 21 倍),合成大页面解析也由 1.775 ms 降到 1.060 ms。结束标签的大小写、</script >这类带空白写法、以及内容里的<均由rawTextCloseTagIsCaseInsensitive回归覆盖。jkit-core:com.alianga.jkit.html的pre上下文没有向上传递保留空白。pre > code里的换行会被折叠成空格(<pre><code>int a = 1;\n int b = 2;</code></pre>的text()得到int a = 1; int b = 2;),代码块抽取结果失真。现在与 Jsoup 一致:从父元素逐级向上,命中pre/textarea等即保留原始空白,中途遇到非行内元素则不再保留——所以pre > code > span保留而pre > div > code不保留。由preContextKeepsNewlines回归覆盖。jkit-core:JSON Schema 的required关键字此前只被解析、从未参与校验——按标准写法声明的必填字段缺失时validateSuccess仍返回true,是静默失效。JSONNode#validateSchemaObject现在校验required列出的字段是否存在(标准语义:只要求字段存在,值为null也算存在;类型判定仍由type负责);required无需配合properties也能生效,嵌套对象同样覆盖。本库自有的must语义不变(要求字段存在且值不为null),两者混用时都要满足。jkit-sql:回写不再丢表达式括号。(a - b) / c此前回写成a - b / c、-(a + b)回写成-a + b、ROUND((SELECT …), 2)回写成ROUND(SELECT …, 2)——求值顺序被改,函数参数里的子查询还会写成非法 SQL。parser 现在保留源文括号标记,函数参数中的标量子查询照常带括号。复杂 SQL 语料 1200 条里 172 条受影响;L2 文本保真率由 45.75% 升到 91.83%,并新增「有效括号不减少」门禁(折叠(col)这类冗余原子括号不算丢失)。jkit-sql:经典 Oracle 对带ORDER BY的 UNION / INTERSECT / EXCEPT / MINUS 做 ROWNUM 分页时,先包成SELECT * FROM (set-op) ORDER BY …再套 ROWNUM,避免子查询里对集合运算列别名排序报ORA-00904。双层包装外层只投影原查询列,不再把中间层的RN输出给调用方(原查询为SELECT *时仍会带出RN)。jkit-sql:SqlNode.toString()默认按 MySQL 回写标识符引号(反引号),与SQL.toSqlString一致;不再误用 ANSI 双引号。addComment("正文")/ 紧凑模式下的--行注释会包成合法块注释,避免把后续 SQL 拼成普通文本或整句注释掉。addHint对未包装的正文补 slash-star-plus。跨方言仍用SQL.toSqlString(stmt, dialect)。jkit-sql-auto:SqlAutoInspector判断表是否存在时补上 schema。未配置时从Connection.getSchema()取;无连接(dry-run)或驱动不支持时从 JDBC URL 解析。PostgreSQL / Gauss 缺省public,SQL Server 缺省dbo,Oracle / 达梦回落用户名。避免把其它 schema 下的同名表误判为已存在,或对本库缺失表发出 ALTER。jkit-curl-codegen:curl 解析告警(如未支持的选项 --digest,已跳过)此前在生成路径全部丢失,现合入GeneratedCode.notes()最前面,生成结果不再静默吞掉提示。jkit-curl-codegen:生成器不再静默丢内容。py-requestsmultipart 真实生成files=并传入请求(此前文件上传整体消失);py-httpx补 multipart / 文件正文;js-fetch/js-axios的-T文件正文给出带路径的 fs 读取示例而非裸undefined;go-nethttp/csharp-httpclient真实生成 multipart(mime/multipart/MultipartFormDataContent)与文件正文(os.Open/File.ReadAllBytes);php-curlmultipart 生成CURLFile。代理处理对齐:py-requests不再硬编码http://(带 scheme 与认证);java-okhttp/kotlin-okhttp补proxyAuthenticator;go-nethttp走http.Transport;csharp-httpclient走WebProxy;java-apache补setProxy;js-axios补proxy配置;php-curl补CURLOPT_PROXY;无法按请求配代理的js-fetch/js-native会在 notes 里说明。jkit-curl-codegen:har生成器decode不再把 URL/表单里的字面+错变成空格(保留+,%20/%2B仍正常解码);creator 版本号不再硬编码2.0.1,jar 内读Implementation-Version,开发环境回落当前版本。CodeQuote的 JS/Python/Go/C#/Ruby/R 字符串转义补上 U+2028/U+2029 与其余控制字符(此前可生成非法 JS);Rust/Swift 用\u{XXXX},Lua 用三位十进制\ddd。jkit-curl-codegen:修复三处会把生成代码「打破」或注入的转义缺口。新增CodeQuote.kotlin()(额外转义${,Kotlin 模板插值此前会被求值,含${}的值直接让生成结果编译失败)、CodeQuote.swiftEscape()(Swift 字符串内容;此前 multipart 的字段名 / 文件名 / Content-Type 裸拼进字面量,含双引号即断串)、CodeQuote.rName()(R 反引号名;此前头名与 multipart 字段名裸拼,含反引号即解析失败)。jkit-curl-codegen:kotlin-okhttp的正文构造改用 okhttp 4.x 扩展函数toMediaType()/toRequestBody()/asRequestBody()。此前用的MediaType.parse在 Kotlin 下是 ERROR 级废弃、RequestBody.create(MediaType, x)也已废弃,生成的代码根本编译不过;multipart 文件分片同时改用分片自带的 Content-Type,不再固定application/octet-stream。jkit-curl-codegen:-u的 basic 认证不再写两遍。visibleHeaders()已注入Authorization,而lua/httpie/php-guzzle/ruby-httparty又各自渲染一遍原生认证;新增CurlGenSupport.headersWithoutAuth()供这类生成器取头。lua原先还会额外写进一个 luasocket 并不认识的authentication选项,已去掉。jkit-curl-codegen:-k真正生成忽略证书校验的代码。java-okhttp/kotlin-okhttp此前只加一行注释(Java 版注释还写着「已生成」但并没生成),生成的代码仍严格校验证书;现按各自语言生成 trust-allX509TrustManager并放行主机名校验,同时保留「仅用于开发环境」提示。jkit-curl-codegen:swift-urlsession补#if canImport(FoundationNetworking),Linux 上不再因缺少该 import 而编译失败。jkit-curl-codegen:httpie命令补--ignore-stdin。非交互执行(脚本 / 管道)时 HTTPie 会把 stdin 当请求正文,与--raw同时出现就报Request body (from stdin, --raw or a file) and request data (key=value) cannot be mixed,生成结果直接不可用。urlencoded 正文不再同时给语义冲突的--form与--raw,只保留--raw。jkit-curl-codegen:csharp-httpclient补using System;。项目关闭ImplicitUsings时生成的Console/Exception会编译不过。jkit-core:curl 解析多个-b/--cookie按 curl 语义用;拼接(此前互相覆盖只留最后一个);-E与--cert一致按「忽略不参与请求构造」处理(此前报「未支持」)。jkit-core:HttpUtils调试日志与「total timeout exceeded」异常里的 URL 保留完整结构但敏感字段打码——access_token/token/sign/signature/secret/key等查询参数值,以及 Telegram/bot<token>/、Server酱/<SendKey>.send路径令牌;新增公开方法HttpIo.maskUrl。钉钉/Telegram/Server酱/阿里云短信等凭证不再随日志或异常落盘。jkit-notify:SMTP 修复四处——收件人/发件人/Reply-To 不再允许 CR/LF(配置层剥除 + 渠道层拒绝),杜绝向 MIME 头与 SMTP 命令注入;DATA 阶段真正执行 RFC 5321 dot-stuffing(此前dotStuff()存在但未接入发送路径);EHLO 声明时优先AUTH PLAIN(此前只会AUTH LOGIN);单个 RCPT 被拒不再中断整封邮件,全部拒绝才算失败,部分被拒在结果的 response 里列出。jkit-notify:新增NotificationChannel.validate(ChannelConfig)(默认空实现)。NotificationManager在任何网络发送前调用,sendAll/sendFailover恢复「编程错误不会部分发送」的契约——此前缺 webhook/token 会在前一渠道真实发出后才抛。短信与 ntfy 渠道的 URL 构造依赖消息,改用探针消息预检。jkit-notify:Message新增copy();短信渠道逐号码发送基于副本注入当前收件人,不再把smsReceiver写回污染调用方消息,同一消息并发发多个短信渠道也不会互相覆盖收件人。jkit-notify:SendResult聚合时本地抑制(SUPPRESSED)按最轻级别处理,不再盖过限流/配置错误成为聚合结果的最严重类别。jkit-notify:Attachment.contentBytes()文件附件按块读取,不再按声明长度一次性分配(大文件直接 OOM);超过 2GB 快速失败并提示改用openStream()。jkit-notify:渠道注册表register/unregister/get/list加同步,并发读写不再可能丢渠道或抛ConcurrentModificationException;NotifyPolicy去重占位与限流计数前移到beforeSend原子完成(afterAttempt保留兼容、不再重复记账),并发下同一条消息不会同时通过去重检查、也不会放行超额消息;默认异步线程池改为 8 线程 + 1000 有界队列 + CallerRunsPolicy 背压,不再无限积压。jkit-notify:SPI 加载逐 provider 容错,单个扩展渠道损坏(缺依赖 / 构造抛异常)只告警跳过,不再让整个模块以ExceptionInInitializerError崩掉。jkit-notify-extra:阿里云短信RegionId跟随CFG_REGION(可配地域),不再硬编码cn-hangzhou。jkit-core:默认包(无名包)类的反射包装与 JSON 序列化不再抛NullPointerException。ClassStrucWrap.checkClassStructure与 JSON POJO 序列化代码生成都直接取getPackage().getName(),而默认包类的getPackage()在 JDK 8 下返回null(JDK 9+ 返回空名包,代码生成会写出非法的package ;)。现在两处都做空保护:拿不到包名时不生成package语句、也不再误判成 JDK 内置模块,默认包 POJO 在 JDK 8 能序列化、在 JDK 9+ 能走代码生成的快路径。该缺陷与上游 wast 同源(ClassStrucWrap.java:614、JSONPojoSerializerCodeGen.java:40:JDK 8 直接 NPE,JDK 11+ 生成package ;后静默退回反射序列化)。新增ReflectNullPackageTest(2 例)与DefaultPackageSerializationTest(3 例)。jkit-core:JDK 17+ 上回退引擎的 PATCH 之类的扩展 HTTP 方法不再抛ProtocolException。HttpURLConnection.setRequestMethod只承认白名单方法,旧实现在抛异常后用setAccessible(true)改写method字段;JDK 9 起模块封装生效,JDK 17 起该反射直接抛InaccessibleObjectException,回退引擎上的 PATCH 必然失败。现在优先用Unsafe写method字段(反射保留为兜底),JDK 8 / 11 / 17 / 21 / 25 实测均可真正发出 PATCH。新增UrlConnectionPatchMethodTest(3 例,直接驱动引擎、不经过上层引擎选择)。jkit-core:测试源码改按 JDK 8 toolchain 编译。maven-toolchains-plugin会把默认 toolchain 换成声明里的 9 / 11,默认testCompile跟着用 JDK 11 的 javac——字节码虽是 52,方法签名却按 JDK 11 解析(ByteBuffer#flip()变成返回ByteBuffer的协变版本),用 JDK 8 跑测试就是NoSuchMethodError。现在base-compile与default-testCompile在两个 profile 里都指回 JDK 8 toolchain,五个 JDK 上mvn clean test均为 870 例通过(3 例跳过)。jkit-core:表达式求值器com.alianga.jkit.expression修复两处正确性问题。&&/||此前不做短路求值——进入运算符分派前就急切算出了右操作数,导致true || (1/0)、false && (1/0)这类本应短路的表达式反而抛除零异常,右操作数的副作用也无法被跳过;现在ExprEvaluator在左操作数已能决定结果时直接返回、不再计算右操作数,与 Java 语义一致。>/</>=/<=此前硬性按Number转型,字符串比较直接抛异常;改为数字按数值、其余Comparable按自然序(如字符串字典序)比较,类型不可比时给出清晰错误。新增ExpressionRegressionTest(23 例)固化短路、字符串关系比较、运算符优先级、类型强制、内置函数与三元等语义——该包此前零测试。jkit-core:com.alianga.jkit.beans修复两处正确性问题。BeanUtils.copyProperties(srcMap, tgtMap, excludeFields)带忽略字段时,循环把源值写回了源 Map 而非目标 Map,导致目标 Map 完全没被更新(源反而被原地重写);改为写入目标 Map,与不带excludeFields的putAll分支行为一致。ObjectUtils.set(collection, "[n]", value)此前即便下标赋值成功也会在设完元素后无条件抛TypeNotMatchExecption,使集合下标赋值不可用;改为仅当 key 不是[n]形态时才抛错。新增BeansTest(17 例)覆盖四向拷贝、merge、集合下标赋值、多级路径、非空字段与isEmpty等语义——该包此前零测试。jkit-core:MultiKeyHashMap的读写两侧语义此前自相矛盾。类声明「获取值时可忽略 key 大小写」,但只有put登记了内部keyMap,其它入口都没跟上:putAll走父类实现,批量写入的条目完全无法被忽略大小写地读到(实测putAll({"UserName":"tom"})后get("username")返回null,而逐条put能取到值);remove只按精确 key 删除,put("UserName")之后remove("username")静默失败——size()不变、get("username")仍返回原值;remove/clear都不清理keyMap,已删除的 key 被长期持有无法回收;containsKey同样只认精确 key,与get给出相反答案。现在putAll逐条走put完成登记,remove支持忽略大小写删除并同步失效登记,clear一并清空keyMap,containsKey与get语义对齐。jkit-core:MultiKeyHashMap的忽略大小写匹配改用toLowerCase(Locale.ROOT)。原来的无参toLowerCase()依赖运行时语言环境,在土耳其语(tr-TR)环境下"ID"会被转成"ıd"(无点 i),写入侧与读取侧的规范化结果对不上,忽略大小写查找整体失效——实测put("ID", …)后get("id")返回null、containsKey("id")为false。上游 wast 亦有 14 处同类无 Locale 写法(集中在 HTTP 头与协议名处理),不在本库范围内。jkit-core:Maps.getNumber(及其派生的getInteger/getLong/getDouble/getFloat/getShort/getByte)不再把半截字符串当成数字。NumberFormat#parse(String)只做前缀解析,此前"123abc"返回123、"2024-01"返回2024、"1.2.3"返回1.2——格式错误的数据被静默转成合法数值继续往下传,且这种错误在 JDK 8 / 11 / 17 / 21 / 25 上表现完全一致,断言数值的调用方无从察觉。现在先trim再用ParsePosition校验整串被完整消费,未完整解析的一律返回null(走defaultValue重载时回落默认值)。副作用是带首尾空白的" 42 "由原来的null变为42,属行为变更。jkit-core:com.alianga.jkit.collection补齐首轮测试(此前零测试)。新增MultiKeyHashMapTest(12 例)、MapsNumberParseTest(9 例)、CollectionsTest(13 例,覆盖空判断、getFirst/getLast、按对象与谓词删除、查找与转换、size的 Map / 数组 / Iterator / Enumeration / 原始类型数组分支、contains与findValueOfType)。回退修复后 9 例失败,全部落在上述修复点上。另修正Collections.size(Object)的 javadoc——null实际返回0,并非声明的抛IllegalArgumentException;CountMap补充get与继承来的containsKey/getOrDefault的语义差异说明,判断「是否发生过计数」应统一用containsKey。jkit-core:com.alianga.jkit.html常驻内存反超 Jsoup 1.18.1(150 份 300 KB 页面 DOM:203.5 MB → 153.3 MB,由高出 13% 变为低 15%)。新增解析期字符串驻留缓存StringCache(开放寻址哈希表,容量与装载率有上限、超过 128 字符的长文本跳过),把重复的文本节点内容与属性值合并为同一实例——真实页面里 class 值、模板化列表文本高度重复,此前逐节点各建一份,字符串 + 字节数组合计比 Jsoup 多出 324 KB/DOM,是内存对比吃亏的全部来源(Jsoup 1.18 同样内置字符串缓存)。堆直方图逐类对比定位:集合、属性结构两侧 jkit 本就更省,只有字符串一项落后。解析开销约 5%(大页面 ~0.06 ms),选择器与端到端领先幅度不变;差分验证保持 112 组一致 / 7 组已知差异。jkit-core:HTML 选择器(Selector)对畸形输入全面收紧并修复崩溃。空选择器此前抛IndexOutOfBoundsException,",p"/"p,,p"这类残缺分组会因空复合对任何元素都返回 true 而静默匹配整个文档(含#document根),"p >"的末尾悬空组合符被静默忽略——现在空选择器、分组缺操作数、组合符缺右操作数一律抛SelectorException。属性选择器同步收紧:img[src(未闭合[)此前抛StringIndexOutOfBoundsException,[src^png]/[src!](操作符后缺=)此前被静默丢弃操作符、退化成存在性匹配,现均报错。:not()补齐完整语义:支持选择器列表(:not(a,p),此前静默丢弃第二个之后的参数)与含组合器的复杂选择器(:not(div p));伪类参数括号未闭合(p:nth-child(23)此前被截成nth-child(2)返回错误结果,现报错;不支持的伪类(如table:hover)改在解析期报错,不再因文档无候选元素而静默返回空结果;:nth-child(abc)抛SelectorException而非NumberFormatException。标签名归一补Locale.ROOT。jkit-core:HTML 树操作递归改迭代,防恶意深嵌套输入。解析主循环本是迭代实现,但text()/outerHtml()/nodeText()/ 选择器收集与Document索引构建都是递归——20000 层<div>能解析成功,随后调用任何读取 API 就StackOverflowError。上述遍历全部改为显式栈迭代,产出语义(文档顺序、块级元素补空格、索引文档序)不变。jkit-core:HTML 解析与序列化修三处。<!-->/<!--->是 HTML5 定义的完整空注释(abrupt-closing-of-empty-comment),此前被当作未闭合注释把剩余整个文档吞进注释;😀这类高于 BMP 的数字实体此前被(char)强转截断成 U+F600 乱码(现用Character.toChars生成代理对);<html lang="en">标签上的属性此前被静默丢弃(head/body都保留,唯独html丢)。jkit-core:选择器与Elements两处性能 / 一致性。分组选择器("p, .c")的去重由结果集线性contains(O(n²))改为HashSet标记;Elements.select落实文档声明的「合并去重」——嵌套div下doc.select("div").select("p")此前返回 2 个重复p,现与 jsoup 一致只保留一次。jkit-core:表达式求值器上一轮修复只落在通用求值路径,压缩后的 Impl 快速路径没跟上——解析后的表达式复用求值第 3 次起(ExprParser会在第 2 次求值后把表达式树压缩为Impl类)行为突变:flag && (1/0)此前短路返回false,第 3 次起抛除零异常;字符串"a" < "b"第 3 次起抛裸ClassCastException。现在LogicalAndImpl/LogicalOrImpl与通用路径一致短路求值并对齐异常包装,Gt/Lt/GE/LE四个 Impl 改用compareRelational;ExpressionRegressionTest新增「同一表达式复用求值 ≥3 次」用例堵住回归逃逸(此前的回归测试每条表达式只求值一次,恰好绕开压缩路径)。jkit-core:JSONPatch的test操作数值比较此前走doubleValue(),超过 2^53 的整数被截断产生假相等——9007199254740993与9007199254740992判等,两个相差 1 的 30 位BigInteger也判等(toBigDecimal对BigInteger还经longValue()截断);test是乐观锁原语,假相等会在前置条件不成立时误放行补丁。现在统一走BigDecimal精确比较,1与1.0语义相等保留。另按 RFC 6901 收紧数组索引解析:/a/01(前导零)、/a/+1此前被Integer.parseInt宽松放行,现报JSONPatchException。jkit-core:CircuitBreaker两处并发缺陷。状态转换此前基于进入任务前的陈旧快照做无条件set()——半开下 T1 / T2 并发探测,T2 失败置 OPEN 后 T1 成功仍会把熔断覆盖回 CLOSED 并清零失败计数(反向同理),与「多线程下安全使用」的类文档不符;半开状态下并发探测不限流,冷却结束瞬间放行的是全量流量而非「少量探测」。现在状态转换全部改 CAS 循环(openedAt先于 CAS 写入,避免并发半开转移读到旧时间戳),半开用探测许可保证同一时刻只放行一个探测,成功阈值 >1 时未达阈值续发许可继续探测;任务抛出的Error(OOM 等)保持原类型抛出,与Retryer语义一致。jkit-core:HttpClient的实例作用域此前是「执行后清空」而非「恢复旧值」——拦截器(如 token 刷新)里嵌套调用另一个客户端实例后,内层返回时把 ThreadLocal 清成 null,外层剩余链路(代理 / CookieJar / 超时 / 引擎)静默回退到全局配置且无任何报错。现在保存进入前的作用域并在 finally 恢复;原「执行后清空」测试固化了错误语义,已改为恢复语义并补嵌套场景回归。jkit-core:MultiKeyHashMap的读写一致性修复只完成一半。remove无条件清除小写别名——put("A",1); put("a",2); remove("A")后幸存的"a"条目忽略大小写查找失效(get("A")返回 null,比修复前还回退),现在删除后按现存条目重建别名;putIfAbsent/compute/computeIfAbsent/computeIfPresent/merge未覆写、绕过put的 keyMap 登记,忽略大小写查找对这些入口静默失效,现全部补齐;remove(null)此前因String.valueOf(null)塌缩成"null"误删键为字面量"null"的条目,现在 null key 不参与忽略大小写匹配(get(null)/containsKey(null)也不再误命中"null"条目)。类 javadoc 补充视图迭代删除不维护 keyMap 的限制说明。jkit-core:JwtUtils加固。HS256 共享密钥不足 32 字节(256 位)时签发与验签均抛IllegalArgumentException(低熵密钥签出的令牌可被离线爆破,与类文档「建议不小于 32 字节」对齐);verify新增leewaySeconds重载,exp / nbf 校验可配置时钟偏移余量(默认 0,行为不变);分段解析改用split("\\.", -1),"h.p.sig.."这类多余分段不再被默认 split 吞掉尾部空串后按 3 段接受;签名段的 base64url 解码异常包装为JwtException(此前裸抛IllegalArgumentException,破坏@throws JwtException契约)。jkit-core:EncryptUtils.RSA的 String 版encryptOaep/decryptOaep不再吞异常返回空串——解密失败(密钥错、密文被篡改的BadPaddingException)与「明文本身为空」此前不可区分,调用方不检查就把空串当解密结果落库属静默数据损坏;现改为向上抛出(签名补throws Exception,与同组byte[]变体一致),顺带修掉日志里永不生效的%s占位符。jkit-core:DesensitizeUtils单字符输入不再原样返回——mask在保留位数覆盖全文时输出「首字符 + 掩码」,长度为 1 时退化为原字符串,单字姓名(name("王"))整条敏感信息原样漏出;现在单字符整体打码。jkit-core:ULID三处。单调模式 80 位随机位溢出时此前随机部分归零而时间戳不动,产生一个字典序小于前驱的 ID,违反「单 JVM 严格递增」的文档承诺(同批UUIDv7是进位到下一毫秒的),现对齐为溢出进位;parse补 48 位时间戳上限校验(此前parse("ZZZ…")放行而encode拒绝,parse(x).toString()往返不自洽),isUlid同步覆盖;toBytes去掉同一字符串的重复解码。jkit-core:Retryer对中断的语义。任务自身抛出的InterruptedException此前被当普通失败重试且不清中断标志——多烧一次尝试、吞掉取消信号,现在恢复中断标志并立即终止重试;睡眠被打断时,触发重试的最后一次业务异常不再被丢弃(以 suppressed 附加在ResilienceException上)。jkit-core:HTTP 实例化收尾。UrlConnectionHttpEngine写请求体时的拷贝缓冲改走 active-aware 的HttpUtils.config()(此前固定读全局HttpConfig.shared(),实例级downloadBufferSize不生效);method字段的延迟查找缓存补volatile;HttpConfig.copy()的 javadoc 如实说明executor/endpointPool/sslContext/retryPolicy为共享引用(原「深拷贝」表述失实);删除无调用点的HttpUtils.currentDefaultMediaType()。
2.0.1 - 2026-09-13
仓库拆成多模块。本版本随父 POM 一并交付 jkit-sql、jkit-sql-auto(及 Spring Boot 2 / 3 starter)、jkit-notify、jkit-notify-extra、jkit-curl-codegen。坐标 com.alianga:jkit 仍指向 jkit-core。
新增
jkit-sql
- 新模块
com.alianga:jkit-sql:零依赖手写 SQL 解析器(词法char[]+ 关键字开地址哈希,递归下降 AST)。入口SQL.parse/parseAll/parseExpr/format/toSqlString/tables/stat/parameters/parameterize/exportParameterValues/wall/clone/eval/rewrite。非法 SQL 抛SqlParseException。用法见docs/sql.md。 - 方言:一等枚举
MYSQL(默认)、POSTGRES、ORACLE(12c 以下 ROWNUM 分页)、ORACLE12(OFFSET/FETCH)、SQLSERVER、ANSI、H2、DB2、SQLITE、HIVE(别名maxcompute/odps/argo)、CLICKHOUSE、PRESTO(别名trino)、DAMENG(dm/dameng/jdbc:dm:)。fromName覆盖 GoldenDB / SelectDB / AnalyticDB / MatrixOne / StoneDB / HighGo / UXDB / MogDB / Vastbase / AntDB / IvorySQL / GBase 8a / GBase 8s / 行云 / Oscar 等别名,与 icellcommon-model的 SQL 型数据源对齐。能力由SqlDialectSpec驱动,可用SqlDialectWrapper按需覆写单条能力(如 MySQLANSI_QUOTES)。 - 分页:
getLimit/getOffset/setLimit/setOffset/setPage/addLimit/SQL.adaptPagination。format/toSqlString按目标方言适配(MySQLLIMIT↔ 经典 ORACLE ROWNUM ↔ ORACLE12OFFSET FETCH↔ SQL ServerTOP|OFFSET FETCH)。SqlBuilder.limit/offset按有效方言生成,不再一律输出LIMIT。 - 改写链:
SqlRewriteHook+SqlRewrites(addLimit/setPage/andWhere/replaceTable/replaceColumn/adaptPagination/addSelectItem/removeSelectItem可与自定义规则混排)+SQL.rewrite(先深拷贝再改)。跨方言函数改写覆盖 JOIN ON / MERGE / OVER / 列DEFAULT:DATE_ADD/DATEDIFF/FROM_UNIXTIME/UNIX_TIMESTAMP/SUBSTRING/LEFT/RIGHT/DECODE/NVL2/UCASE/LCASE/CONCAT_WS/LPAD/RPAD/YEAR/MONTH/DAY/HOUR/MINUTE/SECOND/SYSDATE/LAST_DAY/CHAR/CHR等;FULLTEXT 升为MANUAL_ACTION_REQUIRED。函数改写走SqlSchemaConverterProvider.registerFunctions(SPI 后覆盖)。 - 实体 DDL:
SqlEntities扫描@SqlTable/ JPA / MyBatis-Plus / MyBatis@Alias(无第三方编译依赖)。公开columnSql/columnTypeSql/createIndex/orderByForeignKeys/extraSql/sequenceSql/indexName/createTable(..., includeIndexes)。认任意简单名为Comment的注解与 Hibernate@ColumnDefault;@SqlTable(comment)/@SqlColumn(comment)按方言生成注释;Oracle ≤11g 无 IDENTITY 时附录 SEQUENCE + TRIGGER。 - 格式化与解析选项:
SqlFormatOptions.keywordCase(AS_IS/UPPER/LOWER)、quoteIdentifiers;SqlParseOptions.pipesAsConcat/keepComments/placeholders()(模板占位符)/statementParsers()(按前导关键字挂自定义语句解析器)。 - 安全与扩展:
SQL.wall→SqlWallResult;SqlWallRule规则链 +SqlWallConfig.rules(...)(内置 selectOnly / denyDdl / 无 WHERE 写等,行为与违规码不变)。SqlAstVisitor类型分发(不破坏SqlVisitorAdapter)。 - 语句 AST:
EXPLAIN/SET/COMMENT ON/SHOW/ANALYZE|VACUUM|OPTIMIZE/ 事务控制 /COPY/FLUSH/START TRANSACTION/LOAD DATA/LOCK TABLES/PREPARE/BEGIN…END/ 过程·触发器·事件(SqlRoutineParam/SqlControlStatement/SqlDeclareStatement/SqlHandlerStatement)升为独立类型;MODEL/MATCH_RECOGNIZE结构化;RENAME TABLE独立语句。 - SqlBuilder:流式 SELECT / INSERT / UPDATE / DELETE,
join/leftJoin/rightJoin/fullJoin/crossJoin/union/with/distinct/groupBy/having。 - 解析覆盖:相对 Druid BVT / JSqlParser 内联 / 文件语料约 96.3% / 92.3% / 90.0%。窗口、CTE、MERGE、PIVOT、闪回、过程块、Hive / ClickHouse / Informix / DB2 等边角语法见
docs/sql.md,不在此按迭代轮次罗列。
jkit-sql-auto
- 新模块
com.alianga:jkit-sql-auto:启动时按实体自动建表 / 更新表结构。扫描@SqlTable/ JPA / MyBatis-Plus,对照DatabaseMetaData执行CREATE TABLE/ALTER TABLE ADD/CREATE INDEX。模式:none/validate/update(默认,只追加)/create/create-drop。配置前缀jkit.sql.auto.*,数据源可回落spring.datasource.*。运行时零第三方依赖。 SqlAuto.drop按外键逆序删托管表;Spring Boot 2 / 3 自动配置模块jkit-sql-auto-spring-boot-2、jkit-sql-auto-spring-boot-3。- 表 / 列注释按方言执行(
COMMENT/COMMENT ON/sp_addextendedproperty);Oracle ≤11g 自增主键用 SEQUENCE + TRIGGER,达梦列上写 IDENTITY。 postgresIdentityStyle/foreignKeys/autoIncrement:给 OpenGauss / GBase 8a / DuckDB 等能力较窄的产品关掉对应 DDL。table-prefix(jkit.sql.auto.table-prefix/ 链式tablePrefix(String)):所有自动建表名统一加前缀(如t_),作用于建表 / 改表 / 删表 / 索引 / 序列 / 外键目标表。index-prefix-enabled(jkit.sql.auto.index-prefix-enabled,默认true):自动派生的索引名是否也带表前缀;实体里显式@Index(name=…)始终原样保留。
jkit-notify
- 新模块
com.alianga:jkit-notify:钉钉(含加签)/ 企微 / 飞书(含签名)/ Server酱 / Bark / 通用 Webhook / SMTP(纯 Socket:AUTH LOGIN + STARTTLS/SSL + MIME)。NotificationChannelSPI +NotificationManager;MessageType(TEXT / MARKDOWN / HTML)。零第三方依赖。用法见docs/notify.md。 - 失败分类:
SendResult.failureType()/isRetryable(),FailureType为 RETRYABLE / THROTTLED / CONFIG_ERROR / PERMANENT / SUPPRESSED;未收录错误码回退 HTTP 状态。自定义渠道覆写AbstractHttpChannel.classify。 - 消息按 UTF-8 字节截断(不劈开汉字与 emoji);钉钉 @人自动把缺失的
@手机号追加进正文;Message.var/vars替换${key}/${a.b};附件流式读取、按10MB/512KB拆包、自动识别 MIME。 - SMTP:
sslProtocols钉扎 TLS 版本、trustAllCerts支持自签网关;MARKDOWN 经NotifyUtils.markdownToHtml按text/html发送。飞书加签写入 JSON 请求体。NotifyPolicy:静默时段、5 分钟去重、本地限流;sendFailover多账号顺序切换。 - 可选模块
com.alianga:jkit-notify-extra:Slack / Telegram / ntfy / 阿里云 / 腾讯云 / 云片 / 华为云短信。渠道 extras 在各自实现类上,不放进核心Message。
jkit-core
DateUtils.parse(String):自动识别时间戳、紧凑数字、-/.、中文/韩文、ISO-8601(含T/Z/+0800/+08:00)。DateUtils.fromEpochNumber(long):10 位秒或 13 位毫秒时间戳转Date。DateUtils.fromTemporal(TemporalAccessor):java.time时间对象转Date。ConvertUtils.toDate额外支持Instant、OffsetDateTime、ZonedDateTime。HttpUtils.debug/HttpUtils.printCurl:发送前把请求摘要或等价 curl 打到标准输出(不要在生产打开)。ConfigLoadOptions.addLocation:在默认搜索路径上追加目录(classpath:/file:/ 裸路径 /File,~展开为user.home)。Print.enableLog:为false时只打控制台,不再写入 JUL。com.alianga.jkit.log.LocaleFormatter:固定 Locale 的 JUL 格式化器。
jkit-curl-codegen
- 从 jkit 拆出独立 artifact
com.alianga:jkit-curl-codegen,入口CurlCodegen.generate(id, curl)。覆盖 34 种目标:Java(jkit / JDK 11+ / OkHttp / Apache 5 / HttpURLConnection / Unirest)、Kotlin、JavaScript(fetch / axios / request / jQuery / XHR 等)、Python(requests / httpx)、Go、C#、PHP、R、Rust、Swift、Ruby、Lua、PowerShell、curl(含 Windows cmd / PowerShell)、wget,以及http/har/httpie互操作格式。
变更
jkit-sql:破坏性 —SQL.andWhere/replaceTable/replaceColumn改为与addLimit/setPage一致的 clone-then-mutate(返回新 AST,不污染原树);调用方须使用返回值。jkit-sql:SQL.clone改为真正的 AST 树拷贝(SqlAstCloner/SqlNode.copy),热路径不再 format→parse;跨方言format/adaptPagination/setPage显著加速。经典 ORACLE offset=0 单层 ROWNUM 包装免 clone。jkit-sql-auto:dryRun(true)的SqlAuto.run(options)/plan(options)不再打开 JDBC / DataSource。URL 仅用于推断方言,按空库规划全量CREATE TABLE。已传入Connection的重载仍对照活表,只是不执行。jkit-core:DateUtils去掉SimpleDateFormat。yyyy-MM-dd HH:mm:ss走char[]+ 秒级缓存,其余常见 pattern 走手写拼接或DateTimeFormatter。ConvertUtils.toDate(Object)改为按数字字段抽取。常见字符串解析约快一个数量级,结果与 ZmlTools 对齐。jkit-core:JUL 默认格式改为英文级别名(WARNING/SEVERE),不再随 JVM 默认语言变化。jkit-core:RandomUtils.randomBirth改为LocalDate(minAge == maxAge不再抛异常);getUUID去-不再走正则;IdCardUtils随机生日按当月实际天数生成。jkit-core:curl 解析对齐 curl 语义(展开-kLs/-XPOST,--json/--data-urlencode,@file不再当字面正文)。公开 API:parseCurl/curlToRequest/curl/curlString/requestToCurl,模型ParsedCurlRequest。CurlParser.generate与http.codegen包从 jkit 移除,改依赖jkit-curl-codegen。jkit-notify:核心ChannelConfig移除 SMS 专用template/appId/region,改用ChannelConfig.extra+AbstractSmsChannel.CFG_*。核心渠道仅由defaults()注册,SPI 只加载 extra 模块,避免双重注册。jkit-notify-extra:SlackChannel.EXTRA_CHANNEL替代用Message.EXTRA_GROUP表示 Slack 频道(短暂回落兼容EXTRA_GROUP)。
修复
jkit-core:响应声明Content-Encoding: gzip但正文为空时,JDK 8 不再抛EOFException,返回空流(关闭时仍回收底层连接)。jkit-sql:子类与MappedSuperclass/ 父类重复声明同名列时只保留子类字段,避免 PostgreSQLcolumn specified more than once。jkit-sql:@GeneratedValue(generator="system-uuid")/GenerationType.UUID/ 非整数@SqlGenerated不再生成AUTO_INCREMENT/IDENTITY(PostgreSQL 对VARCHAR主键写 IDENTITY 会语法错误)。jkit-sql:自动生成的索引名 / 序列名 / 触发器名按方言标识符长度上限截断(经典 Oracle 30 字符,超长时保留前缀 + 4 位散列)。未命名索引改为{table}_{col}_idx,同表多个不再撞名。公开SqlDialectSpec.maxIdentifierLength/fitIdentifier。jkit-sql:SQL.formatpretty 模式对CREATE TABLE按列换行缩进(toSqlString/ compact 仍单行)。jkit-sql:GROUP_CONCAT无显式SEPARATOR时转STRING_AGG/LISTAGG不再丢分隔符;MySQLMODIFY/CHANGE转 PG/ANSI/H2/PRESTO 时不再把列名写重。jkit-sql:SUBSTRING/LEFT/RIGHT按方言回写(PG/MySQL/H2 用FROM n FOR m,SQL Server/SQLite/Hive/ClickHouse 用逗号形态;负起点按LENGTH/LEN改写);达梦裸 SELECT 补FROM dual。jkit-sql:SqlDialectWrapper不再委托派生方法(identQuoteClose/quoteIdent/pipesAreConcat/preferredLimitStyle),子类只覆写原语时派生能力跟着变。jkit-sql:回写保真 —NATURAL JOIN不再丢修饰符;RENAME TABLE不再写成非法ALTER TABLE;ADD UNIQUE KEY不再丢UNIQUE;INSERT/UPDATE/DELETE的IGNORE/LOW_PRIORITY/HIGH_PRIORITY入 AST 并回写;非裸标识符别名强制加引号。jkit-sql:tables()补漏CREATE TRIGGER/CREATE TABLE … LIKE/RENAME TO/ 多表OPTIMIZE|ANALYZE;库名、例程名、CTE 名不再误计。仅注释 / 空白输入归为OTHER,不再抛empty SQL。jkit-sql-auto:经典 Oracle 下自动生成的CREATE INDEX名超过 30 字符时截断,避免 ORA-00972。jkit-curl-codegen:R(httr2)改用req_perform();PowerShell 5.1 受限头 / Cookie 逗号 / 中文乱码;Windows curl 拆成 cmd(shell-curl-windows)与 PowerShell(shell-curl-powershell)两个生成器。
构建
- 仓库改为多模块:父 POM
com.alianga:jkit-parent,运行时库在jkit-core(发布坐标仍是com.alianga:jkit),其余能力各一模块。根目录mvn test同时构建全部模块。 git-commit-id-plugin、buildnumber-maven-plugin、maven-source-plugin从publishprofile 挪到默认构建,package即可产出带构建信息的 sources jar。- 整条 reactor 可用 JDK 8 启动 Maven:其余模块按 JDK 8 编译;
jkit-core的META-INF/versions/9、/11经 toolchain 走 JDK 9 / 11;仅jkit-sql-auto-spring-boot-3经 toolchain 走 JDK 17(Spring Boot 3 要求)。依赖本机~/.m2/toolchains.xml已声明 jdk 8/9/11/17/21。启动示例:JAVA_HOME=$(jdk8) mvn -o clean install。 jkit-notify实发用例须-Djkit.notify.live=true且 yml 凭证齐全;默认mvn test即使有密钥也不实发。
文档
docs/sql.md/docs/en/sql.md按代码对齐跨方言分页、SQL.clone、SqlRewrites、SqlEntities公开方法与方言表;两侧 API 覆盖已对齐。README 与模块 README 去掉内部开发计划入口。docs/sql-auto.md(及英文)按「选包 → 写实体 → Spring Boot / 非 Spring → 配置项」重排;模块 README、快速开始页同步。
2.0.0
jkit 首个对外版本,由 ZmlTools 迁移而来:零第三方依赖、包名改为 com.alianga.jkit,坐标 com.alianga:jkit:2.0.0。
能力概要见 README.md:CSV / HTTP / JSON / YAML / 配置 / 表达式等均用纯 JDK 重写。从 ZmlTools 迁过来的项目可继续用 relocated/zmltools 把 top.wuyongshi:ZmlTools:2.0.0 重定向到本坐标(包名仍需手工替换)。
如何记录后续版本
- 把根
pom.xml(jkit-parent)的<version>改成新版本号,子模块继承该版本;同步README.md页头与依赖示例、Main.java打印的版本。 - 在简介段落后、当前最新版本小节上方插入新版本,日期用当天(
YYYY-MM-DD)。同一版本号只有一个二级标题;未发版前把新条目追加进该小节,不要再开## x.y.z / unreleased。 - 按「新增 / 变更 / 修复 / 构建」分类写清调用方能感知的变化(多模块时用加粗模块名分组)。不要只贴 git 标题,不要按内部里程碑或竞品覆盖率轮次记账(那些写
docs/sql.md/docs/next-plan.md)。 - 本次新增的公开 API 在 javadoc 里补
@since x.y.z。 - 历史版本留在原处,不要把旧条目改写成新版本。
模板:
## x.y.z - YYYY-MM-DD
### 新增
- ...
### 变更
- ...
### 修复
- ...
### 构建
- ...没有某一类变化时省略对应小标题即可。