Skip to content

The changelog is maintained in Chinese. An English translation may be added in the future; version numbers, class names and code identifiers remain readable as-is.

Release Notes ​

This document records user-visible changes in each jkit release. Each version number appears once, listed from newest to oldest.

Unreleased changes are appended to the current version section (currently 2.0.4); once released, that section is frozen and a new version section is added above it. Do not rewrite frozen historical versions, and do not open a new unreleased heading for the same version number.

2.0.3 - 2026-09-27 ​

Added ​

jkit-mock

  • New jkit-mock module (coordinates com.alianga:jkit-mock:2.0.3, JPMS module name com.alianga.jkit.mock) with no third-party dependencies. Two modes sharing one corpus: a template mode that mirrors Mock.js 1.1.0 (corpora and behaviour were extracted by running the original library, not by reading its docs), and a field mode that generates records from built-in field definitions.
  • Template mode, four parts: MockRule parses the four rule forms 'name|min-max', 'name|+step', 'name|count', 'name|min-max.dmin-dmax'; MockRandom supplies 58 placeholders across Basic / Date / Image / Color / Text / Name / Web / Address / Helper / Misc plus a dedicated Chinese set, backed by 500 frequent Chinese characters, 263 top-level domains and 35 provinces / 366 cities / 2595 counties, with value distributions matched to the original; MockRegex generates text from a regex (recursive descent covering groups, alternation, quantifiers, character classes, back-references and lookahead); MockJs is the template engine, accepts single-quoted templates and leaves unknown placeholders untouched. Extends Mock.js with fractional ranges: 'price|9.9-999.2' parses correctly (the original turns the trailing .2 into a bogus price.2 key).
  • Field mode: MockDataGenerator supplies 30 built-in fields (personal / commercial / technical) and 5 quick templates (MockTemplate); MockCustomField / MockCustomFieldType define custom fields, MockDataProducer drives generation, and MockDataFormatter exports to JSON / CSV / SQL INSERT / XML (MockOutputFormat).
  • MockSchema: generates sample data from a JSON Schema, configured through MockSchemaOptions. Supports type / properties / required / items (including tuple form) / additionalProperties and enum / const / default; supports allOf merging, oneOf / anyOf branch sampling, and $ref resolution against definitions / $defs / components.schemas with a maxDepth guard against recursive-schema infinite descent. Strings are resolved by pattern (handed to MockRegex) > format (email / date-time / uri / uuid, …) > property-name heuristics (name yields a Chinese name, email an email address, phone a mobile number, createdAt a timestamp), then clamped to minLength / maxLength; numbers honour minimum / maximum / exclusiveMinimum / exclusiveMaximum / multipleOf. MockSchemaOptions is a builder controlling whether optional properties are emitted, the default array length, whether default wins, and the maximum recursion depth. Entry points are mock / mockJson / mockMany / samples, and passing a seeded MockRandom reproduces the same output.
  • Business placeholders and an extension point: MockRandom.extend() mirrors Mock.js Random.extend, registering custom placeholders (built-ins may be overridden), with allPlaceholders() listing every available name. Sixteen business placeholders added — @phone @gender @company @department @position @salary @bankCard @creditCard @currency @mac @userAgent @password @token @timestamp @fileName @mime — so template mode can now produce MAC addresses, bank cards, User-Agents and friends that previously were out of reach.
  • Placeholder lookup groundwork: MockRandom.placeholderGroups() returns placeholder names grouped into 12 categories (basic / date / image / color / text / name / web / address / helper / misc / business / Chinese), and sample(String name) returns a recommended invocation with typical arguments (e.g. @integer(1,100), @city(true), @cparagraph(3), falling back to "@" + name), laying the foundation for grouping and search in a quick-reference panel.
  • SQL output options: MockSqlOptions covers TypeMode (AUTO inferring from values / all VARCHAR / all TEXT), batched INSERT (multiple VALUES rows merged into one statement), quoting (none / backticks / double quotes), table name (defaults to fake_data) and whether to emit a CREATE TABLE statement. MockDataFormatter.toSql(records, options) honours them and the no-arg overload is kept for compatibility; AUTO infers types from the first record (integers BIGINT, decimals DOUBLE, booleans BOOLEAN, everything else VARCHAR(255)), and MockDataProducer.setSqlOptions(options) exposes the same to field mode.
  • MockValid validates templates and derives JSON Schema; MockJs.normalizeQuotes(String) was promoted from package-private to public for callers, and the new MockJs.prettyTemplate(String) first folds single quotes into double quotes, then pretty-prints valid JSON — invalid templates are returned untouched.

jkit-sql

  • Dameng bare procedure calls: system-procedure invocations without the CALL keyword in disql scripts (e.g. SP_SET_PARA_VALUE(1, 'HJ_BUF_GLOBAL_SIZE', 4000);) previously threw "unsupported statement" at an IDENT ( at statement start. They now parse as CALL (arguments into the AST, schema.proc(...) qualified names supported), write back in the bare form, and survive the parse → format → parse round-trip. New dialect capability SqlDialectSpec.supportsImplicitProcedureCall(), enabled only for DAMENG (Oracle-family bare calls are only valid inside PL/SQL blocks); other dialects are unchanged. SqlSimpleStatement.implicitCall() distinguishes bare calls from explicit CALL.
  • Entity scanning now recognizes classes carrying only JPA @Table (without @Entity; both javax and jakarta); previously they were not scanned even though table/column/index mapping already read @Table.
  • @SqlColumn gains defaultValue: declare a column default as a raw SQL fragment (no DEFAULT keyword) — strings include their own quotes (@SqlColumn(defaultValue = "'N'")), functions/expressions are written as-is ("0", "CURRENT_TIMESTAMP"). It takes precedence over Hibernate @ColumnDefault and flows through DDL generation and auto-DDL.
  • SqlReservedWords: per-dialect reserved-word registry (SQL-standard core plus MySQL / PostgreSQL / Oracle / SQL Server / H2 / DB2 / DM / Hive / ClickHouse extensions) with isKeyword(dialect, name). SqlSchemaConvertOptions gains keywordQuote and keywordQuotedListener: identifiers stay unquoted by default (case folding is left to the database), but reserved-word collisions are quoted automatically and reported through the callback once per identifier; jkit-sql itself does no logging, the host decides how to warn.

jkit-sql-auto

  • Connected dry-run: new connectOnDryRun(true) / config jkit.sql.auto.connect-on-dry-run. With dryRun(true) it now opens the configured DataSource to read metadata and diff against live tables, so plan.sql() / plan.changes() contain the actual incremental changes (CREATE for missing tables, ADD COLUMN for missing columns, …) instead of a full CREATE against an empty schema — but nothing is executed, locked, or recorded. With no connection configured it falls back to offline planning.
  • Reserved-word auto-quoting: table / column / index names stay unquoted by default; when one matches a target-database reserved word (e.g. order, desc, value) it is quoted automatically as a fallback with a WARN (once per identifier). CREATE / ADD / ALTER / DROP / CREATE INDEX / COMMENT all stay in the same form, so subsequent startups do not re-alter tables due to a shape mismatch. Always on, no configuration.

Changed ​

jkit-mock

  • Streaming generation and formatting, fixing the memory blow-up and UI freeze when generating 100k rows: MockDataFormatter gains Appendable variants of formatTo / writeJson / toCsv / toSql / toXml, MockDataProducer gains formatTo, and field mode now emits row by row instead of materialising a full List<Map>. A new lazy container, MockRepeat, reuses a single MockJs instance and generates while iterating (size() is known without expansion); genArray returns it when streaming and count >= 1000, so 100k objects never pile into an ArrayList — peak heap drops from 1.96 GB to roughly 150 MB. The String-returning APIs are unchanged and now delegate to the streaming path.
  • MockValid goes streaming: a Progress callback reports progress for arrays and lazy structures (MockRepeat) element by element and lets the caller cancel mid-way; typeName classifies both MockRepeat and Iterable as arrays, avoiding false negatives and driving per-element recursion. The non-cancellable synchronous path stays backward compatible.
  • Identity fields are now coherent: a record fixes its gender and birth date first, then derives the ID card number, with age and address parsed back from that same document, so province / city / district no longer each roll their own. The user-info quick templates now include ID card and birth date, so one click shows a single consistent identity across card number, birthday, gender and age.
  • Both modes share one corpus: 20 dictionaries previously private to MockDataGenerator (surnames / companies / departments / job titles / User-Agents / MIME types / bank-card prefixes, …) move into MockDict instead of being maintained twice. The address dictionary deliberately stays duplicated (field mode follows FeHelper's flat concatenation, template mode follows Mock.js's real province/city/district hierarchy — different semantics, not force-merged). JSON output funnels through MockDataFormatter.prettyJson(), so both modes indent two spaces per level (template mode used to emit a single compact line).

Fixed ​

jkit-mock

  • MockSchema collapsed to the bound when only one numeric bound was given: minimum alone or maximum alone degenerated the range to [x, x], so generated values always equalled that bound. minimum alone now derives an upper bound (DEFAULT_NUM_SPAN = 1000); maximum alone derives a lower bound (starting at 0 when the bound is non-negative). The [0, 100] default applies only when both sides are omitted.
  • MockJs lost fractional digits in its upper-bound guard: hitting the integer part of the bound zeroed the decimals, so 100.00 came back as 100.0 and contradicted the two decimal places declared by the template. When the range is non-degenerate the integer part now steps back by one (99.xx < 100), preserving both the range limit and the digit count.

jkit-http

  • An explicit preferHttp2 is no longer overwritten by the global default: HttpUtils.applyDefaults null-guards connect and read timeouts with isXxxSet(), but preferHttp2 was unconditionally overwritten with HttpConfig.isHttp2(), so an explicit preferHttp2(false) was silently dropped and plaintext http:// requests still sent Upgrade: h2c. Servers without h2c support (e.g. Next.js / Node dev servers) close such connections without a response header, surfacing as HTTP/1.1 header parser received no bytes. HttpRequest now carries a preferHttp2Set flag (preserved through copy(), exposed via isPreferHttp2Set()), and applyDefaults applies the global default only when nothing explicit was set — same semantics as the timeouts.

jkit-notify

  • A single DingTalk custom-group-robot news item (NEWS) is now sent as msgtype=link with title, summary, cover and jump URL, instead of the feedCard form that dropped the body text.
  • WeCom IMAGE accepts only EXTRA_PIC_URL with a public URL; it is downloaded and converted to base64 + md5 before sending. Adds Message.imageUrl and NotifyUtils.downloadBytes.

jkit-sql-auto

  • With quote-identifiers on, the CREATE INDEX table name was quoted without case folding, breaking on upper-folding databases (Oracle / H2 / DM); index columns were never quoted, so reserved-word columns always failed to get indexed. Index statements are now generated with the same quoting rules as CREATE TABLE.
  • SqlAuto.run(Connection, options) (the Spring Boot starter entry) skipped identifier-case detection, so quote-identifiers and reserved-word quoting never took effect on the starter path; the drop entry is fixed the same way.
  • The PostgreSQL metadata lock was never actually acquired: pg_advisory_lock returns void, which PG JDBC maps to an empty string, and Boolean.parseBoolean("") is always false — the lock silently never worked on PostgreSQL since 2.0.2. Void / empty-string results are now treated as success, so the lock genuinely engages on PostgreSQL.
  • Reserved-word list completed from multi-database testing: GBase 8a additionally reserves LEVEL / SORT (collapsed into the MYSQL dialect in jkit; unquoted they are straight syntax errors).
  • Metadata-lock timing: the plan (metadata inspection) used to run before acquiring the lock, so two concurrently cold-starting instances could replay stale plans. The plan is now recomputed under the lock; lock-acquisition failure (60s default wait) logs a WARN and continues without the lock instead of failing silently.
  • Spring Boot starter with multiple DataSource beans and no unique candidate now logs a WARN and skips (hinting to mark one @Primary or set jkit.sql.auto.url) instead of throwing NoUniqueBeanDefinitionException; an invalid phase value logs a WARN.
  • An unknown jkit.sql.auto.mode value (e.g. valdate) silently fell back to UPDATE and modified the schema; it now logs a WARN.
  • History-table write failures are caught as Exception | LinkageError instead of Throwable; the GBase 8a "CREATE INDEX skipped" log is emitted once and only when the plan actually contains indexes.

Build ​

  • flatten-maven-plugin with ${revision}: the parent POM version becomes ${revision}, sourced solely from the revision property in that POM and overridable with -Drevision=x.y.z. The parent version and every inter-module dependency across the 8 sub-modules now reference ${revision}, replacing the per-module jkit.version relay property — publishing a release now means editing revision in exactly one place. The plugin runs with resolveCiFriendliesOnly during process-resources to emit a .flattened-pom.xml with versions resolved, and that is the POM installed / deployed (${revision} inside sub-module dependencies resolves by inheriting the concrete revision property from the published parent); the source POM is otherwise untouched and consumers see no difference. .flattened-pom.xml is git-ignored. Verified with reactor-wide clean package / install, plus an out-of-reactor scratch project resolving the full internal dependency chain of jkit-sql and jkit-sql-auto-spring-boot-2 offline.
  • Automatic-Module-Name on every published jar (a first step towards JPMS): the parent POM configures maven-jar-plugin to write it, with each sub-module supplying the name through its jkit.module.name property (the core is com.alianga.jkit, the rest follow com.alianga.jkit.* — e.g. jkit-mock is com.alianga.jkit.mock). No module-info is introduced, so JDK 8 and classpath users are unaffected; consumers on the module path get a stable module name that no longer derives from the jar file name.

2.0.2 - 2026-09-19 ​

Added ​

jkit-notify

  • MarkdownTheme: 13 rendering themes for Markdown → HTML (default / lark / orangeheart / rainbow / lapis / phycat / blue / vue / green / wheat / ayer / purple / blog). The first 12 match doocs/md; blog copies alianga.com (Halo · LIlGG_Sakura) article styling — coral links, warm-yellow inline code, dark One Dark fences, ¶/# heading marks and dashed list boxes. MarkdownTheme.of("lark") resolves by id, ignoring case and - / _, and falls back to the default theme for unknown values.
  • MarkdownRenderOptions plus Markdown.toHtml(md, options) / toDocument(md, options): pick theme, code highlighting, inline styles and responsive behaviour in one place. inlineStyle(true) writes styles into every tag's style attribute for Outlook / corporate mailboxes / WeChat pasting, all of which strip <head><style>. Markdown is now the public entry point; NotifyUtils.markdownToHtml(md) / markdownToDocument(md, boolean) / wrapHtmlDocument(fragment, boolean) remain and are deprecated.
  • Syntax highlighting for fenced code blocks (on by default, disable with .highlight(false)): a dependency-free lexer covering java / js / ts / go / python / sql / shell / yaml / properties / json / xml / html. It emits inline <span style>, so it renders in both modes; unknown languages fall back to plain escaping.
  • SmtpChannel.markdownTheme(theme) / inlineMarkdownStyle(boolean): pin the Markdown rendering theme for the SMTP channel (default: classic theme + <style> mode).
  • Local image embedding: with MarkdownRenderOptions.imageBaseDir(dir) (or SmtpChannel.markdownImageBaseDir(dir)) every Markdown image that points at a local relative path is converted to data:image/...;base64,... and written into src, so the body stands alone. Only local paths are touched — http(s):// / // / data: / cid: are kept as-is; ./, ../, absolute paths and the file: prefix all resolve. Missing files, non-images (MIME from suffix and magic bytes) and files above maxInlineImageBytes (default 2MB) keep the original src — no exception is thrown.
  • MarkdownRenderOptions also gained .inlineImage(boolean) and .maxInlineImageBytes(long) to toggle embedding and cap the per-image size.

jkit-sql

  • JdbcUrlUtils: parse JDBC URLs (host / cluster nodes / database / schema / parameters), infer SqlDialect via fromUrl, return a short type name via getDbType, and guess the driver class via driverForUrl / getDriverClassName. Covers MySQL replication and load-balance, PostgreSQL HA, Oracle SID/Service/RAC, SQL Server, H2, Gauss/openGauss, Dameng, and more. PostgreSQL-family URLs read schema from currentSchema (default public).
  • com.alianga.jkit.sql.entity.Comment: first-party table/column comment annotation (TYPE+FIELD, value()). Sits alongside Hibernate @Comment and @SqlTable(comment) / @SqlColumn(comment); the scanner still matches the simple name Comment, so jkit-sql-model and similar modules can prefer this package.
  • Complex SQL dialect slice L2/L4 harness (Oracle): ComplexSqlOracleSliceL2* (jkit-sql) and ComplexSqlOracleNativeExecute* (tools-test); 001–300 native execute on oracle19c 300/300 (parseFail=0 / execFail=0); reports under target/complex-sql-reports/oracle-l2|l4-*.
  • Complex SQL dialect slice L2/L4 harness (MySQL): ComplexSqlMysqlSliceL2* (jkit-sql) and ComplexSqlMysqlNativeExecute* (tools-test); 001–300 native execute on MySQL 3308/test_db 300/300 (parseFail=0 / execFail=0); reports under target/complex-sql-reports/mysql-l2|l4-*.
  • Complex SQL dialect slice L2/L4 harness (SQL Server): ComplexSqlSqlServerSliceL2* (jkit-sql) and ComplexSqlSqlServerNativeExecute* (tools-test); 001–300 native execute on jkit_ss_test (1433) 300/300 (parseFail=0 / execFail=0); reports under target/complex-sql-reports/sqlserver-l2|l4-*.
  • Complex SQL dialect slice L2/L4 harness (PostgreSQL): ComplexSqlPostgresSliceL2* (jkit-sql) and ComplexSqlPostgresNativeExecute* (tools-test); 001–300 native execute on jkit_complex (5532) 294/300 (parseFail=0; 6 corpus cases need explicit cast for round(float8,int)); reports under target/complex-sql-reports/postgres-l2|l4-*.
  • SqlGuardedStatement: T-SQL control-flow guard IF <expr> <stmt> [ELSE <stmt>] (e.g. the idempotent drop-table prefix IF OBJECT_ID('t','U') IS NOT NULL DROP TABLE t; in SQL Server init scripts). SqlParser.parseIfGuard captures the raw condition and the inner body and wraps them in a SqlGuardedStatement; its type() delegates to the body, so IF … DROP still reports as DROP to downstream formatting / cross-dialect conversion. An IF at statement start is always parsed as a control-flow guard (in MySQL IF is a function, but that never occurs at statement start). SqlFormatter / SqlAstCloner / SqlSchemaConverter all handle it — sqlserver_init.sql now parses end-to-end via parseAll without the unsupported statement error, its 51 guard statements are recognized as DROP, and cross-dialect conversion reports 0 failures. SqlParserTest.parseSqlServerSql covers this.

jkit-core

  • AESCrypt GCM authenticated encryption: encryptGcm(data, key) / decryptGcm(data, key) use AES/GCM/NoPadding with a fresh random 12-byte IV prepended to the ciphertext (IV || ciphertext); decryption splits it off, so callers never manage IVs. The four-argument encryptGcm(data, key, iv, aad) / decryptGcm(data, key, iv, aad) take an explicit IV and AAD (authenticated but not encrypted — handy for binding a user id or order number). Also generateIv() and the GCM_CIPHER_ALGORITHM / GCM_IV_LENGTH / GCM_TAG_BITS constants. Unlike ECB / CBC, a tampered ciphertext or a wrong key fails loudly instead of decrypting into garbage.

  • EncryptUtils.RSA OAEP padding: encryptOaep(byte[], PublicKey) / decryptOaep(byte[], PrivateKey) plus base64 string helpers, using RSA/ECB/OAEPWithSHA-256AndMGF1Padding; buildKeyPair(int keySize) picks the key size explicitly and DEFAULT_KEY_SIZE exposes the default.

  • ULID: a 26-character Crockford Base32 sortable id in common.idgenerate. next() / nextMonotonic() (adds 1 within the same millisecond, strictly increasing inside one JVM), parse() for timestamp and randomness, toBytes / fromBytes for a compact 16-byte form, isUlid() for validation. Parsing applies the Crockford tolerances (I/L→1, O→0, case-insensitive) and rejects U and other illegal characters. Lexicographic order equals generation order, which suits index keys far better than UUIDv4.

  • UUIDv7: the RFC 9562 time-ordered UUID with the millisecond timestamp in the high 48 bits. next() / nextMonotonic() (12-bit counter; when it is exhausted inside one millisecond the logical clock advances 1ms so ids never go backwards), nextString(), timestamp(UUID) to recover epoch millis, isV7(UUID) to check. Still a well-formed UUID that fits a uuid column.

  • IdGenerator.ulid() / uuidV7() / uuidV7String(): facade entries alongside snowflake.

  • DesensitizeUtils: masking for common data types. Semantic helpers phone / idCard / bankCard / name / email / address / carNo / ip / password, generic mask(value, keepHead, keepTail[, maskChar]) and maskAll, plus desensitize(value, Type) for configuration-driven setups (10 Type constants). null and empty strings pass through without throwing; when keepHead + keepTail covers the whole value only the first character survives instead of returning the plaintext; password always emits six mask characters so the password length stays hidden.

  • HttpClient: instantiable HTTP client (next-plan §4). HttpUtils is a process-wide global facade where every setXxx mutates the global default and threads overwrite each other; HttpClient gives each instance its own config (timeouts / proxy / SSL / CookieJar / engine / interceptors / fakeIp / default Content-Type) with no cross-instance or cross-thread pollution. builder() seeds from the current global defaults and is chainable; shared() returns the global default singleton, equivalent to the HttpUtils static API. Isolation uses a thread-local that propagates the instance config down the send chain, so no global lock is needed. HttpConfig gains copy() so instances get an independent config snapshot.

  • JwtUtils: JSON Web Token issue and verify (the JWT half of next-plan §4 "JWT + encryption defaults"). JDK-only — java.util.Base64 url variant plus javax.crypto HMAC / RSA-SHA256, zero third-party dependencies. createHs256(secret, payload) / createRs256(keyPair, payload) issue tokens; parseHs256(secret, token) / parseRs256(publicKey, token) verify and return the payload (auto-decoded into a Map); withExp(minutes) / withNbf(minutes) / withIssuer / withAudience are claim shorthands, and exp / nbf / iat support both millisecond and second units (expAt(Instant) / expSeconds(long)). Verification is strict: a bad signature, wrong key, expiry, not-yet-valid, or tampered payload all fail; alg=none is explicitly rejected and algorithm confusion is blocked (an HS256 token cannot be opened with an RS256 public key); base64url is unpadded and the payload may be any JSON object (nested and standard registered claims included). JwtUtilsTest (16 cases) covers HS256 / RS256 round-trips, wrong key, tampering, expiry, not-yet-valid, claim retention, alg=none rejection, and base64url edges.

  • JSONPatch: JSON Patch (RFC 6902) apply utility (the Patch half of next-plan §4 "JSON Patch / Schema required alias"; the required alias already landed earlier in 2.0.2). apply(String doc, String patch) parses and serializes for you; apply(Object, List) mutates the parsed Map/List in place and returns it. Supports add / remove / replace / move / copy / test, uses JSON Pointer (RFC 6901, with ~1→/ and ~0→~ decoding), and test compares numbers by value (1 equals 1.0). A malformed path, out-of-range index, remove/replace on a missing member, or a failed test throws JSONPatchException (a subclass of JSONException). JSONPatchTest (32 cases) covers every operation, array insert and the - end token, root replacement, pointer escaping, numeric equality, and the various illegal-input rejections.

  • Retryer / CircuitBreaker: generic (HTTP-agnostic) resilience primitives (next-plan §4 "韧性抽包"). Retryer.retry(Callable, RetryConfig) retries a failing task a limited number of times with exponential backoff + jitter, retries all non-Error exceptions by default, and retryOn(Predicate) / retryOn(Class) narrow the scope; CircuitBreaker is a CLOSED→OPEN→HALF_OPEN state machine that opens after consecutive failures, probes after a cooldown, and closes on success, with atomic internal state safe for concurrent use. Both are pure JDK, zero-dependency. RetryerTest (8 cases) + CircuitBreakerTest (7 cases) cover first-try success, success-after-retry, exhaustion rethrowing the last exception, predicate filtering, no-retry-on-Error, backoff cap, open threshold / immediate OPEN rejection / half-open after cooldown / half-open recovery and re-open / success reset.

  • com.alianga.jkit.html: lightweight HTML parsing and CSS selection (next-plan §4 "轻量 HTML", replacing the removed Jsoup), zero-dependency. Html.parse yields a Document; the DOM offers Element / TextNode / Comment / Elements, with select / selectFirst / text / attr / innerHtml / outerHtml / children on elements; the selector supports type, #id, .class (including .a.b), attribute selectors [attr] plus = / ^= / $= / *= / ~= / |=, descendant and child combinators, comma grouping, and :first-child / :last-child / :only-child / :root / :empty / :not() / :nth-child(an+b); Html.escape / Html.unescape handle entities. The parser tolerates malformed HTML (implicit closing of unclosed tags, void elements never pushed, truncated forms like <div/ terminating safely, script/style treated as raw text). HtmlTest (14 cases) covers structure, every selector kind, pseudo-classes, void-element serialization, text extraction, tolerance, and entity escaping.

  • com.alianga.jkit.html fidelity pass (differentially validated against Jsoup 1.18.1: 86 of 93 "HTML + selector" cases produce identical results): parser — implicit html/head/body skeleton, HTML5-scoped auto-closing of optional end tags (p, li, dt+dd, td+th+tr, option, rt+rp, thead+tbody+tfoot, including a p buried under inline elements), implicit tbody inside table, unquoted attribute values now accept / and & (href=/x?a=1&b=2 is no longer truncated), and a new DataNode so script/style content serializes unescaped; selectors — adjacent + and general ~ sibling combinators, :nth-last-child / :nth-of-type / :nth-last-of-type / :first-of-type / :last-of-type / :only-of-type / :contains / :containsOwn / :matches / :matchesOwn, attribute [attr!=v], case-insensitive pseudo-class names, and illegal selectors (h1 @ p, p ~ + p, p:not()) now throw SelectorException immediately — previously an unknown character caused an infinite loop; text — text() now matches Jsoup (space between block elements, whitespace collapsing, trimming including &nbsp;), plus new ownText() / nextElementSibling() / previousElementSibling() / elementSiblingIndex(), and nodeText() returns the un-normalised original; entities — new Entities table with ~200 frequently used named entities (&copy;, &mdash;, &hellip;, …). HtmlTest grew to 32 cases, including a regression case asserting that illegal selectors throw instead of hanging.

  • com.alianga.jkit.html performance pass (benchmarked on the same machine against Jsoup 1.18.1: 3 warmup rounds, 7 timed rounds, median taken, -Xms1g -Xmx2g; Jsoup is used for benchmarking only and is not a project dependency). Memory — Element attributes moved from a LinkedHashMap to parallel String[] arrays (keys / values / count), child and attribute containers are lazily allocated, and a new Names intern table shares tag and attribute name strings: retained heap for 200 large (300 KB) DOMs dropped from 506.9 MB to 268.0 MB (Jsoup 241.5 MB), narrowing the gap from +110% to +11%. Parsing — head / body membership in the open-element stack is now a cached index instead of an O(depth) linear search, start-tag attributes are written into a reusable buffer instead of allocating a Map per tag, Character.isLetter / isWhitespace / isLetterOrDigit replaced with local ASCII checks, and trim().isEmpty() replaced by a hand-written scan: parsing a 300 KB page went from 2.161 ms to 1.775 ms, i.e. from 12% slower to 3%–28% faster. Selectors — traversal changed from "collect allElements then dedupe via LinkedHashSet" to direct recursion (no intermediate list, no iterator), Compound.matches iterates by index, attribute selectors halve their scan count (one lookup instead of hasAttr + attr), ~= no longer uses a regex split, tag names are normalised at parse time so equalsIgnoreCase degrades to equals, and parsed selectors are cached by access order with a cap of 256 (Jsoup caches its queries too): on a medium (30 KB) page selectors went from up to 5x slower to 1.6x–4.1x faster, and on a large page they are roughly on par (0.91x–1.15x). HtmlTest still passes 32/32 and the Jsoup differential still reports 112 identical / 7 known differences — identical to the pre-optimization run, so no behaviour regressed.

  • com.alianga.jkit.html selector index: Document now carries a lazy id / class / tag index (key prefixes t: / #: / .:, built on first query and reused afterwards). doc.select(...) / doc.selectFirst(...) no longer walk the whole tree every time — they fetch candidates and verify them. Key priority is id > class > tag name: div.foo keys on .foo, a[href^=/p/] keys on a (tag names are index keys too, so attribute queries benefit as well), and only pure attribute / pseudo-class queries ([data-x], :empty) fall back to a full traversal. When the query root is a child element (article.selectFirst("a.post-title")) the traversal still runs, because candidates are document-wide and testing each one for subtree containment costs more than walking a small subtree — so in extraction code prefer hoisting queries to document level. The index is only this cheap because the DOM is immutable once parsed (no setters, no way to add or remove children): it is built once and can never go stale, so it needs no invalidation logic at all. Measured (synthetic corpus, order-balanced timing): selectors go from 1.3x–2.2x to 4.6x–80x on medium pages and 8.6x–749x on large ones, with end-to-end at 1.89x; on the real alianga.com site the index parse is 1.74x, parse + extract-all-fields 1.73x, and the four extraction selectors run 4.5x–37.1x. Retained heap is still 202.6 MB for parse-only (a DOM that is never queried never builds the index — identical to before), rising to 209.1 MB vs Jsoup's 181.0 MB once a query runs; the index costs roughly 3.5% of one DOM. HtmlTest grew from 34 to 39 cases with indexAndTraversalReturnTheSameElements (runs the same queries at document level via the index and at element level via traversal, asserting assertSame on every element), indexKeepsDocumentOrder, repeatedIdAndMultiClassAreIndexedCompletely, missKeyYieldsEmptyInsteadOfCrash and indexIsBuiltOnceAndReused; the Jsoup differential still reports 112 identical / 7 known differences and all 5 fields of all 10 real-site posts still match.

  • jkit-sql-auto: configuration binding gaps closed. foreign-keys / auto-increment / postgres-identity-style were previously settable only through the Java chain API — neither jkit.sql.auto.* config nor the Spring binding could reach them (the OpenGauss / DuckDB workarounds recommended in the docs were effectively unavailable to Spring users); both entry points support them now. The starter's additional-spring-configuration-metadata.json lists all 24 configuration keys, so jkit.sql.auto.* gets full IDE completion and descriptions.

  • jkit-sql-auto: concurrency protection and audit for multi-instance deployments. lock: true (default) takes a metadata lock on the working connection before executing (MySQL GET_LOCK / PG pg_advisory_lock, skipped automatically on other dialects) so instances starting at the same time serialise instead of all reading "table missing" and racing to CREATE; history: true records every applied DDL into the jkit_schema_history table (created automatically; rows carry timestamp / host / mode / statement; an empty plan adds no rows); export: path writes the planned DDL to a file (UTF-8, one statement per line) — combined with dry-run it is a plain schema generator.

  • jkit-sql-auto: quote-identifiers upgraded to column-level quoting. Real-entity multi-database testing (15 MyBatis-Plus entities from an icell project, 209 columns, 10 local databases) revealed that column names like level / mode collide with reserved words on Oracle and GBase 8a, failing CREATE TABLE outright; SqlSchemaConvertOptions gains quoteIdentifiers, and CREATE / ADD / ALTER / DROP now quote column names through the dialect, while table names are case-folded per the connection metadata so CREATE and DROP / ALTER agree. The history table's own mode column collided with the Oracle reserved word and became sync_mode; its existence probe gained case variants and tolerance for old Oracle drivers without Connection#getSchema. Verified: 15 entities / 209 columns build and stay idempotent on MySQL / PostgreSQL / Oracle 10g+19c / SQL Server / Dameng / GBase 8a / H2 / SQLite / DuckDB.

Changed ​

  • jkit-core: EncryptUtils.RSA.buildKeyPair() now generates 2048-bit keys by default (was 1024). 1024-bit RSA no longer meets current baselines (NIST and PCI-DSS both require ≥2048); call buildKeyPair(1024) when the old length is truly needed, and anything below 512 bits throws IllegalArgumentException.

  • jkit-core: DESCrypt and EncryptUtils.DES are @Deprecated (56 effective bits are brute-forceable, and neither ECB nor CBC detects tampering). Both still work for reading legacy ciphertext; use AESCrypt.encryptGcm for anything new.

  • jkit-sql-auto: the Spring Boot starter now runs by default with phase: eager — schema sync happens during context refresh (after the DataSource is ready, before the web port opens), so a fail-fast error aborts the application before it accepts traffic (previously the work hung off ApplicationReadyEvent with the port already bound); set phase: ready to restore the 2.0.1 behaviour. Also, with no DataSource bean and no jkit.sql.auto.url configured, the starter now skips with a warn log instead of failing application startup.

  • jkit-notify: Markdown rendering now lives on Markdown (toHtml / toDocument / wrapDocument). The 2.0.1 NotifyUtils.markdownToHtml(md) / markdownToDocument(md, boolean) / wrapHtmlDocument(fragment, boolean) methods remain and are @Deprecated. toDocument / wrapDocument generate the stylesheet from the active theme instead of a hard-coded CSS block; the default look shifts slightly (16px body, 1.75 line height, richer heading and table styling) and code highlighting is on by default.

  • jkit-notify: fixed the macOS-window code blocks (blue / vue / ayer) where the three top dots overlapped the first code line. Both the stylesheet and the inline style now reserve 36px on top (previously only padding-top:34px, which was wiped out by the inline style and by the shorthand padding in the mobile media query).

  • jkit-notify: mobile media-query rules now carry !important — in inline mode a tag's style attribute outranks the stylesheet, so without it the whole mobile adaptation was dead. The mobile block also shrinks heading sizes, tightens table cell padding, caps image width and enables inertial table scrolling; it no longer overrides the code block's top padding with a shorthand padding.

  • jkit-notify: quotes with the decorative quote mark (purple / lapis / orangeheart) now place the mark inside a 2.6em left gutter instead of on top of the text, and inline-mode quote padding matches the stylesheet (previously the bar and filled variants produced identical output).

  • jkit-notify: the column layout moved onto the shell's own <div class="jkit-md"> container (it used to hang off body). Gmail, QQ Mail and similar clients strip <html>/<head>/<body>, so max-width / padding written on body vanished with the tag and the text filled the whole reading pane. Width, padding, font, colour and line height are now inlined on that div.

  • jkit-notify: fixed the complete HTML document pinning the column to the left in a browser — body no longer carries max-width (the shell inlines margin:0 on body so the background fills the viewport; the width cap stayed, the centering margin was overridden). Column width and margin:0 auto hang only off div.jkit-md; the mobile media query also pads that container alone.

  • jkit-notify: default column width 720 → 820px; desktop padding is now 24px 16px and mobile 12px 8px (almost no horizontal gutter). The container width comes from the theme's own maxWidth token instead of being hard-coded in a media query.

  • jkit-notify: images are centred and pick up the theme corner radius (display:block;margin:1.4em auto), sharing the same width as paragraphs, code blocks and tables — narrowing images alone made them misalign with the text. Desktop code blocks get 18px horizontal padding and a 1.7 line height.

  • jkit-notify: raw HTML <img> in the Markdown (common in WeChat articles as <img src="..." width="100%" />) is sanitised and passed through instead of being escaped to &lt;img&gt;. Only src/alt/title/width/height/class/loading are kept; event handlers such as onerror are dropped and a javascript: src becomes #. Other HTML tags are still escaped.

  • jkit-sql: the row-level inject implementation is renamed from SqlTenantRewriter to SqlInjectRewriter (2.0.2 has not shipped, so the old name is not kept). Tenant is one scenario; class and method names no longer say tenant.

  • jkit-sql: complex-SQL regression gates tightened — L1 parse and L3 convert-then-parse moved from "≥95% / ≥90%" to 100%; L3 grew from 5 direction pairs to the full 4×3 matrix (3600 conversions, adding PostgreSQL as a source); L2 gained a hard "no loss of significant parentheses" assertion. The 8 dialect slice L2 tests now extend AbstractComplexSqlSliceL2Test and only declare dialect and id range (about 1100 fewer duplicated lines).

  • jkit-sql: SqlIdentifier now tracks quoting per name part (markQuotedPart / isPartQuoted / quotedParts). Previously a single quoted flag covered the whole identifier, so c."LEVEL" round-tripped to "c"."LEVEL" — the quotes leaked onto the table alias, and Oracle rejects it with ORA-00904 because "c" does not match alias C (15 of 900 corpus cases failed on real databases). SqlParser marks per part, SqlAstCloner copies the bitmap, SqlFormatter emits per part; identifiers built by rewriters have no per-part info and fall back to the whole-identifier flag, so behaviour is unchanged there.

  • jkit-sql: L2 structure comparison grew from 6 statement-level fields to a deep walk — ComplexSqlReports.deepStructureDrift recursively compares select-item count / WHERE / GROUP BY / HAVING / ORDER BY / FROM / DISTINCT / UNION / LIMIT. A dropped select item or a vanished WHERE clause used to go unnoticed.

  • jkit-sql: SQL.bind / bindNamed string entry points no longer clone the tree just produced by parse; visitor dispatch puts SELECT/expr types first; bind values allocate less (index over Iterator, fast-path strings without quotes, interned small-int text). The AST entry points still clone-then-mutate.

  • jkit-sql-auto: SqlAutoDialects.fromUrl / driverForUrl now delegate to JdbcUrlUtils (more URL prefixes: Gauss, Kingbase, Hive, ClickHouse, Trino, …).

Fixed ​

  • jkit-core: end-tag lookup for script / style / textarea in com.alianga.jkit.html was O(tags × document length) — every lookup ran toLowerCase() over the whole document before indexOf. A synthetic page with one script hid it, but a real 70 KB index page with 24 script tags parsed in 3.929 ms, 14x slower than Jsoup. It now scans character by character and compares with regionMatches(true, …), allocating nothing: the same page drops from 3.929 ms to 0.179 ms (21x faster), and the synthetic large page parse drops from 1.775 ms to 1.060 ms. End-tag casing, spaced forms like </script >, and < inside content are covered by the rawTextCloseTagIsCaseInsensitive regression.
  • jkit-core: pre context did not propagate whitespace preservation in com.alianga.jkit.html. Newlines inside pre > code were collapsed to spaces (text() on <pre><code>int a = 1;\n int b = 2;</code></pre> returned int a = 1; int b = 2;), corrupting code-block extraction. It now matches Jsoup: walk up through parents, preserve raw whitespace once a pre / textarea is hit, and stop preserving when a non-inline element appears in between — so pre > code > span preserves while pre > div > code does not. Covered by the preContextKeepsNewlines regression.
  • jkit-core: the JSON Schema required keyword was parsed but never enforced — a payload missing a field declared the standard way still passed validateSuccess, a silent failure. JSONNode#validateSchemaObject now checks that every field listed in required exists (standard semantics: presence is enough, and a null value counts as present; type still decides type mismatches). required works without properties and applies to nested objects. The library's own must keeps its meaning (field present and not null), and when both appear both must hold.
  • jkit-sql: round-tripping no longer drops expression parentheses. (a - b) / c used to come back as a - b / c, -(a + b) as -a + b, and ROUND((SELECT …), 2) as ROUND(SELECT …, 2) — the evaluation order changed, and the last one is not even valid SQL. The parser now keeps the source parenthesized flag, and a scalar subquery passed as a function argument keeps its parentheses. 172 of the 1200 complex-SQL corpus cases were affected; L2 text fidelity rose from 45.75% to 91.83%, and a new gate rejects any loss of significant parentheses (collapsing a redundant (col) does not count).
  • jkit-sql: classic Oracle ROWNUM pagination of a UNION / INTERSECT / EXCEPT / MINUS that has an ORDER BY now wraps as SELECT * FROM (set-op) ORDER BY … before the ROWNUM layers, so Oracle does not raise ORA-00904 when ordering a set-op by column aliases inside a subquery. The outer select of a double wrap projects the original columns only and no longer returns the helper RN column (a source SELECT * still includes RN).
  • jkit-sql: SqlNode.toString() now uses MySQL identifier quotes (backticks) by default, matching SQL.toSqlString. addComment("text") and compact -- line comments are emitted as block comments so they cannot swallow the following statement. Bare addHint text is wrapped as a slash-star-plus hint. Use SQL.toSqlString(stmt, dialect) for another dialect.
  • jkit-sql-auto: SqlAutoInspector now resolves schema when checking whether a table exists. If unset, it uses Connection.getSchema(); with no connection (dry-run) or an unsupported driver, it parses the JDBC URL. PostgreSQL / Gauss default to public, SQL Server to dbo, Oracle / Dameng fall back to the username. This avoids treating a same-named table in another schema as present, or emitting ALTER for a table missing from the current schema.
  • jkit-curl-codegen: curl parse warnings (e.g. "unsupported option --digest, skipped") were dropped on the generation path; they are now merged at the front of GeneratedCode.notes(), ahead of the generator's own notes.
  • jkit-curl-codegen: generators no longer silently drop content. py-requests now emits a real files= dict for multipart and passes it to the request (file uploads previously vanished); py-httpx gained multipart / file bodies; js-fetch / js-axios emit a readable fs example with the file path for -T bodies instead of a bare undefined; go-nethttp / csharp-httpclient generate real multipart code (mime/multipart / MultipartFormDataContent) and file bodies (os.Open / File.ReadAllBytes); php-curl emits CURLFile for multipart. Proxy handling aligned: py-requests no longer hardcodes http:// (keeps scheme and credentials); java-okhttp / kotlin-okhttp emit proxyAuthenticator; go-nethttp uses http.Transport; csharp-httpclient uses WebProxy; java-apache emits setProxy; js-axios emits a proxy block; php-curl emits CURLOPT_PROXY; js-fetch / js-native explain in notes when a per-request proxy cannot be expressed.
  • jkit-curl-codegen: the har generator's decode no longer turns a literal + in URLs/forms into a space (+ is preserved; %20 / %2B still decode correctly); the creator version is no longer hardcoded 2.0.1 — it reads Implementation-Version from the jar and falls back to the current version in development. CodeQuote now escapes U+2028/U+2029 and remaining control characters for JS/Python/Go/C#/Ruby/R (previously able to emit illegal JS); Rust/Swift use \u{XXXX}, Lua uses three-digit decimal \ddd.
  • jkit-core: curl parsing now joins multiple -b / --cookie flags with ; like curl does (previously each overwrote the last); -E is skipped consistently with --cert (reported as ignored rather than unsupported).
  • jkit-core: URLs in HttpUtils debug logs and "total timeout exceeded" exceptions keep their full structure but mask sensitive fields — query values of access_token / token / sign / signature / secret / key and path tokens such as Telegram /bot<token>/ and ServerChan /<SendKey>.send; new public helper HttpIo.maskUrl. DingTalk/Telegram/ServerChan/Aliyun-SMS credentials no longer end up in logs or exception messages.
  • jkit-notify: four SMTP fixes — recipients/sender/Reply-To no longer accept CR/LF (stripped at config level, rejected at channel level), closing MIME-header/SMTP-command injection; the DATA phase now actually performs RFC 5321 dot-stuffing (the dotStuff() helper existed but was never wired in); AUTH PLAIN is preferred when advertised by EHLO (previously only AUTH LOGIN); a single rejected RCPT no longer aborts the whole email — only all-rejected fails, and partial rejections are listed in the result response.
  • jkit-notify: new NotificationChannel.validate(ChannelConfig) (default no-op). NotificationManager invokes it before any network send, restoring the "programming errors never surface after a partial send" contract of sendAll / sendFailover — previously a missing webhook/token surfaced only after an earlier channel had already been sent. SMS and ntfy channels, whose URL building depends on the message, validate via a probe message instead.
  • jkit-notify: Message gains copy(); SMS channels now stamp the current receiver on a copy per number instead of mutating the caller's message, so no smsReceiver leaks back and one message can fan out to several SMS channels concurrently.
  • jkit-notify: SendResult aggregation now treats local suppression (SUPPRESSED) as the least severe category, so it no longer outranks throttling/config errors as the aggregate failure type.
  • jkit-notify: Attachment.contentBytes() reads file-backed attachments in chunks instead of allocating the declared length upfront (large files OOMed); attachments over 2GB fail fast with a hint to use openStream().
  • jkit-notify: the channel registry (register/unregister/get/list) is now synchronized, so concurrent access can neither lose channels nor throw ConcurrentModificationException; NotifyPolicy dedup reservation and rate-limit counting moved into beforeSend as atomic operations (afterAttempt is kept for compatibility and no longer double-counts), so concurrent sends of the same message cannot both pass dedup and never exceed the rate limit; the default async pool is now 8 threads with a bounded queue of 1000 and CallerRunsPolicy backpressure instead of an unbounded queue.
  • jkit-notify: SPI loading tolerates broken providers one by one — a damaged extension channel (missing dependency / constructor failure) is skipped with a warning instead of crashing the whole module with ExceptionInInitializerError.
  • jkit-notify-extra: Aliyun SMS RegionId now follows CFG_REGION (configurable region) instead of being hardcoded to cn-hangzhou.
  • jkit-core: the expression evaluator com.alianga.jkit.expression fixes two correctness issues. && / || previously did no short-circuit evaluation — the right operand was eager-evaluated before the operator dispatch, so expressions that should short-circuit such as true || (1/0) or false && (1/0) threw a division-by-zero exception, and any side effect in the right operand could not be skipped. ExprEvaluator now returns as soon as the left operand decides the result, without evaluating the right operand, matching Java semantics. > / < / >= / <= previously hard-cast operands to Number, so comparing strings threw; they now compare numbers by value and other Comparables by natural order (e.g. string lexicographic order), with a clear error for incomparable types. New ExpressionRegressionTest (23 cases) locks in short-circuit, string relational comparison, operator precedence, type coercion, built-in functions and ternary — this package previously had zero tests.
  • jkit-core: com.alianga.jkit.beans fixes two correctness issues. BeanUtils.copyProperties(srcMap, tgtMap, excludeFields) with an exclude list wrote the source values back into the source Map instead of the target Map, so the target Map was left completely unupdated (and the source was rewritten in place); it now writes into the target Map, matching the putAll branch used when no exclude list is given. ObjectUtils.set(collection, "[n]", value) previously threw TypeNotMatchExecption unconditionally even after a successful positional assignment, making collection index assignment unusable; the throw now fires only when the key is not an [n] form. New BeansTest (17 cases) covers the four copy directions, merge, collection index assignment, multi-level paths, non-empty fields and isEmpty — this package previously had zero tests.
  • jkit-core: MultiKeyHashMap read and write sides previously disagreed with each other. The class documents that lookups ignore key case, but only put registered the internal keyMap; every other entry point lagged behind. putAll delegated to the superclass, so batch-written entries could never be read case-insensitively (after putAll({"UserName":"tom"}), get("username") returned null, while per-entry put returned the value). remove matched the key exactly, so remove("username") after put("UserName", …) failed silently — size() unchanged and get("username") still returning the value. Neither remove nor clear cleaned keyMap, so deleted keys were retained and could not be collected. containsKey was likewise exact-only and contradicted get. Now putAll routes each entry through put, remove deletes case-insensitively and invalidates the registration, clear also empties keyMap, and containsKey matches get.
  • jkit-core: MultiKeyHashMap case-insensitive matching now uses toLowerCase(Locale.ROOT). The no-arg toLowerCase() depends on the runtime locale: under Turkish (tr-TR) "ID" becomes "ıd" (dotless i), so the write side and the read side normalise differently and case-insensitive lookup breaks entirely — after put("ID", …), get("id") returned null and containsKey("id") was false. The upstream wast project has 14 similar locale-less conversions (mostly HTTP header and protocol name handling), which are out of scope here.
  • jkit-core: Maps.getNumber (and the derived getInteger / getLong / getDouble / getFloat / getShort / getByte) no longer treats a half-parsed string as a number. NumberFormat#parse(String) only parses a prefix, so "123abc" returned 123, "2024-01" returned 2024 and "1.2.3" returned 1.2 — malformed data was silently converted into a valid number and passed downstream, identically on JDK 8 / 11 / 17 / 21 / 25, leaving callers that assert on numbers no way to notice. The value is now trimmed and parsed with ParsePosition, requiring the whole string to be consumed; anything not fully parsed returns null (falling back to the default value in the defaultValue overloads). Side effect: " 42 " now yields 42 instead of null. This is a behaviour change.
  • jkit-core: com.alianga.jkit.collection gets its first test coverage (previously zero tests). New MultiKeyHashMapTest (12 cases), MapsNumberParseTest (9 cases) and CollectionsTest (13 cases, covering emptiness, getFirst / getLast, removal by object and by predicate, find and convert, the Map / array / Iterator / Enumeration / primitive-array branches of size, plus contains and findValueOfType). Reverting the fixes makes 9 of them fail, all on the fixed points above. Also corrected the Collections.size(Object) javadoc — null actually returns 0 rather than throwing IllegalArgumentException as documented — and documented the semantic gap in CountMap between get and the inherited containsKey / getOrDefault: use containsKey to ask whether a key was ever counted.
  • jkit-core: retained heap of com.alianga.jkit.html now beats Jsoup 1.18.1 (150 large-page DOMs: 203.5 MB → 153.3 MB, from 13% higher to 15% lower). A parse-time string-interning cache (StringCache, an open-addressed table with bounded capacity and load factor, skipping strings longer than 128 chars) merges repeated text-node content and attribute values into single instances — class values and templated list text repeat heavily on real pages, and the module used to allocate a fresh instance per node. Strings plus their byte arrays accounted for 324 KB/DOM of excess, which was the entire memory deficit (Jsoup 1.18 ships the same kind of cache). A per-class heap histogram showed jkit already ahead on collections and attribute storage; strings were the only losing column. Parse overhead is ~5% (~0.06 ms on the large page); selector and end-to-end leads are unchanged, and the differential stays at 112 identical / 7 known differences.
  • jkit-core: the HTML selector engine (Selector) is tightened against malformed input, fixing crashes. An empty selector used to throw IndexOutOfBoundsException, broken groups like ",p" / "p,,p" silently matched the entire document (including the #document root) because an empty compound matched every element, and a trailing combinator in "p >" was ignored — an empty selector, a group missing an operand, or a combinator missing its right operand now all throw SelectorException. Attribute selectors are tightened too: img[src (unterminated [) used to throw StringIndexOutOfBoundsException, and [src^png] / [src!] (operator without =) silently degraded to an existence match; both now throw. :not() gets its full semantics: selector lists (:not(a,p) — arguments after the first were silently dropped) and complex selectors with combinators (:not(div p)). An unterminated pseudo-class argument (p:nth-child(23) was truncated to nth-child(2) and returned a wrong result; it now throws. Unsupported pseudo-classes (table:hover) are rejected at parse time instead of silently returning an empty result when the document has no candidate elements, and :nth-child(abc) throws SelectorException rather than NumberFormatException. Tag-name normalisation uses Locale.ROOT.
  • jkit-core: tree operations in com.alianga.jkit.html were converted from recursion to explicit-stack iteration, hardening against maliciously deep input. The parse loop was already iterative, but text() / outerHtml() / nodeText(), selector traversal and the Document index build were recursive — a 20000-deep <div> document parsed fine and then blew the stack on any read API. All of these traversals are now iterative, with identical output (document order, block spacing, index order).
  • jkit-core: three parsing/serialization fixes in com.alianga.jkit.html. <!--> and <!---> are complete empty comments per HTML5 (abrupt closing) and no longer swallow the rest of the document into a comment; numeric entities beyond the BMP such as &#128512; were truncated by a (char) cast into a U+F600 mojibake (now a proper surrogate pair via Character.toChars); and attributes on the <html lang="en"> tag were silently dropped (head / body kept theirs).
  • jkit-core: two performance/consistency items for selectors and Elements. Group-selector deduplication ("p, .c") switched from a linear contains over the result list (O(n²)) to a HashSet marker; Elements.select now actually deduplicates as its javadoc claims — with nested divs, doc.select("div").select("p") used to return the same p twice, and now matches Jsoup with a single occurrence.
  • jkit-core: the previous expression-evaluator fix only landed on the general evaluation path and missed the compressed fast path — a parsed expression behaves differently from its 3rd evaluation on (ExprParser compiles the tree into Impl classes after the 2nd evaluation): flag && (1/0) short-circuited to false for the first two evaluations and then threw a division-by-zero exception, and comparing strings with "a" < "b" threw a raw ClassCastException. LogicalAndImpl / LogicalOrImpl now short-circuit and wrap exceptions exactly like the general path, and the Gt / Lt / GE / LE implementations delegate to compareRelational. ExpressionRegressionTest gains cases that evaluate the same parsed expression 5+ times, closing the regression escape (the previous regression suite evaluated each expression once, bypassing compression entirely).
  • jkit-core: the test operation of JSONPatch compared numbers via doubleValue(), so integers beyond 2^53 were truncated into false equality — 9007199254740993 equals 9007199254740992, and two 30-digit BigIntegers differing by 1 compare equal (toBigDecimal also truncated BigInteger through longValue()); test is the optimistic-concurrency primitive, so false equality lets a patch apply with its precondition unmet. Comparison now goes through BigDecimal exactly, keeping 1 == 1.0. Array-index parsing also follows RFC 6901 strictly: /a/01 (leading zero) and /a/+1, previously accepted by Integer.parseInt, now throw JSONPatchException.
  • jkit-core: CircuitBreaker had two concurrency defects. State transitions used a stale snapshot taken before the task ran and applied unconditional set() calls — with two concurrent probes in HALF_OPEN, T2 failing to OPEN was overwritten back to CLOSED by T1's success (and the failure counter reset), contradicting the "safe under multithreading" class doc; and HALF_OPEN had no probe throttling, so the whole traffic burst entered the moment the cooldown expired instead of "a few probes". Transitions are now CAS loops (openedAt is written before the CAS so a racing half-open transition cannot read a stale timestamp), HALF_OPEN admits a single probe at a time via a permit, further probes keep being admitted while the success threshold is not yet met, and Errors thrown by the task propagate with their original type, matching Retryer.
  • jkit-core: HttpClient's instance scope was "cleared after execute" instead of "restored" — when an interceptor (say, token refresh) executed a request on another client instance, the inner call cleared the ThreadLocal and the outer instance's remaining pipeline (proxy / CookieJar / timeouts / engine) silently fell back to the global configuration with no error. The previous scope is now saved and restored in a finally block; the old "cleared after execute" test enshrined the wrong semantics and has been rewritten, with a nested-call regression added.
  • jkit-core: the MultiKeyHashMap read/write consistency fix was only half done. remove cleared the lower-case alias unconditionally — after put("A",1); put("a",2); remove("A") the surviving "a" entry lost case-insensitive lookup (get("A") returned null, a regression from before the fix); the alias is now rebuilt from the surviving entries after deletion. putIfAbsent / compute / computeIfAbsent / computeIfPresent / merge were not overridden and bypassed put's keyMap registration, silently disabling case-insensitive lookup for those entry points; all are covered now. remove(null) used to collapse String.valueOf(null) into "null" and delete an entry keyed by the literal string "null"; null keys now take no part in case-insensitive matching (get(null) / containsKey(null) no longer hit the "null" entry either). The class javadoc documents that removals via the collection views do not maintain keyMap.
  • jkit-core: JwtUtils hardening. Signing and verification now reject an HS256 shared key shorter than 32 bytes (256 bits) with IllegalArgumentException — tokens signed with low-entropy keys can be brute-forced offline (aligning with the documented recommendation). verify gains leewaySeconds overloads so exp / nbf checks can tolerate clock skew (default 0, behaviour unchanged). Segment splitting uses split("\\.", -1), so "h.p.sig.." is rejected instead of being silently accepted as three segments by the default split's trailing-empty trimming. Base64url decode failures of the signature segment are wrapped in JwtException (previously a raw IllegalArgumentException escaped, breaking the @throws JwtException contract).
  • jkit-core: the String-based EncryptUtils.RSA.encryptOaep / decryptOaep no longer swallow exceptions and return an empty string — decryption failure (wrong key, tampered ciphertext, i.e. BadPaddingException) was indistinguishable from empty plaintext, and a caller that skipped the check silently persisted garbage. They now throw (signatures declare throws Exception, matching the sibling byte[] variants), and the log statements with never-interpolated %s placeholders are gone.
  • jkit-core: DesensitizeUtils no longer returns single-character input unchanged — mask emits "first char + mask" when the keep counts cover the whole value, which degenerates to the original string at length 1, leaking a one-character name (name("王")) verbatim; a single character is now masked entirely.
  • jkit-core: three ULID fixes. The monotonic mode used to wrap the 80-bit random part to zero while keeping the timestamp, producing an ID that sorts before its predecessor and violating the documented strict single-JVM monotonicity (UUIDv7 in the same release carries into the next millisecond; ULID now does too). parse gained the 48-bit timestamp upper-bound check that encode already had (parse("ZZZ…") used to pass and then parse(x).toString() threw, breaking round-trips), with isUlid aligned. toBytes no longer decodes the same string twice.
  • jkit-core: Retryer interruption semantics. An InterruptedException thrown by the task itself was treated as an ordinary failure and retried without restoring the interrupt flag — one wasted attempt and a swallowed cancellation signal; the flag is now restored and retries stop immediately. When the sleep is interrupted, the last business exception that triggered the retry is no longer discarded (attached as suppressed to the ResilienceException).
  • jkit-core: instantiated-HTTP wrap-up. UrlConnectionHttpEngine reads the body copy buffer through the active-aware HttpUtils.config() (it used to read the global HttpConfig.shared() unconditionally, so an instance-level downloadBufferSize had no effect); the lazily cached method field is now volatile; the HttpConfig.copy() javadoc honestly states that executor / endpointPool / sslContext / retryPolicy are shared references (the "deep copy" wording was inaccurate); and the caller-less HttpUtils.currentDefaultMediaType() is removed.

Documentation ​

  • docs/sql.md / docs/en/sql.md: entity table/column comments now name this module's com.alianga.jkit.sql.entity.Comment.
  • docs/sql.md / docs/en/sql.md: classic Oracle ROWNUM wrapping now documents lifting ORDER BY off a set-op before pagination, and that toSqlString must name the target dialect.
  • docs/sql.md / docs/en/sql.md business-scenario section: extra copy-paste samples for bind / inject / expandStar+replaceSelectItems / addComment+dialect quotes / MyBatis #{}/ ${}. The template-placeholder section now shows the common parse+bind pattern. Samples match SqlBusinessScenarioTest.
  • Business scenarios expanded to 18 (2.0.2): dialect-from-URL (JdbcUrlUtils), report DATE_FORMAT rewrite, safe dynamic table-name bind, low-code query sandbox (Wall table allow/deny, required WHERE columns, max tables). Scenario 4 also covers tautology LIKE '%' / XOR.
  • docs/toolkit.md: the crypto section now covers AES-GCM (IV layout / AAD / argument checks / how it differs from ECB), RSA (OAEP vs PKCS#1 v1.5 decision table, key size, per-block limit), and the DES deprecation; new "ID Generation" section compares snowflake / ULID / UUIDv7 and explains the monotonic modes, and a new "Desensitization" section documents what each type keeps and how mask falls back.
  • docs/json.md / docs/en/json.md: the keyword table now distinguishes required (standard array form) from must (this library's boolean form), with a new "Required: required or must?" subsection.
  • docs/csv.md / docs/en/csv.md: fill in the previously empty (0-byte) file, covering CSVUtils lightweight I/O, CSV/CSVTable header-aware read with POJO mapping, CSVObjectWriter streaming object write, streaming readStream, charset and parsing details, and exceptions; removed from srcExclude and added to the zh/en sidebars.
  • docs/expression.md / docs/en/expression.md: fill in the previously empty (0-byte) file, covering arithmetic/logic, Map and JavaBean contexts, positional parameters p0/p1, @-prefixed built-in functions, custom functions and static-method registration, renderTemplate, the CacheableExpression cache notes, and exceptions; added to the sidebars, and toolkit.md gains an "Expression Engine" pointer subsection.
  • docs/html.md / docs/en/html.md: every §9 performance number was remeasured, and timing is now pairwise and order-balanced (A→B→B→A, lower median kept). Running jkit first and Jsoup second every time penalized whichever ran first — JIT compilation, branch prediction and cache warmup all land in the opening rounds — so .post on the large page reported a 0.60x "jkit is slower" that does not exist; alternating order settles it at ~1.3x. After the refresh: parses run 1.92x / 2.06x / 2.02x (small / medium / large), parse + text() 1.67x, selectors 1.55x–2.04x on medium and 1.28x–1.77x on large pages, end-to-end 2.01x, retained heap for 150 large DOMs 202.1 MB vs Jsoup's 179.7 MB (0.89x); on the real alianga.com site the index parse is 0.203 ms vs 0.381 ms (1.88x) and parse + extract-all-fields 0.247 ms vs 0.428 ms (1.74x). The benchmark now lives in the separate tools-test project (com.alianga.test.html.HtmlParseBenchTest / HtmlRealSiteBenchTest), reports are archived under reports/, and §9 gains a "Reproducing the benchmark" subsection with the command and the assertion policy.
  • docs/html.md / docs/en/html.md: §9 refreshed again with post-index numbers, plus a new "Where the index applies" subsection (document-level queries only, how the key is picked, and why results stay identical since candidates are a superset). The optimization list grows from four items to five — the first four only reach 1.3x–2.2x on selectors; it is the lazy index that produces 4.4x–35x.
  • jkit-core: reflection wrapping and JSON serialization of default-package (unnamed package) classes no longer throw NullPointerException. Both ClassStrucWrap.checkClassStructure and the JSON POJO serializer code generator called getPackage().getName() directly, while getPackage() returns null for default-package classes on JDK 8 (and an empty-named package on JDK 9+, which made the generator emit an illegal package ;). Both sites are now null-safe: no package statement is emitted when the package name is unavailable, and the class is no longer misdetected as a JDK built-in module. Default-package POJOs serialize on JDK 8 and take the generated-code fast path on JDK 9+. The same defect exists upstream in wast (ClassStrucWrap.java:614, JSONPojoSerializerCodeGen.java:40: a hard NPE on JDK 8, a silent fallback to reflective serialization after emitting package ; on JDK 11+). Adds ReflectNullPackageTest (2 cases) and DefaultPackageSerializationTest (3 cases).
  • jkit-core: extended HTTP methods such as PATCH no longer throw ProtocolException on the fallback engine under JDK 17+. HttpURLConnection.setRequestMethod only accepts whitelisted methods; the previous fallback rewrote the private method field via setAccessible(true), which throws InaccessibleObjectException from JDK 17 onwards because of module encapsulation. The field is now written through Unsafe first (reflection kept as a second fallback), so PATCH is really sent on JDK 8 / 11 / 17 / 21 / 25. Adds UrlConnectionPatchMethodTest (3 cases driving the engine directly, bypassing engine selection).
  • jkit-core: test sources are now compiled with the JDK 8 toolchain. maven-toolchains-plugin swaps the default toolchain for the declared 9 / 11, so the default testCompile ran with the JDK 11 javac — bytecode was still major 52, but method signatures were resolved against JDK 11 (e.g. the covariant ByteBuffer#flip()), which surfaced as NoSuchMethodError when the suite ran on JDK 8. base-compile and default-testCompile now pin the JDK 8 toolchain in both profiles; mvn clean test passes 870 cases (3 skipped) on all five JDKs.
  • docs/en/sql.md: bring the "Cross-dialect type conversion" section to full parity with the Chinese — Normal Form framing and design-doc link, full Phase 0–1 import code, SqlDataTypeRegistry validation and RegistryValidationTest, Phase 2–3 whole-statement SQL.convert entry with SqlSchemaConvertOptions / generateOracleSequence, the complete function-rewrite list (incl. CONVERT USING charset not mis-mapped, DATEADD / GETDATE / FROM_UNIXTIME, UCASE / LPAD / CEIL / YEAR …), SQL.convertBatch and ALTER handling, the real-DB regression command block, SqlDialectSpec extension, ConversionResult.sqlWithExtras() and SqlSchemaConverterProvider.registerFunctions, and the type-alias / function-extension guidance.
  • docs/html.md / docs/en/html.md: fully refreshed alongside the HTML fixes and the memory flip — §4 adds :not() selector-list and complex-selector examples plus the dedup semantics of Elements.select; §5 notes that numeric entities beyond the BMP produce surrogate pairs; §6 documents <!--> / <!---> empty comments and <html> attribute preservation; §7 removes "combinators inside :not()" from the differences list (now supported); §8 lists the malformed selectors rejected at parse time; §9 numbers are refreshed from the latest tools-test reports (JDK 17), with the retained-heap verdict flipped from "13% higher" to "15% lower" and the optimization list growing from five items to six (the parse-time string-interning cache).
  • docs/resilience.md / docs/en/resilience.md: the HALF_OPEN state now reads "exactly one probe admitted at a time; further probes are admitted while the success threshold is unmet".
  • docs/json.md / docs/en/json.md: the JSONPatch section documents the strict RFC 6901 array-index rules (no leading zeros except 0, no +) and the exact BigDecimal comparison of test (large integers are no longer truncated into false equality by double).
  • docs/toolkit.md / docs/en/toolkit.md: the ULID section documents the 48-bit timestamp upper bound enforced by parse and the round-trip guarantee.
  • docs/http.md / docs/en/http.md: the instance scope is described as "save and restore the previous scope", and nested calls from interceptors keep the outer instance's config.

2.0.1 - 2026-09-13 ​

The repository has been split into multiple modules. This release delivers jkit-sql, jkit-sql-auto (with Spring Boot 2 / 3 starters), jkit-notify, jkit-notify-extra, and jkit-curl-codegen alongside the parent POM. The coordinate com.alianga:jkit still refers to jkit-core.

Added ​

jkit-sql

  • New module com.alianga:jkit-sql: zero-dependency hand-written SQL parser (lexical analysis via char[] + open-address keyword hashing, recursive-descent AST). Entry points: SQL.parse / parseAll / parseExpr / format / toSqlString / tables / stat / parameters / parameterize / exportParameterValues / wall / clone / eval / rewrite. Invalid SQL throws SqlParseException. Usage: see docs/sql.md.
  • Dialects: first-class enum MYSQL (default), POSTGRES, ORACLE (pre-12c ROWNUM pagination), ORACLE12 (OFFSET/FETCH), SQLSERVER, ANSI, H2, DB2, SQLITE, HIVE (aliases maxcompute/odps/argo), CLICKHOUSE, PRESTO (alias trino), DAMENG (dm/dameng/jdbc:dm:). fromName covers GoldenDB / SelectDB / AnalyticDB / MatrixOne / StoneDB / HighGo / UXDB / MogDB / Vastbase / AntDB / IvorySQL / GBase 8a / GBase 8s / Xingyun / Oscar and more, aligning with icell common-model SQL data sources. Capabilities are driven by SqlDialectSpec and can be individually overridden via SqlDialectWrapper (e.g. MySQL ANSI_QUOTES).
  • Pagination: getLimit / getOffset / setLimit / setOffset / setPage / addLimit / SQL.adaptPagination. format / toSqlString adapt to the target dialect (MySQL LIMIT ↔ classic ORACLE ROWNUM ↔ ORACLE12 OFFSET FETCH ↔ SQL Server TOP | OFFSET FETCH). SqlBuilder.limit / offset generate output based on the active dialect instead of always emitting LIMIT.
  • Rewrite chain: SqlRewriteHook + SqlRewrites (addLimit / setPage / andWhere / replaceTable / replaceColumn / adaptPagination / addSelectItem / removeSelectItem, combinable with custom rules) + SQL.rewrite (deep-clone then mutate). Cross-dialect function rewriting covers JOIN ON / MERGE / OVER / column 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 etc.; FULLTEXT upgraded to MANUAL_ACTION_REQUIRED. Function rewriting is extensible via SqlSchemaConverterProvider.registerFunctions (SPI override).
  • Entity DDL: SqlEntities scans @SqlTable / JPA / MyBatis-Plus / MyBatis @Alias (no third-party compile dependencies). Public APIs: columnSql / columnTypeSql / createIndex / orderByForeignKeys / extraSql / sequenceSql / indexName / createTable(..., includeIndexes). Recognizes any simple-name Comment annotation and Hibernate @ColumnDefault; @SqlTable(comment) / @SqlColumn(comment) generate dialect-aware comments; Oracle ≤11g appends SEQUENCE + TRIGGER when IDENTITY is unavailable.
  • Formatting & parse options: SqlFormatOptions.keywordCase (AS_IS / UPPER / LOWER), quoteIdentifiers; SqlParseOptions.pipesAsConcat / keepComments / placeholders() (template placeholders) / statementParsers() (custom statement parsers keyed by leading keyword).
  • Security & extensibility: SQL.wall → SqlWallResult; SqlWallRule chain + SqlWallConfig.rules(...) (built-in selectOnly / denyDdl / no-WHERE writes etc., behavior and violation codes unchanged). SqlAstVisitor type dispatch (non-breaking to SqlVisitorAdapter).
  • Statement AST: EXPLAIN / SET / COMMENT ON / SHOW / ANALYZE|VACUUM|OPTIMIZE / transaction control / COPY / FLUSH / START TRANSACTION / LOAD DATA / LOCK TABLES / PREPARE / BEGIN…END / routines·triggers·events (SqlRoutineParam / SqlControlStatement / SqlDeclareStatement / SqlHandlerStatement) promoted to standalone types; MODEL / MATCH_RECOGNIZE structured; RENAME TABLE independent statement.
  • SqlBuilder: fluent SELECT / INSERT / UPDATE / DELETE, join / leftJoin / rightJoin / fullJoin / crossJoin / union / with / distinct / groupBy / having.
  • Parsing coverage: relative to Druid BVT / JSqlParser inline / file corpora approximately 96.3% / 92.3% / 90.0%. Window functions, CTE, MERGE, PIVOT, flashback, procedure blocks, Hive / ClickHouse / Informix / DB2 edge-case syntax and more are documented in docs/sql.md rather than listed per iteration.

jkit-sql-auto

  • New module com.alianga:jkit-sql-auto: auto-creates / updates tables at startup based on entities. Scans @SqlTable / JPA / MyBatis-Plus, compares against DatabaseMetaData, and executes CREATE TABLE / ALTER TABLE ADD / CREATE INDEX. Modes: none / validate / update (default, append only) / create / create-drop. Configuration prefix jkit.sql.auto.*, data source falls back to spring.datasource.*. Zero third-party runtime dependencies.
  • SqlAuto.drop deletes managed tables in reverse foreign-key order; Spring Boot 2 / 3 auto-configuration modules jkit-sql-auto-spring-boot-2 and jkit-sql-auto-spring-boot-3.
  • Table / column comments executed per dialect (COMMENT / COMMENT ON / sp_addextendedproperty); Oracle ≤11g auto-increment primary keys use SEQUENCE + TRIGGER; Dameng uses IDENTITY on columns.
  • postgresIdentityStyle / foreignKeys / autoIncrement: disable the corresponding DDL for products with limited capabilities such as OpenGauss / GBase 8a / DuckDB.
  • table-prefix (jkit.sql.auto.table-prefix / chain tablePrefix(String)): prepends a prefix (e.g. t_) to all auto-generated table names; applies to create / alter / drop / indexes / sequences / foreign-key target tables.
  • index-prefix-enabled (jkit.sql.auto.index-prefix-enabled, default true): whether auto-derived index names also carry the table prefix; explicit @Index(name=…) on entities is always preserved as-is.

jkit-notify

  • New module com.alianga:jkit-notify: DingTalk (with signing), WeCom, Feishu (with signature), ServerChan, Bark, generic Webhook, SMTP (raw socket: AUTH LOGIN + STARTTLS/SSL + MIME). NotificationChannel SPI + NotificationManager; MessageType (TEXT / MARKDOWN / HTML). Zero third-party dependencies. Usage: see docs/notify.md.
  • Failure classification: SendResult.failureType() / isRetryable(), FailureType is RETRYABLE / THROTTLED / CONFIG_ERROR / PERMANENT / SUPPRESSED; unlisted error codes fall back to the HTTP status. Custom channels can override AbstractHttpChannel.classify.
  • Messages truncated by UTF-8 bytes (no splitting of CJK characters or emoji); DingTalk @-mentions automatically append missing @phone into the body; Message.var / vars replace ${key} / ${a.b}; attachments streamed, split at 10MB / 512KB, MIME auto-detected.
  • SMTP: sslProtocols pins the TLS version, trustAllCerts supports self-signed gateways; MARKDOWN sent as text/html via NotifyUtils.markdownToHtml. Feishu signature written into the JSON request body. NotifyPolicy: quiet hours, 5-minute deduplication, local rate limiting; sendFailover sequential account failover.
  • Optional module com.alianga:jkit-notify-extra: Slack / Telegram / ntfy / Alibaba Cloud / Tencent Cloud / Yunpian / Huawei Cloud SMS. Channel extras live on their implementation classes, not in the core Message.

jkit-core

  • DateUtils.parse(String): auto-detects timestamps, compact numerics, - / / ., Chinese / Korean formats, ISO-8601 (including T/Z/+0800/+08:00).
  • DateUtils.fromEpochNumber(long): converts 10-digit seconds or 13-digit milliseconds to Date.
  • DateUtils.fromTemporal(TemporalAccessor): converts java.time objects to Date.
  • ConvertUtils.toDate additionally supports Instant, OffsetDateTime, ZonedDateTime.
  • HttpUtils.debug / HttpUtils.printCurl: logs the request summary or equivalent curl to stdout before sending (do not enable in production).
  • ConfigLoadOptions.addLocation: appends directories to the default search path (classpath: / file: / bare path / File, ~ expanded to user.home).
  • Print.enableLog: when false, prints to console only without writing to JUL.
  • com.alianga.jkit.log.LocaleFormatter: JUL formatter with a fixed Locale.

jkit-curl-codegen

  • Extracted from jkit as a standalone artifact com.alianga:jkit-curl-codegen, entry point CurlCodegen.generate(id, curl). Covers 34 targets: Java (jkit / JDK 11+ / OkHttp / Apache 5 / HttpURLConnection / Unirest), Kotlin, JavaScript (fetch / axios / request / jQuery / XHR etc.), Python (requests / httpx), Go, C#, PHP, R, Rust, Swift, Ruby, Lua, PowerShell, curl (Windows cmd / PowerShell), wget, plus http / har / httpie interop formats.

Changed ​

  • jkit-sql: Breaking — SQL.andWhere / replaceTable / replaceColumn now use the same clone-then-mutate pattern as addLimit / setPage (return a new AST, leave the original tree untouched); callers must use the return value.
  • jkit-sql: SQL.clone now performs a true AST tree copy (SqlAstCloner / SqlNode.copy); hot paths no longer format→parse; cross-dialect format / adaptPagination / setPage significantly faster. Classic ORACLE offset=0 single-layer ROWNUM wrapping skips cloning.
  • jkit-sql-auto: dryRun(true) on SqlAuto.run(options) / plan(options) no longer opens a JDBC connection / DataSource. The URL is only used to infer the dialect; a full CREATE TABLE plan is produced against a blank database. Overloads that receive a Connection still compare against live tables but do not execute.
  • jkit-core: DateUtils drops SimpleDateFormat. yyyy-MM-dd HH:mm:ss uses char[] + second-level cache; other common patterns use hand-written assembly or DateTimeFormatter. ConvertUtils.toDate(Object) refactored to extract by numeric field. Common string parsing roughly an order of magnitude faster; results aligned with ZmlTools.
  • jkit-core: JUL default format changed to English level names (WARNING / SEVERE), no longer following the JVM default locale.
  • jkit-core: RandomUtils.randomBirth switched to LocalDate (no longer throws when minAge == maxAge); getUUID dash removal no longer uses regex; IdCardUtils random birthday generates by actual days in the month.
  • jkit-core: curl parsing aligned with curl semantics (expands -kLs/-XPOST, --json / --data-urlencode, @file no longer treated as literal body). Public APIs: parseCurl / curlToRequest / curl / curlString / requestToCurl, model ParsedCurlRequest. CurlParser.generate and the http.codegen package removed from jkit core; depend on jkit-curl-codegen instead.
  • jkit-notify: core ChannelConfig removes SMS-specific template / appId / region; replaced by ChannelConfig.extra + AbstractSmsChannel.CFG_*. Core channels registered only via defaults(); SPI loads only extra modules to avoid double registration.
  • jkit-notify-extra: SlackChannel.EXTRA_CHANNEL replaces using Message.EXTRA_GROUP for Slack channels (brief backward-compatible fallback to EXTRA_GROUP).

Fixed ​

  • jkit-core: when the response declares Content-Encoding: gzip but the body is empty, JDK 8 no longer throws EOFException; returns an empty stream (underlying connection is still reclaimed on close).
  • jkit-sql: when a subclass and a MappedSuperclass / parent class both declare a same-named column, only the subclass field is kept, preventing PostgreSQL column specified more than once.
  • jkit-sql: @GeneratedValue(generator="system-uuid") / GenerationType.UUID / non-integer @SqlGenerated no longer produce AUTO_INCREMENT / IDENTITY (PostgreSQL would syntax-error on IDENTITY for a VARCHAR primary key).
  • jkit-sql: auto-generated index / sequence / trigger names are truncated to the dialect identifier length limit (classic Oracle 30 characters; when exceeded, the prefix is kept plus a 4-character hash). Unnamed indexes changed to {table}_{col}_idx; multiple unnamed indexes on the same table no longer collide. Public APIs: SqlDialectSpec.maxIdentifierLength / fitIdentifier.
  • jkit-sql: SQL.format pretty mode now wraps and indents CREATE TABLE columns (toSqlString / compact still single-line).
  • jkit-sql: GROUP_CONCAT without an explicit SEPARATOR no longer loses the separator when rewriting to STRING_AGG / LISTAGG; MySQL MODIFY / CHANGE rewritten to PG/ANSI/H2/PRESTO no longer duplicate the column name.
  • jkit-sql: SUBSTRING / LEFT / RIGHT rewritten per dialect (PG/MySQL/H2 use FROM n FOR m; SQL Server/SQLite/Hive/ClickHouse use comma form; negative start rewritten with LENGTH/LEN); Dameng bare SELECT appends FROM dual.
  • jkit-sql: SqlDialectWrapper no longer delegates derived methods (identQuoteClose / quoteIdent / pipesAreConcat / preferredLimitStyle); subclass overrides of primitives now automatically cascade to derived capabilities.
  • jkit-sql: rewrite fidelity — NATURAL JOIN no longer loses its modifier; RENAME TABLE no longer writes illegal ALTER TABLE; ADD UNIQUE KEY no longer drops UNIQUE; IGNORE / LOW_PRIORITY / HIGH_PRIORITY on INSERT/UPDATE/DELETE are preserved in the AST and rewritten; non-bare-identifier aliases are force-quoted.
  • jkit-sql: tables() now captures CREATE TRIGGER / CREATE TABLE … LIKE / RENAME TO / multi-table OPTIMIZE|ANALYZE; database names, routine names, and CTE names are no longer miscounted. Comment-only / blank-only input is classified as OTHER instead of throwing empty SQL.
  • jkit-sql-auto: on classic Oracle, auto-generated CREATE INDEX names exceeding 30 characters are now truncated to avoid ORA-00972.
  • jkit-curl-codegen: R (httr2) now uses req_perform(); PowerShell 5.1 restricted headers / cookie commas / Chinese encoding fixed; Windows curl split into cmd (shell-curl-windows) and PowerShell (shell-curl-powershell) generators.

Build ​

  • Repository converted to multi-module: parent POM com.alianga:jkit-parent, runtime library in jkit-core (release coordinate remains com.alianga:jkit), other capabilities in separate modules. mvn test at the root builds all modules.
  • git-commit-id-plugin, buildnumber-maven-plugin, maven-source-plugin moved from the publish profile to the default build; package now produces sources jars with build info.
  • The full reactor can be built with JDK 8 launching Maven: other modules compile against JDK 8; jkit-core's META-INF/versions/9, /11 use toolchains targeting JDK 9 / 11; only jkit-sql-auto-spring-boot-3 uses toolchain for JDK 17 (required by Spring Boot 3). Requires ~/.m2/toolchains.xml declaring jdk 8/9/11/17/21. Launch example: JAVA_HOME=$(jdk8) mvn -o clean install.
  • jkit-notify live-use tests require -Djkit.notify.live=true and complete YML credentials; default mvn test never sends live messages even when keys are present.

Documentation ​

  • docs/sql.md / docs/en/sql.md updated to align with code: cross-dialect pagination, SQL.clone, SqlRewrites, SqlEntities public methods, and dialect tables; API coverage is now aligned on both sides. READMEs and module READMEs removed internal development-plan links.
  • docs/sql-auto.md (and its English version) reordered to follow "pick package → write entity → Spring Boot / non-Spring → configuration" flow; module READMEs and quick-start pages synchronized.

2.0.0 ​

jkit's first public release, migrated from ZmlTools: zero third-party dependencies, package renamed to com.alianga.jkit, coordinate com.alianga:jkit:2.0.0.

Capability summary in README.md: CSV / HTTP / JSON / YAML / configuration / expressions etc. all rewritten with pure JDK. Projects migrating from ZmlTools can continue using relocated/zmltools to redirect top.wuyongshi:ZmlTools:2.0.0 to this coordinate (package names still require manual replacement).


How to Record Future Versions ​

  1. Change the <version> in the root pom.xml (jkit-parent) to the new version number; child modules inherit it. Update the README header and dependency examples, and the version printed by Main.java.
  2. Insert the new version above the current latest-version section, with today's date (YYYY-MM-DD). Each version number gets exactly one H2 heading; before release, append new items to that section — do not open a separate ## x.y.z / unreleased.
  3. Document caller-visible changes under Added / Changed / Fixed / Build (use bold module names for grouping in multi-module projects). Do not simply paste git titles, and do not record internal milestones or competitor-coverage rounds (those go in docs/sql.md / docs/next-plan.md).
  4. Add @since x.y.z to javadoc for newly added public APIs.
  5. Leave historical versions in place; do not rewrite old entries as new versions.

Template:

markdown
## x.y.z - YYYY-MM-DD

### Added
- ...

### Changed
- ...

### Fixed
- ...

### Build
- ...

Omit any section heading when there are no changes in that category.

Released under the Apache License 2.0