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-mockmodule (coordinatescom.alianga:jkit-mock:2.0.3, JPMS module namecom.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:
MockRuleparses the four rule forms'name|min-max','name|+step','name|count','name|min-max.dmin-dmax';MockRandomsupplies 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;MockRegexgenerates text from a regex (recursive descent covering groups, alternation, quantifiers, character classes, back-references and lookahead);MockJsis 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.2into a bogusprice.2key). - Field mode:
MockDataGeneratorsupplies 30 built-in fields (personal / commercial / technical) and 5 quick templates (MockTemplate);MockCustomField/MockCustomFieldTypedefine custom fields,MockDataProducerdrives generation, andMockDataFormatterexports to JSON / CSV / SQL INSERT / XML (MockOutputFormat). MockSchema: generates sample data from a JSON Schema, configured throughMockSchemaOptions. Supportstype/properties/required/items(including tuple form) /additionalPropertiesandenum/const/default; supportsallOfmerging,oneOf/anyOfbranch sampling, and$refresolution againstdefinitions/$defs/components.schemaswith amaxDepthguard against recursive-schema infinite descent. Strings are resolved bypattern(handed toMockRegex) >format(email / date-time / uri / uuid, …) > property-name heuristics (nameyields a Chinese name,emailan email address,phonea mobile number,createdAta timestamp), then clamped tominLength/maxLength; numbers honourminimum/maximum/exclusiveMinimum/exclusiveMaximum/multipleOf.MockSchemaOptionsis a builder controlling whether optional properties are emitted, the default array length, whetherdefaultwins, and the maximum recursion depth. Entry points aremock/mockJson/mockMany/samples, and passing a seededMockRandomreproduces the same output.- Business placeholders and an extension point:
MockRandom.extend()mirrors Mock.jsRandom.extend, registering custom placeholders (built-ins may be overridden), withallPlaceholders()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), andsample(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:
MockSqlOptionscoversTypeMode(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 tofake_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)), andMockDataProducer.setSqlOptions(options)exposes the same to field mode. MockValidvalidates templates and derives JSON Schema;MockJs.normalizeQuotes(String)was promoted from package-private to public for callers, and the newMockJs.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
CALLkeyword in disql scripts (e.g.SP_SET_PARA_VALUE(1, 'HJ_BUF_GLOBAL_SIZE', 4000);) previously threw "unsupported statement" at anIDENT (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 theparse → format → parseround-trip. New dialect capabilitySqlDialectSpec.supportsImplicitProcedureCall(), enabled only forDAMENG(Oracle-family bare calls are only valid inside PL/SQL blocks); other dialects are unchanged.SqlSimpleStatement.implicitCall()distinguishes bare calls from explicitCALL. - 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. @SqlColumngainsdefaultValue: declare a column default as a raw SQL fragment (noDEFAULTkeyword) — strings include their own quotes (@SqlColumn(defaultValue = "'N'")), functions/expressions are written as-is ("0","CURRENT_TIMESTAMP"). It takes precedence over Hibernate@ColumnDefaultand 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) withisKeyword(dialect, name).SqlSchemaConvertOptionsgainskeywordQuoteandkeywordQuotedListener: 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)/ configjkit.sql.auto.connect-on-dry-run. WithdryRun(true)it now opens the configured DataSource to read metadata and diff against live tables, soplan.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:
MockDataFormattergainsAppendablevariants offormatTo/writeJson/toCsv/toSql/toXml,MockDataProducergainsformatTo, and field mode now emits row by row instead of materialising a fullList<Map>. A new lazy container,MockRepeat, reuses a singleMockJsinstance and generates while iterating (size()is known without expansion);genArrayreturns it when streaming andcount >= 1000, so 100k objects never pile into anArrayList— peak heap drops from 1.96 GB to roughly 150 MB. TheString-returning APIs are unchanged and now delegate to the streaming path. MockValidgoes streaming: aProgresscallback reports progress for arrays and lazy structures (MockRepeat) element by element and lets the caller cancel mid-way;typeNameclassifies bothMockRepeatandIterableas 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 intoMockDictinstead 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 throughMockDataFormatter.prettyJson(), so both modes indent two spaces per level (template mode used to emit a single compact line).
Fixed
jkit-mock
MockSchemacollapsed to the bound when only one numeric bound was given:minimumalone ormaximumalone degenerated the range to[x, x], so generated values always equalled that bound.minimumalone now derives an upper bound (DEFAULT_NUM_SPAN = 1000);maximumalone derives a lower bound (starting at 0 when the bound is non-negative). The[0, 100]default applies only when both sides are omitted.MockJslost fractional digits in its upper-bound guard: hitting the integer part of the bound zeroed the decimals, so100.00came back as100.0and 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
preferHttp2is no longer overwritten by the global default:HttpUtils.applyDefaultsnull-guards connect and read timeouts withisXxxSet(), butpreferHttp2was unconditionally overwritten withHttpConfig.isHttp2(), so an explicitpreferHttp2(false)was silently dropped and plaintexthttp://requests still sentUpgrade: h2c. Servers without h2c support (e.g. Next.js / Node dev servers) close such connections without a response header, surfacing asHTTP/1.1 header parser received no bytes.HttpRequestnow carries apreferHttp2Setflag (preserved throughcopy(), exposed viaisPreferHttp2Set()), andapplyDefaultsapplies 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 asmsgtype=linkwith title, summary, cover and jump URL, instead of thefeedCardform that dropped the body text. - WeCom
IMAGEaccepts onlyEXTRA_PIC_URLwith a public URL; it is downloaded and converted to base64 + md5 before sending. AddsMessage.imageUrlandNotifyUtils.downloadBytes.
jkit-sql-auto
- With
quote-identifierson, theCREATE INDEXtable 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; thedropentry is fixed the same way.- The PostgreSQL metadata lock was never actually acquired:
pg_advisory_lockreturns void, which PG JDBC maps to an empty string, andBoolean.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
@Primaryor setjkit.sql.auto.url) instead of throwingNoUniqueBeanDefinitionException; an invalidphasevalue logs a WARN. - An unknown
jkit.sql.auto.modevalue (e.g.valdate) silently fell back toUPDATEand modified the schema; it now logs a WARN. - History-table write failures are caught as
Exception | LinkageErrorinstead ofThrowable; the GBase 8a "CREATE INDEX skipped" log is emitted once and only when the plan actually contains indexes.
Build
flatten-maven-pluginwith${revision}: the parent POM version becomes${revision}, sourced solely from therevisionproperty 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-modulejkit.versionrelay property — publishing a release now means editingrevisionin exactly one place. The plugin runs withresolveCiFriendliesOnlyduringprocess-resourcesto emit a.flattened-pom.xmlwith versions resolved, and that is the POM installed / deployed (${revision}inside sub-module dependencies resolves by inheriting the concreterevisionproperty from the published parent); the source POM is otherwise untouched and consumers see no difference..flattened-pom.xmlis git-ignored. Verified with reactor-wide clean package / install, plus an out-of-reactor scratch project resolving the full internal dependency chain ofjkit-sqlandjkit-sql-auto-spring-boot-2offline.Automatic-Module-Nameon every published jar (a first step towards JPMS): the parent POM configuresmaven-jar-pluginto write it, with each sub-module supplying the name through itsjkit.module.nameproperty (the core iscom.alianga.jkit, the rest followcom.alianga.jkit.*— e.g.jkit-mockiscom.alianga.jkit.mock). Nomodule-infois 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;blogcopies 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.MarkdownRenderOptionsplusMarkdown.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'sstyleattribute for Outlook / corporate mailboxes / WeChat pasting, all of which strip<head><style>.Markdownis 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)(orSmtpChannel.markdownImageBaseDir(dir)) every Markdown image that points at a local relative path is converted todata:image/...;base64,...and written intosrc, so the body stands alone. Only local paths are touched —http(s)://////data:/cid:are kept as-is;./,../, absolute paths and thefile:prefix all resolve. Missing files, non-images (MIME from suffix and magic bytes) and files abovemaxInlineImageBytes(default 2MB) keep the originalsrc— no exception is thrown. MarkdownRenderOptionsalso 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), inferSqlDialectviafromUrl, return a short type name viagetDbType, and guess the driver class viadriverForUrl/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 fromcurrentSchema(defaultpublic).com.alianga.jkit.sql.entity.Comment: first-party table/column comment annotation (TYPE+FIELD,value()). Sits alongside Hibernate@Commentand@SqlTable(comment)/@SqlColumn(comment); the scanner still matches the simple nameComment, sojkit-sql-modeland similar modules can prefer this package.- Complex SQL dialect slice L2/L4 harness (Oracle):
ComplexSqlOracleSliceL2*(jkit-sql) andComplexSqlOracleNativeExecute*(tools-test); 001–300 native execute on oracle19c 300/300 (parseFail=0 / execFail=0); reports undertarget/complex-sql-reports/oracle-l2|l4-*. - Complex SQL dialect slice L2/L4 harness (MySQL):
ComplexSqlMysqlSliceL2*(jkit-sql) andComplexSqlMysqlNativeExecute*(tools-test); 001–300 native execute on MySQL3308/test_db300/300 (parseFail=0 / execFail=0); reports undertarget/complex-sql-reports/mysql-l2|l4-*. - Complex SQL dialect slice L2/L4 harness (SQL Server):
ComplexSqlSqlServerSliceL2*(jkit-sql) andComplexSqlSqlServerNativeExecute*(tools-test); 001–300 native execute onjkit_ss_test(1433) 300/300 (parseFail=0 / execFail=0); reports undertarget/complex-sql-reports/sqlserver-l2|l4-*. - Complex SQL dialect slice L2/L4 harness (PostgreSQL):
ComplexSqlPostgresSliceL2*(jkit-sql) andComplexSqlPostgresNativeExecute*(tools-test); 001–300 native execute onjkit_complex(5532) 294/300 (parseFail=0; 6 corpus cases need explicit cast forround(float8,int)); reports undertarget/complex-sql-reports/postgres-l2|l4-*. SqlGuardedStatement: T-SQL control-flow guardIF <expr> <stmt> [ELSE <stmt>](e.g. the idempotent drop-table prefixIF OBJECT_ID('t','U') IS NOT NULL DROP TABLE t;in SQL Server init scripts).SqlParser.parseIfGuardcaptures the raw condition and the inner body and wraps them in aSqlGuardedStatement; itstype()delegates to the body, soIF … DROPstill reports as DROP to downstream formatting / cross-dialect conversion. AnIFat statement start is always parsed as a control-flow guard (in MySQLIFis a function, but that never occurs at statement start).SqlFormatter/SqlAstCloner/SqlSchemaConverterall handle it —sqlserver_init.sqlnow parses end-to-end viaparseAllwithout theunsupported statementerror, its 51 guard statements are recognized as DROP, and cross-dialect conversion reports 0 failures.SqlParserTest.parseSqlServerSqlcovers this.
jkit-core
AESCryptGCM authenticated encryption:encryptGcm(data, key)/decryptGcm(data, key)useAES/GCM/NoPaddingwith a fresh random 12-byte IV prepended to the ciphertext (IV || ciphertext); decryption splits it off, so callers never manage IVs. The four-argumentencryptGcm(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). AlsogenerateIv()and theGCM_CIPHER_ALGORITHM/GCM_IV_LENGTH/GCM_TAG_BITSconstants. Unlike ECB / CBC, a tampered ciphertext or a wrong key fails loudly instead of decrypting into garbage.EncryptUtils.RSAOAEP padding:encryptOaep(byte[], PublicKey)/decryptOaep(byte[], PrivateKey)plus base64 string helpers, usingRSA/ECB/OAEPWithSHA-256AndMGF1Padding;buildKeyPair(int keySize)picks the key size explicitly andDEFAULT_KEY_SIZEexposes the default.ULID: a 26-character Crockford Base32 sortable id incommon.idgenerate.next()/nextMonotonic()(adds 1 within the same millisecond, strictly increasing inside one JVM),parse()for timestamp and randomness,toBytes/fromBytesfor a compact 16-byte form,isUlid()for validation. Parsing applies the Crockford tolerances (I/L→1,O→0, case-insensitive) and rejectsUand 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 auuidcolumn.IdGenerator.ulid()/uuidV7()/uuidV7String(): facade entries alongside snowflake.DesensitizeUtils: masking for common data types. Semantic helpersphone/idCard/bankCard/name/email/address/carNo/ip/password, genericmask(value, keepHead, keepTail[, maskChar])andmaskAll, plusdesensitize(value, Type)for configuration-driven setups (10Typeconstants).nulland empty strings pass through without throwing; whenkeepHead + keepTailcovers the whole value only the first character survives instead of returning the plaintext;passwordalways emits six mask characters so the password length stays hidden.HttpClient: instantiable HTTP client (next-plan §4).HttpUtilsis a process-wide global facade where everysetXxxmutates the global default and threads overwrite each other;HttpClientgives 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 theHttpUtilsstatic API. Isolation uses a thread-local that propagates the instance config down the send chain, so no global lock is needed.HttpConfiggainscopy()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.Base64url variant plusjavax.cryptoHMAC / 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 aMap);withExp(minutes)/withNbf(minutes)/withIssuer/withAudienceare claim shorthands, andexp/nbf/iatsupport 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=noneis 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=nonerejection, and base64url edges.JSONPatch: JSON Patch (RFC 6902) apply utility (the Patch half of next-plan §4 "JSON Patch / Schemarequiredalias"; therequiredalias already landed earlier in 2.0.2).apply(String doc, String patch)parses and serializes for you;apply(Object, List)mutates the parsedMap/Listin place and returns it. Supportsadd/remove/replace/move/copy/test, uses JSON Pointer (RFC 6901, with~1→/and~0→~decoding), andtestcompares numbers by value (1equals1.0). A malformedpath, out-of-range index,remove/replaceon a missing member, or a failedtestthrowsJSONPatchException(a subclass ofJSONException).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-Errorexceptions by default, andretryOn(Predicate)/retryOn(Class)narrow the scope;CircuitBreakeris aCLOSED→OPEN→HALF_OPENstate 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.parseyields aDocument; the DOM offersElement/TextNode/Comment/Elements, withselect/selectFirst/text/attr/innerHtml/outerHtml/childrenon 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.unescapehandle 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.htmlfidelity pass (differentially validated against Jsoup 1.18.1: 86 of 93 "HTML + selector" cases produce identical results): parser — implicithtml/head/bodyskeleton, HTML5-scoped auto-closing of optional end tags (p,li,dt+dd,td+th+tr,option,rt+rp,thead+tbody+tfoot, including apburied under inline elements), implicittbodyinsidetable, unquoted attribute values now accept/and&(href=/x?a=1&b=2is no longer truncated), and a newDataNodesoscript/stylecontent 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 throwSelectorExceptionimmediately — previously an unknown character caused an infinite loop; text —text()now matches Jsoup (space between block elements, whitespace collapsing, trimming including ), plus newownText()/nextElementSibling()/previousElementSibling()/elementSiblingIndex(), andnodeText()returns the un-normalised original; entities — newEntitiestable with ~200 frequently used named entities (©,—,…, …).HtmlTestgrew to 32 cases, including a regression case asserting that illegal selectors throw instead of hanging.com.alianga.jkit.htmlperformance 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 —Elementattributes moved from aLinkedHashMapto parallelString[]arrays (keys / values / count), child and attribute containers are lazily allocated, and a newNamesintern 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/isLetterOrDigitreplaced with local ASCII checks, andtrim().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 "collectallElementsthen dedupe viaLinkedHashSet" to direct recursion (no intermediate list, no iterator),Compound.matchesiterates by index, attribute selectors halve their scan count (one lookup instead ofhasAttr+attr),~=no longer uses a regexsplit, tag names are normalised at parse time soequalsIgnoreCasedegrades toequals, 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).HtmlTeststill 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.htmlselector index:Documentnow carries a lazy id / class / tag index (key prefixest:/#:/.:, 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.fookeys on.foo,a[href^=/p/]keys ona(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.HtmlTestgrew from 34 to 39 cases withindexAndTraversalReturnTheSameElements(runs the same queries at document level via the index and at element level via traversal, assertingassertSameon every element),indexKeepsDocumentOrder,repeatedIdAndMultiClassAreIndexedCompletely,missKeyYieldsEmptyInsteadOfCrashandindexIsBuiltOnceAndReused; 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-stylewere previously settable only through the Java chain API — neitherjkit.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'sadditional-spring-configuration-metadata.jsonlists all 24 configuration keys, sojkit.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 (MySQLGET_LOCK/ PGpg_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: truerecords every applied DDL into thejkit_schema_historytable (created automatically; rows carry timestamp / host / mode / statement; an empty plan adds no rows);export: pathwrites the planned DDL to a file (UTF-8, one statement per line) — combined withdry-runit is a plain schema generator.jkit-sql-auto:quote-identifiersupgraded 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 likelevel/modecollide with reserved words on Oracle and GBase 8a, failingCREATE TABLEoutright;SqlSchemaConvertOptionsgainsquoteIdentifiers, 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 ownmodecolumn collided with the Oracle reserved word and becamesync_mode; its existence probe gained case variants and tolerance for old Oracle drivers withoutConnection#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); callbuildKeyPair(1024)when the old length is truly needed, and anything below 512 bits throwsIllegalArgumentException.jkit-core:DESCryptandEncryptUtils.DESare@Deprecated(56 effective bits are brute-forceable, and neither ECB nor CBC detects tampering). Both still work for reading legacy ciphertext; useAESCrypt.encryptGcmfor anything new.jkit-sql-auto: the Spring Boot starter now runs by default withphase: eager— schema sync happens during context refresh (after the DataSource is ready, before the web port opens), so afail-fasterror aborts the application before it accepts traffic (previously the work hung off ApplicationReadyEvent with the port already bound); setphase: readyto restore the 2.0.1 behaviour. Also, with no DataSource bean and nojkit.sql.auto.urlconfigured, the starter now skips with a warn log instead of failing application startup.jkit-notify: Markdown rendering now lives onMarkdown(toHtml/toDocument/wrapDocument). The 2.0.1NotifyUtils.markdownToHtml(md)/markdownToDocument(md, boolean)/wrapHtmlDocument(fragment, boolean)methods remain and are@Deprecated.toDocument/wrapDocumentgenerate 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 onlypadding-top:34px, which was wiped out by the inline style and by the shorthandpaddingin the mobile media query).jkit-notify: mobile media-query rules now carry!important— in inline mode a tag'sstyleattribute 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 shorthandpadding.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 offbody). Gmail, QQ Mail and similar clients strip<html>/<head>/<body>, somax-width/paddingwritten onbodyvanished 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 —bodyno longer carriesmax-width(the shell inlinesmargin:0on body so the background fills the viewport; the width cap stayed, the centering margin was overridden). Column width andmargin:0 autohang only offdiv.jkit-md; the mobile media query also pads that container alone.jkit-notify: default column width 720 → 820px; desktop padding is now24px 16pxand mobile12px 8px(almost no horizontal gutter). The container width comes from the theme's ownmaxWidthtoken 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<img>. Only src/alt/title/width/height/class/loading are kept; event handlers such asonerrorare dropped and ajavascript:src becomes#. Other HTML tags are still escaped.jkit-sql: the row-level inject implementation is renamed fromSqlTenantRewritertoSqlInjectRewriter(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 extendAbstractComplexSqlSliceL2Testand only declare dialect and id range (about 1100 fewer duplicated lines).jkit-sql:SqlIdentifiernow tracks quoting per name part (markQuotedPart/isPartQuoted/quotedParts). Previously a singlequotedflag covered the whole identifier, soc."LEVEL"round-tripped to"c"."LEVEL"— the quotes leaked onto the table alias, and Oracle rejects it withORA-00904because"c"does not match aliasC(15 of 900 corpus cases failed on real databases).SqlParsermarks per part,SqlAstClonercopies the bitmap,SqlFormatteremits 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.deepStructureDriftrecursively 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/bindNamedstring 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/driverForUrlnow delegate toJdbcUrlUtils(more URL prefixes: Gauss, Kingbase, Hive, ClickHouse, Trino, …).
Fixed
jkit-core: end-tag lookup forscript/style/textareaincom.alianga.jkit.htmlwas O(tags × document length) — every lookup rantoLowerCase()over the whole document beforeindexOf. A synthetic page with onescripthid it, but a real 70 KB index page with 24scripttags parsed in 3.929 ms, 14x slower than Jsoup. It now scans character by character and compares withregionMatches(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 therawTextCloseTagIsCaseInsensitiveregression.jkit-core:precontext did not propagate whitespace preservation incom.alianga.jkit.html. Newlines insidepre > codewere collapsed to spaces (text()on<pre><code>int a = 1;\n int b = 2;</code></pre>returnedint a = 1; int b = 2;), corrupting code-block extraction. It now matches Jsoup: walk up through parents, preserve raw whitespace once apre/textareais hit, and stop preserving when a non-inline element appears in between — sopre > code > spanpreserves whilepre > div > codedoes not. Covered by thepreContextKeepsNewlinesregression.jkit-core: the JSON Schemarequiredkeyword was parsed but never enforced — a payload missing a field declared the standard way still passedvalidateSuccess, a silent failure.JSONNode#validateSchemaObjectnow checks that every field listed inrequiredexists (standard semantics: presence is enough, and anullvalue counts as present;typestill decides type mismatches).requiredworks withoutpropertiesand applies to nested objects. The library's ownmustkeeps its meaning (field present and notnull), and when both appear both must hold.jkit-sql: round-tripping no longer drops expression parentheses.(a - b) / cused to come back asa - b / c,-(a + b)as-a + b, andROUND((SELECT …), 2)asROUND(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 anORDER BYnow wraps asSELECT * FROM (set-op) ORDER BY …before the ROWNUM layers, so Oracle does not raiseORA-00904when 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 helperRNcolumn (a sourceSELECT *still includesRN).jkit-sql:SqlNode.toString()now uses MySQL identifier quotes (backticks) by default, matchingSQL.toSqlString.addComment("text")and compact--line comments are emitted as block comments so they cannot swallow the following statement. BareaddHinttext is wrapped as a slash-star-plus hint. UseSQL.toSqlString(stmt, dialect)for another dialect.jkit-sql-auto:SqlAutoInspectornow resolves schema when checking whether a table exists. If unset, it usesConnection.getSchema(); with no connection (dry-run) or an unsupported driver, it parses the JDBC URL. PostgreSQL / Gauss default topublic, SQL Server todbo, 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 ofGeneratedCode.notes(), ahead of the generator's own notes.jkit-curl-codegen: generators no longer silently drop content.py-requestsnow emits a realfiles=dict for multipart and passes it to the request (file uploads previously vanished);py-httpxgained multipart / file bodies;js-fetch/js-axiosemit a readable fs example with the file path for-Tbodies instead of a bareundefined;go-nethttp/csharp-httpclientgenerate real multipart code (mime/multipart/MultipartFormDataContent) and file bodies (os.Open/File.ReadAllBytes);php-curlemitsCURLFilefor multipart. Proxy handling aligned:py-requestsno longer hardcodeshttp://(keeps scheme and credentials);java-okhttp/kotlin-okhttpemitproxyAuthenticator;go-nethttpuseshttp.Transport;csharp-httpclientusesWebProxy;java-apacheemitssetProxy;js-axiosemits aproxyblock;php-curlemitsCURLOPT_PROXY;js-fetch/js-nativeexplain in notes when a per-request proxy cannot be expressed.jkit-curl-codegen: thehargenerator'sdecodeno longer turns a literal+in URLs/forms into a space (+is preserved;%20/%2Bstill decode correctly); the creator version is no longer hardcoded2.0.1— it readsImplementation-Versionfrom the jar and falls back to the current version in development.CodeQuotenow 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/--cookieflags with;like curl does (previously each overwrote the last);-Eis skipped consistently with--cert(reported as ignored rather than unsupported).jkit-core: URLs inHttpUtilsdebug logs and "total timeout exceeded" exceptions keep their full structure but mask sensitive fields — query values ofaccess_token/token/sign/signature/secret/keyand path tokens such as Telegram/bot<token>/and ServerChan/<SendKey>.send; new public helperHttpIo.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 (thedotStuff()helper existed but was never wired in);AUTH PLAINis preferred when advertised by EHLO (previously onlyAUTH 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: newNotificationChannel.validate(ChannelConfig)(default no-op).NotificationManagerinvokes it before any network send, restoring the "programming errors never surface after a partial send" contract ofsendAll/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:Messagegainscopy(); SMS channels now stamp the current receiver on a copy per number instead of mutating the caller's message, so nosmsReceiverleaks back and one message can fan out to several SMS channels concurrently.jkit-notify:SendResultaggregation 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 useopenStream().jkit-notify: the channel registry (register/unregister/get/list) is now synchronized, so concurrent access can neither lose channels nor throwConcurrentModificationException;NotifyPolicydedup reservation and rate-limit counting moved intobeforeSendas atomic operations (afterAttemptis 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 withExceptionInInitializerError.jkit-notify-extra: Aliyun SMSRegionIdnow followsCFG_REGION(configurable region) instead of being hardcoded tocn-hangzhou.jkit-core: the expression evaluatorcom.alianga.jkit.expressionfixes 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 astrue || (1/0)orfalse && (1/0)threw a division-by-zero exception, and any side effect in the right operand could not be skipped.ExprEvaluatornow returns as soon as the left operand decides the result, without evaluating the right operand, matching Java semantics.>/</>=/<=previously hard-cast operands toNumber, so comparing strings threw; they now compare numbers by value and otherComparables by natural order (e.g. string lexicographic order), with a clear error for incomparable types. NewExpressionRegressionTest(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.beansfixes 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 theputAllbranch used when no exclude list is given.ObjectUtils.set(collection, "[n]", value)previously threwTypeNotMatchExecptionunconditionally even after a successful positional assignment, making collection index assignment unusable; the throw now fires only when the key is not an[n]form. NewBeansTest(17 cases) covers the four copy directions, merge, collection index assignment, multi-level paths, non-empty fields andisEmpty— this package previously had zero tests.jkit-core:MultiKeyHashMapread and write sides previously disagreed with each other. The class documents that lookups ignore key case, but onlyputregistered the internalkeyMap; every other entry point lagged behind.putAlldelegated to the superclass, so batch-written entries could never be read case-insensitively (afterputAll({"UserName":"tom"}),get("username")returnednull, while per-entryputreturned the value).removematched the key exactly, soremove("username")afterput("UserName", …)failed silently —size()unchanged andget("username")still returning the value. NeitherremovenorclearcleanedkeyMap, so deleted keys were retained and could not be collected.containsKeywas likewise exact-only and contradictedget. NowputAllroutes each entry throughput,removedeletes case-insensitively and invalidates the registration,clearalso emptieskeyMap, andcontainsKeymatchesget.jkit-core:MultiKeyHashMapcase-insensitive matching now usestoLowerCase(Locale.ROOT). The no-argtoLowerCase()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 — afterput("ID", …),get("id")returnednullandcontainsKey("id")wasfalse. 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 derivedgetInteger/getLong/getDouble/getFloat/getShort/getByte) no longer treats a half-parsed string as a number.NumberFormat#parse(String)only parses a prefix, so"123abc"returned123,"2024-01"returned2024and"1.2.3"returned1.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 withParsePosition, requiring the whole string to be consumed; anything not fully parsed returnsnull(falling back to the default value in thedefaultValueoverloads). Side effect:" 42 "now yields42instead ofnull. This is a behaviour change.jkit-core:com.alianga.jkit.collectiongets its first test coverage (previously zero tests). NewMultiKeyHashMapTest(12 cases),MapsNumberParseTest(9 cases) andCollectionsTest(13 cases, covering emptiness,getFirst/getLast, removal by object and by predicate, find and convert, the Map / array / Iterator / Enumeration / primitive-array branches ofsize, pluscontainsandfindValueOfType). Reverting the fixes makes 9 of them fail, all on the fixed points above. Also corrected theCollections.size(Object)javadoc —nullactually returns0rather than throwingIllegalArgumentExceptionas documented — and documented the semantic gap inCountMapbetweengetand the inheritedcontainsKey/getOrDefault: usecontainsKeyto ask whether a key was ever counted.jkit-core: retained heap ofcom.alianga.jkit.htmlnow 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 throwIndexOutOfBoundsException, broken groups like",p"/"p,,p"silently matched the entire document (including the#documentroot) 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 throwSelectorException. Attribute selectors are tightened too:img[src(unterminated[) used to throwStringIndexOutOfBoundsException, 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 tonth-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)throwsSelectorExceptionrather thanNumberFormatException. Tag-name normalisation usesLocale.ROOT.jkit-core: tree operations incom.alianga.jkit.htmlwere converted from recursion to explicit-stack iteration, hardening against maliciously deep input. The parse loop was already iterative, buttext()/outerHtml()/nodeText(), selector traversal and theDocumentindex 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 incom.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😀were truncated by a(char)cast into a U+F600 mojibake (now a proper surrogate pair viaCharacter.toChars); and attributes on the<html lang="en">tag were silently dropped (head/bodykept theirs).jkit-core: two performance/consistency items for selectors andElements. Group-selector deduplication ("p, .c") switched from a linearcontainsover the result list (O(n²)) to aHashSetmarker;Elements.selectnow actually deduplicates as its javadoc claims — with nesteddivs,doc.select("div").select("p")used to return the sameptwice, 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 (ExprParsercompiles the tree intoImplclasses after the 2nd evaluation):flag && (1/0)short-circuited tofalsefor the first two evaluations and then threw a division-by-zero exception, and comparing strings with"a" < "b"threw a rawClassCastException.LogicalAndImpl/LogicalOrImplnow short-circuit and wrap exceptions exactly like the general path, and theGt/Lt/GE/LEimplementations delegate tocompareRelational.ExpressionRegressionTestgains 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: thetestoperation ofJSONPatchcompared numbers viadoubleValue(), so integers beyond 2^53 were truncated into false equality —9007199254740993equals9007199254740992, and two 30-digitBigIntegers differing by 1 compare equal (toBigDecimalalso truncatedBigIntegerthroughlongValue());testis the optimistic-concurrency primitive, so false equality lets a patch apply with its precondition unmet. Comparison now goes throughBigDecimalexactly, keeping1==1.0. Array-index parsing also follows RFC 6901 strictly:/a/01(leading zero) and/a/+1, previously accepted byInteger.parseInt, now throwJSONPatchException.jkit-core:CircuitBreakerhad two concurrency defects. State transitions used a stale snapshot taken before the task ran and applied unconditionalset()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 (openedAtis 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, andErrors thrown by the task propagate with their original type, matchingRetryer.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.removecleared the lower-case alias unconditionally — afterput("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/mergewere not overridden and bypassedput's keyMap registration, silently disabling case-insensitive lookup for those entry points; all are covered now.remove(null)used to collapseString.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 maintainkeyMap.jkit-core:JwtUtilshardening. Signing and verification now reject an HS256 shared key shorter than 32 bytes (256 bits) withIllegalArgumentException— tokens signed with low-entropy keys can be brute-forced offline (aligning with the documented recommendation).verifygainsleewaySecondsoverloads so exp / nbf checks can tolerate clock skew (default 0, behaviour unchanged). Segment splitting usessplit("\\.", -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 inJwtException(previously a rawIllegalArgumentExceptionescaped, breaking the@throws JwtExceptioncontract).jkit-core: the String-basedEncryptUtils.RSA.encryptOaep/decryptOaepno 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 declarethrows Exception, matching the siblingbyte[]variants), and the log statements with never-interpolated%splaceholders are gone.jkit-core:DesensitizeUtilsno longer returns single-character input unchanged —maskemits "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: threeULIDfixes. 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 (UUIDv7in the same release carries into the next millisecond; ULID now does too).parsegained the 48-bit timestamp upper-bound check thatencodealready had (parse("ZZZ…")used to pass and thenparse(x).toString()threw, breaking round-trips), withisUlidaligned.toBytesno longer decodes the same string twice.jkit-core:Retryerinterruption semantics. AnInterruptedExceptionthrown 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 theResilienceException).jkit-core: instantiated-HTTP wrap-up.UrlConnectionHttpEnginereads the body copy buffer through the active-awareHttpUtils.config()(it used to read the globalHttpConfig.shared()unconditionally, so an instance-leveldownloadBufferSizehad no effect); the lazily cachedmethodfield is nowvolatile; theHttpConfig.copy()javadoc honestly states thatexecutor/endpointPool/sslContext/retryPolicyare shared references (the "deep copy" wording was inaccurate); and the caller-lessHttpUtils.currentDefaultMediaType()is removed.
Documentation
docs/sql.md/docs/en/sql.md: entity table/column comments now name this module'scom.alianga.jkit.sql.entity.Comment.docs/sql.md/docs/en/sql.md: classic Oracle ROWNUM wrapping now documents liftingORDER BYoff a set-op before pagination, and thattoSqlStringmust name the target dialect.docs/sql.md/docs/en/sql.mdbusiness-scenario section: extra copy-paste samples forbind/inject/expandStar+replaceSelectItems/addComment+dialect quotes / MyBatis#{}/ ${}. The template-placeholder section now shows the common parse+bind pattern. Samples matchSqlBusinessScenarioTest.- Business scenarios expanded to 18 (2.0.2): dialect-from-URL (
JdbcUrlUtils), reportDATE_FORMATrewrite, safe dynamic table-name bind, low-code query sandbox (Wall table allow/deny, required WHERE columns, max tables). Scenario 4 also covers tautologyLIKE '%'/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 howmaskfalls back.docs/json.md/docs/en/json.md: the keyword table now distinguishesrequired(standard array form) frommust(this library's boolean form), with a new "Required:requiredormust?" subsection.docs/csv.md/docs/en/csv.md: fill in the previously empty (0-byte) file, coveringCSVUtilslightweight I/O,CSV/CSVTableheader-aware read with POJO mapping,CSVObjectWriterstreaming object write, streamingreadStream, charset and parsing details, and exceptions; removed fromsrcExcludeand 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 parametersp0/p1,@-prefixed built-in functions, custom functions and static-method registration,renderTemplate, theCacheableExpressioncache notes, and exceptions; added to the sidebars, andtoolkit.mdgains 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.poston 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 separatetools-testproject (com.alianga.test.html.HtmlParseBenchTest/HtmlRealSiteBenchTest), reports are archived underreports/, 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 throwNullPointerException. BothClassStrucWrap.checkClassStructureand the JSON POJO serializer code generator calledgetPackage().getName()directly, whilegetPackage()returnsnullfor default-package classes on JDK 8 (and an empty-named package on JDK 9+, which made the generator emit an illegalpackage ;). Both sites are now null-safe: nopackagestatement 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 emittingpackage ;on JDK 11+). AddsReflectNullPackageTest(2 cases) andDefaultPackageSerializationTest(3 cases).jkit-core: extended HTTP methods such as PATCH no longer throwProtocolExceptionon the fallback engine under JDK 17+.HttpURLConnection.setRequestMethodonly accepts whitelisted methods; the previous fallback rewrote the privatemethodfield viasetAccessible(true), which throwsInaccessibleObjectExceptionfrom JDK 17 onwards because of module encapsulation. The field is now written throughUnsafefirst (reflection kept as a second fallback), so PATCH is really sent on JDK 8 / 11 / 17 / 21 / 25. AddsUrlConnectionPatchMethodTest(3 cases driving the engine directly, bypassing engine selection).jkit-core: test sources are now compiled with the JDK 8 toolchain.maven-toolchains-pluginswaps the default toolchain for the declared 9 / 11, so the defaulttestCompileran with the JDK 11 javac — bytecode was still major 52, but method signatures were resolved against JDK 11 (e.g. the covariantByteBuffer#flip()), which surfaced asNoSuchMethodErrorwhen the suite ran on JDK 8.base-compileanddefault-testCompilenow pin the JDK 8 toolchain in both profiles;mvn clean testpasses 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,SqlDataTypeRegistryvalidation andRegistryValidationTest, Phase 2–3 whole-statementSQL.convertentry withSqlSchemaConvertOptions/generateOracleSequence, the complete function-rewrite list (incl.CONVERT USING charsetnot mis-mapped,DATEADD/GETDATE/FROM_UNIXTIME,UCASE/LPAD/CEIL/YEAR…),SQL.convertBatchandALTERhandling, the real-DB regression command block,SqlDialectSpecextension,ConversionResult.sqlWithExtras()andSqlSchemaConverterProvider.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 ofElements.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: theHALF_OPENstate 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 except0, no+) and the exactBigDecimalcomparison oftest(large integers are no longer truncated into false equality bydouble).docs/toolkit.md/docs/en/toolkit.md: the ULID section documents the 48-bit timestamp upper bound enforced byparseand 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 viachar[]+ 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 throwsSqlParseException. Usage: seedocs/sql.md. - Dialects: first-class enum
MYSQL(default),POSTGRES,ORACLE(pre-12c ROWNUM pagination),ORACLE12(OFFSET/FETCH),SQLSERVER,ANSI,H2,DB2,SQLITE,HIVE(aliasesmaxcompute/odps/argo),CLICKHOUSE,PRESTO(aliastrino),DAMENG(dm/dameng/jdbc:dm:).fromNamecovers GoldenDB / SelectDB / AnalyticDB / MatrixOne / StoneDB / HighGo / UXDB / MogDB / Vastbase / AntDB / IvorySQL / GBase 8a / GBase 8s / Xingyun / Oscar and more, aligning with icellcommon-modelSQL data sources. Capabilities are driven bySqlDialectSpecand can be individually overridden viaSqlDialectWrapper(e.g. MySQLANSI_QUOTES). - Pagination:
getLimit/getOffset/setLimit/setOffset/setPage/addLimit/SQL.adaptPagination.format/toSqlStringadapt to the target dialect (MySQLLIMIT↔ classic ORACLE ROWNUM ↔ ORACLE12OFFSET FETCH↔ SQL ServerTOP|OFFSET FETCH).SqlBuilder.limit/offsetgenerate output based on the active dialect instead of always emittingLIMIT. - 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 / columnDEFAULT: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/CHRetc.; FULLTEXT upgraded toMANUAL_ACTION_REQUIRED. Function rewriting is extensible viaSqlSchemaConverterProvider.registerFunctions(SPI override). - Entity DDL:
SqlEntitiesscans@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-nameCommentannotation 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;SqlWallRulechain +SqlWallConfig.rules(...)(built-in selectOnly / denyDdl / no-WHERE writes etc., behavior and violation codes unchanged).SqlAstVisitortype dispatch (non-breaking toSqlVisitorAdapter). - 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_RECOGNIZEstructured;RENAME TABLEindependent 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.mdrather 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 againstDatabaseMetaData, and executesCREATE TABLE/ALTER TABLE ADD/CREATE INDEX. Modes:none/validate/update(default, append only) /create/create-drop. Configuration prefixjkit.sql.auto.*, data source falls back tospring.datasource.*. Zero third-party runtime dependencies. SqlAuto.dropdeletes managed tables in reverse foreign-key order; Spring Boot 2 / 3 auto-configuration modulesjkit-sql-auto-spring-boot-2andjkit-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/ chaintablePrefix(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, defaulttrue): 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).NotificationChannelSPI +NotificationManager;MessageType(TEXT / MARKDOWN / HTML). Zero third-party dependencies. Usage: seedocs/notify.md. - Failure classification:
SendResult.failureType()/isRetryable(),FailureTypeis RETRYABLE / THROTTLED / CONFIG_ERROR / PERMANENT / SUPPRESSED; unlisted error codes fall back to the HTTP status. Custom channels can overrideAbstractHttpChannel.classify. - Messages truncated by UTF-8 bytes (no splitting of CJK characters or emoji); DingTalk @-mentions automatically append missing
@phoneinto the body;Message.var/varsreplace${key}/${a.b}; attachments streamed, split at10MB/512KB, MIME auto-detected. - SMTP:
sslProtocolspins the TLS version,trustAllCertssupports self-signed gateways; MARKDOWN sent astext/htmlviaNotifyUtils.markdownToHtml. Feishu signature written into the JSON request body.NotifyPolicy: quiet hours, 5-minute deduplication, local rate limiting;sendFailoversequential 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 coreMessage.
jkit-core
DateUtils.parse(String): auto-detects timestamps, compact numerics,-//., Chinese / Korean formats, ISO-8601 (includingT/Z/+0800/+08:00).DateUtils.fromEpochNumber(long): converts 10-digit seconds or 13-digit milliseconds toDate.DateUtils.fromTemporal(TemporalAccessor): convertsjava.timeobjects toDate.ConvertUtils.toDateadditionally supportsInstant,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 touser.home).Print.enableLog: whenfalse, 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 pointCurlCodegen.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, plushttp/har/httpieinterop formats.
Changed
jkit-sql: Breaking —SQL.andWhere/replaceTable/replaceColumnnow use the same clone-then-mutate pattern asaddLimit/setPage(return a new AST, leave the original tree untouched); callers must use the return value.jkit-sql:SQL.clonenow performs a true AST tree copy (SqlAstCloner/SqlNode.copy); hot paths no longer format→parse; cross-dialectformat/adaptPagination/setPagesignificantly faster. Classic ORACLE offset=0 single-layer ROWNUM wrapping skips cloning.jkit-sql-auto:dryRun(true)onSqlAuto.run(options)/plan(options)no longer opens a JDBC connection / DataSource. The URL is only used to infer the dialect; a fullCREATE TABLEplan is produced against a blank database. Overloads that receive aConnectionstill compare against live tables but do not execute.jkit-core:DateUtilsdropsSimpleDateFormat.yyyy-MM-dd HH:mm:ssuseschar[]+ second-level cache; other common patterns use hand-written assembly orDateTimeFormatter.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.randomBirthswitched toLocalDate(no longer throws whenminAge == maxAge);getUUIDdash removal no longer uses regex;IdCardUtilsrandom birthday generates by actual days in the month.jkit-core: curl parsing aligned with curl semantics (expands-kLs/-XPOST,--json/--data-urlencode,@fileno longer treated as literal body). Public APIs:parseCurl/curlToRequest/curl/curlString/requestToCurl, modelParsedCurlRequest.CurlParser.generateand thehttp.codegenpackage removed from jkit core; depend onjkit-curl-codegeninstead.jkit-notify: coreChannelConfigremoves SMS-specifictemplate/appId/region; replaced byChannelConfig.extra+AbstractSmsChannel.CFG_*. Core channels registered only viadefaults(); SPI loads only extra modules to avoid double registration.jkit-notify-extra:SlackChannel.EXTRA_CHANNELreplaces usingMessage.EXTRA_GROUPfor Slack channels (brief backward-compatible fallback toEXTRA_GROUP).
Fixed
jkit-core: when the response declaresContent-Encoding: gzipbut the body is empty, JDK 8 no longer throwsEOFException; returns an empty stream (underlying connection is still reclaimed on close).jkit-sql: when a subclass and aMappedSuperclass/ parent class both declare a same-named column, only the subclass field is kept, preventing PostgreSQLcolumn specified more than once.jkit-sql:@GeneratedValue(generator="system-uuid")/GenerationType.UUID/ non-integer@SqlGeneratedno longer produceAUTO_INCREMENT/IDENTITY(PostgreSQL would syntax-error onIDENTITYfor aVARCHARprimary 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.formatpretty mode now wraps and indentsCREATE TABLEcolumns (toSqlString/ compact still single-line).jkit-sql:GROUP_CONCATwithout an explicitSEPARATORno longer loses the separator when rewriting toSTRING_AGG/LISTAGG; MySQLMODIFY/CHANGErewritten to PG/ANSI/H2/PRESTO no longer duplicate the column name.jkit-sql:SUBSTRING/LEFT/RIGHTrewritten per dialect (PG/MySQL/H2 useFROM n FOR m; SQL Server/SQLite/Hive/ClickHouse use comma form; negative start rewritten withLENGTH/LEN); Dameng bare SELECT appendsFROM dual.jkit-sql:SqlDialectWrapperno longer delegates derived methods (identQuoteClose/quoteIdent/pipesAreConcat/preferredLimitStyle); subclass overrides of primitives now automatically cascade to derived capabilities.jkit-sql: rewrite fidelity —NATURAL JOINno longer loses its modifier;RENAME TABLEno longer writes illegalALTER TABLE;ADD UNIQUE KEYno longer dropsUNIQUE;IGNORE/LOW_PRIORITY/HIGH_PRIORITYonINSERT/UPDATE/DELETEare preserved in the AST and rewritten; non-bare-identifier aliases are force-quoted.jkit-sql:tables()now capturesCREATE TRIGGER/CREATE TABLE … LIKE/RENAME TO/ multi-tableOPTIMIZE|ANALYZE; database names, routine names, and CTE names are no longer miscounted. Comment-only / blank-only input is classified asOTHERinstead of throwingempty SQL.jkit-sql-auto: on classic Oracle, auto-generatedCREATE INDEXnames exceeding 30 characters are now truncated to avoid ORA-00972.jkit-curl-codegen: R (httr2) now usesreq_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 injkit-core(release coordinate remainscom.alianga:jkit), other capabilities in separate modules.mvn testat the root builds all modules. git-commit-id-plugin,buildnumber-maven-plugin,maven-source-pluginmoved from thepublishprofile to the default build;packagenow 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'sMETA-INF/versions/9,/11use toolchains targeting JDK 9 / 11; onlyjkit-sql-auto-spring-boot-3uses toolchain for JDK 17 (required by Spring Boot 3). Requires~/.m2/toolchains.xmldeclaring jdk 8/9/11/17/21. Launch example:JAVA_HOME=$(jdk8) mvn -o clean install. jkit-notifylive-use tests require-Djkit.notify.live=trueand complete YML credentials; defaultmvn testnever sends live messages even when keys are present.
Documentation
docs/sql.md/docs/en/sql.mdupdated to align with code: cross-dialect pagination,SQL.clone,SqlRewrites,SqlEntitiespublic 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
- Change the
<version>in the rootpom.xml(jkit-parent) to the new version number; child modules inherit it. Update the README header and dependency examples, and the version printed byMain.java. - 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. - 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). - Add
@since x.y.zto javadoc for newly added public APIs. - Leave historical versions in place; do not rewrite old entries as new versions.
Template:
## x.y.z - YYYY-MM-DD
### Added
- ...
### Changed
- ...
### Fixed
- ...
### Build
- ...Omit any section heading when there are no changes in that category.