La singularité technique de Champollion tient en deux points : un parser JSON-P state-machine taillé à la main pour minimiser les allocations, et un codegen JSON-B compile-time via APT qui élimine la réflexion à chaud. Cette page détaille les deux pipelines.

Vue d’ensemble des modules

Diagram

champollion-jsonb dépend de champollion-jsonp. Jamais l’inverse — la couche binding compose sur la couche processing.

Pipeline JSON-P — parser pull

Le parser implémente une state machine RFC 8259 scannant caractère par caractère, la plus branchless possible.

stateDiagram-v2
    [*] --> START
    START --> IN_OBJECT: '{'
    START --> IN_ARRAY: '['
    START --> VALUE: literal
    IN_OBJECT --> KEY: '"'
    KEY --> AFTER_KEY: '"'
    AFTER_KEY --> VALUE: ':'
    VALUE --> AFTER_VALUE
    AFTER_VALUE --> IN_OBJECT: ','
    AFTER_VALUE --> END_OBJECT: '}'
    AFTER_VALUE --> IN_ARRAY: ','
    AFTER_VALUE --> END_ARRAY: ']'
    END_OBJECT --> [*]
    END_ARRAY --> [*]

Composants clés (package io.vidocq.champollion.internal.jsonp.parser) :

  • ChampollionJsonParser — cœur de la state machine, expose l’API JsonParser.

  • Tokenizer caractère par caractère, branchless dès que possible (table de transitions plutôt que switch).

  • Détection BOM UTF-8 / UTF-16 BE/LE / UTF-32 BE/LE conformément à RFC 8259 + JSON-P §3.3.

  • BufferPool simple (file MPMC bornée), pas de dépendance externe — découvert via Json.createParserFactory(Map).

Le JsonGenerator symétrique pousse via un Writer automatiquement bufferisé (BufferedWriter interposé sur tout Writer non déjà bufferisé). Escape RFC 8259 §7 précompilé pour les caractères de contrôle.

Pipeline JSON-B — runtime introspectif (mode défaut)

sequenceDiagram
    participant App
    participant Jsonb as ChampollionJsonb
    participant Cache as ClassValue<BindingPlan>
    participant MH as MethodHandles
    participant Gen as JsonGenerator

    App->>Jsonb: toJson(person)
    Jsonb->>Cache: get(Person.class)
    alt cache miss
        Cache->>MH: privateLookupIn + unreflect
        MH-->>Cache: BindingPlan{writers...}
    end
    Cache-->>Jsonb: BindingPlan
    loop for each PropertyWriter
        Jsonb->>Gen: write(name, MH.invoke(person))
    end
    Gen-->>App: "{...}"
  • BindingPlan est calculé une fois par classe, lecture lock-free via ClassValue.

  • PropertyWriter / PropertyReader adossés à MethodHandles — pas de setAccessible(true) à chaud, le consommateur doit opens son package.

  • Customization (@JsonbProperty, @JsonbDateFormat, @JsonbTypeAdapter, etc.) compilée dans le plan une seule fois.

Pipeline JSON-B — codegen statique (APT)

C’est le point fort identitaire de Champollion. Plutôt que d’introspecter à chaud, l’annotation processor champollion-codegen-apt génère à la compilation un <Type>$$Binding.java qui implémente JsonbBinding<T> directement, sans réflexion.

Diagram

Caractéristiques :

  • Code Java généré, pas de bytecode dans champollion-codegen-apt. Le bytecode arrive via javac sur le source généré — débuggable, lisible.

  • Class-File API (JEP 484) envisagée pour des passes secondaires (constantes pré-encodées en bytecode UTF-8). Pas de Byte Buddy, pas d’ASM.

  • META-INF/services/io.vidocq.champollion.jsonb.spi.BindingFactoryProvider : ServiceLoader auto-découverte par ChampollionJsonb.

  • Runtime fallback : si aucun binding statique trouvé, ChampollionJsonb retombe sur l’introspection M4. Loggué INFO ; flag champollion.jsonb.warn-on-fallback=true pour audit en prod.

  • Compatible AOT : zéro réflexion ⇒ GraalVM native-image sans config, Leyden CDS heureux.

  • Optimisations dans le binding généré : noms de propriétés en byte[] constants pré-encodés UTF-8, ordre des écritures stable, écritures groupées (writeStartObject + premier write fusionnés via writeString fast-path).

champollion-codegen-maven-plugin est un orchestrateur Maven qui scanne le classpath du projet hôte et invoque l’APT sur les types JSON-B détectés mais non annotés @JsonbStatic (utile pour les classes tierces).

Virtual threads & ScopedValue

  • Aucun synchronized, aucun ThreadLocal exporté.

  • La propagation contextuelle pendant toJson / fromJson (config courante, recursion guard) passe par ScopedValue (JsonbContext.CURRENT).

  • Un pool de parsers est exposé qualifié JPMS : exports io.vidocq.champollion.internal.jsonp.parser to io.vidocq.champollion.jsonb (cf. lien:https://codeberg.org/Vidocq/champollion/src/branch/main/BENCH.md[BENCH.md] §7.4).

Compatibilité AOT

GraalVM native-image

Mode statique : zéro config nécessaire (pas de réflexion). Mode runtime : nécessite reachability metadata pour les classes bindées.

Leyden CDS

Mode statique : pleinement supporté ; l’AppCDS pré-charge les <Type>$$Binding. Mode runtime : compatible mais sans bénéfice.

Java Module System

module-info.java strict pour chaque artifact ; packages internal.* non exportés ; SPI exposée uniquement via provides …​ with.

Pourquoi ce choix d’architecture ?

  • Zéro dépendance : Champollion s’embarque dans n’importe quel runtime sans entraîner Parsson, Yasson, Jackson, ou leurs transitives. Surface d’attaque minimale, démarrage rapide.

  • Codegen plutôt que réflexion : prévisible (pas de surprise au premier hit), AOT-friendly, débuggable (le code généré est lisible).

  • JPMS strict : encapsulation forte, on peut faire confiance à internal.* pour casser entre versions sans préavis.

  • Virtual threads : les serveurs REST (Cassini) modernes tournent à des milliers de threads concurrents — pas de point de contention dans le sérialiseur.