Il problema degli oggetti "fantasma" nelle compilazioni native (Java/Kotlin)

Il problema degli oggetti "fantasma" nelle compilazioni native (Java/Kotlin)

English version below


Il problema degli oggetti "fantasma" nelle compilazioni native (Java/Kotlin)

Scenario del problema

Vi siete mai chiesti perché alcune risorse/oggetti/funzionalità non risultino disponibili quando eseguite un rilascio su una piattaforma per esempio docker.

Ebbene si, durante lo sviluppo di un'applicazione Quarkus con linguaggio java/kotlin (valido anche per Spring Boot con GraalVM) ci siamo imbattuti negli oggetti fantasma delle compilazioni native.

Il comportamento era particolarmente insidioso perché:

  • La compilazione JVM tradizionale funzionava correttamente
  • L'errore si manifestava solo dopo la compilazione nativa
  • La classe mancante apparteneva a una dipendenza standard (jakarta.validation)

Ed è proprio qui che abbiamo affrontato l'errore:

Type not found: class jakarta.validation.constraints.Pattern$Flag

when calling ...FieldValidator

Tutto funzionava senza problemi fino a quando l'applicazione era fine a se stessa; con l'obiettivo di generalizzare alcune funzionalità da condividere con più applicazioni abbiamo separato le logiche creando un sdk common, che scatenato il problema sopra descritto.

La compilazione nativa grazie all'approccio conservativo, opera come un barboncino troppo zelante, GraalVM taglia tutto ciò che non è esplicitamente referenziato nel codice durante l'analisi statica.

Diagnosi del problema

Il problema è radicato nel modo in cui la compilazione nativa (via GraalVM) gestisce la reflection:

  1. Ottimizzazione aggressiva: Il compilatore nativo esclude per default classi non referenziate esplicitamente
  2. Dynamic Loading: Alcune librerie (come Hibernate Validator) usano reflection per caricare classi dinamicamente
  3. Jakarta Validation: Le annotazioni come @Pattern usano internamente classi annidate (es. Pattern$Flag) che non vengono rilevate dall'analisi statica

Soluzione implementata

Abbiamo risolto con un'estensione Quarkus che forza l'inclusione delle classi necessarie(es.):

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerReflectionClasses(BuildProducer<ReflectiveClassBuildItem> producer) {
    producer.produce(
        new FeatureBuildItem("core-validator")
    );
  ...
}

Best practices per evitare il problema

  1. Test early: Eseguire test in modalità nativa durante lo sviluppo
  2. Profile Reflection: Usare quarkus.native.enable-reports=true per analizzare le classi caricate
  3. Extension-first: Preferire le estensioni Quarkus alle soluzioni manuali

Conclusioni

Le compilazioni native richiedono un approccio diverso alla gestione delle dipendenze. Questo caso specifico dimostra come anche librerie standard possano causare problemi inaspettati. La soluzione tramite estensione Quarkus si è rivelata la più mantenibile e scalabile per il nostro use case.

The Problem of "Ghost" Objects in Native Compiling (Java/Kotlin)

Problem scenario

Have you ever wondered why some resources/objects/functionalities are not available when you perform a release on a platform such as Docker.

Well, yes, during the development of a Quarkus application with java/kotlin language (also valid for Spring Boot with GraalVM) we came across ghost objects of native compilations.

The behavior was particularly insidious, gradually proceeding, because:

  • Traditional JVM compilation worked fine
  • ​​The error appeared only after native compilation
  • The missing class belonged to a standard dependency (jakarta.validation)

And that's where we faced the error:

Type not found: class jakarta.validation.constraints.Pattern$Flag

when calling ...FieldValidator

Everything worked fine until the application was an end in itself; with the aim of generalizing some functionality to share with multiple applications, we separated the logic by creating a common SDK, which triggered the problem described above.

Native compiling, thanks to the conservative approach, works like an overzealous poodle; GraalVM cuts everything that is not explicitly referenced in the code during static analysis.

Problem diagnosis

The problem is rooted in the way native compilation (via GraalVM) handles reflection:

  1. Aggressive optimization: The native compiler excludes classes that are not explicitly referenced by default
  2. Dynamic Loading: Some libraries (like Hibernate Validator) use reflection to load classes dynamically
  3. Jakarta Validation: Annotations like @Pattern internally use nested classes (e.g. Pattern$Flag) that are not detected by static analysis

Implemented solution

We solved it with a Quarkus extension that forces the inclusion of the necessary classes (e.g.):

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerReflectionClasses(BuildProducer<ReflectiveClassBuildItem> producer) {
producer.produce(
new FeatureBuildItem("core-validator")
);
...
}

Best practices to avoid the problem

  1. Test early: Run native tests during development
  2. Profile Reflection: Use quarkus.native.enable-reports=true to analyze loaded classes
  3. Extension-first: Prefer Quarkus extensions to manual solutions

Conclusions

Native builds require a different approach to dependency management. This specific case shows how even standard libraries can cause unexpected problems. The Quarkus extension solution turned out to be the most maintainable and scalable for our use case.

Related Posts

Clean Code: L’Economia dei Commenti [4/12]

Clean Code: L’Economia dei Commenti [4/12]

Quando usare i commenti e come evitarne l’eccesso, puntando su un codice autoesplicativo.

Implementazione del Cross Charge su Web Application legacy  Java Clann

Implementazione del Cross Charge su Web Application legacy Java Clann

cross charge implementation

Clean Code: Manuale per un Codice di Qualità [1/12]

Clean Code: Manuale per un Codice di Qualità [1/12]

Esploriamo i principi fondamentali del clean code, introducendo le basi per scrivere software leggibile, manutenibile e scalabile.

TypeScript Narrowing: come migliorare la gestione dei tipi con Type Guards e Conditional Types

TypeScript Narrowing: come migliorare la gestione dei tipi con Type Guards e Conditional Types

Come sfruttare i Type Guard e i Conditional Types per semplificare il codice e migliorare la manutenibilità in TypeScript

L'informatica, specchio della scienza anarchica di Feyerabend e di Lakatos: prima parte

L'informatica, specchio della scienza anarchica di Feyerabend e di Lakatos: prima parte

L'essere come fondamento dell'informatica

Software Development Project

Software Development Project

Custom solution

Best of R&D Proxy in 2023

Best of R&D Proxy in 2023

From Be Part of IT Magazine 2023

Best of (Confidential) in 2023

Best of (Confidential) in 2023

From Be Part of IT Magazine 2023