From 5977c67b3bed4301c21f4a6408b6f0ce84a305db Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 22 Oct 2025 12:38:03 +0200 Subject: [PATCH] HHH-19887 Construct FormatMapper with context for AggregatedClassLoader access --- .../SessionFactoryOptionsBuilder.java | 92 +++++++++++++------ .../format/FormatMapperCreationContext.java | 20 ++++ .../format/jackson/JacksonIntegration.java | 68 ++++++++++++-- .../jackson/JacksonJsonFormatMapper.java | 20 +++- .../jackson/JacksonXmlFormatMapper.java | 24 ++++- 5 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/type/format/FormatMapperCreationContext.java diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index 85008f7ccd40..b65f8a5282ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -7,6 +7,7 @@ package org.hibernate.boot.internal; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; @@ -70,6 +71,7 @@ import org.hibernate.resource.transaction.spi.TransactionCoordinatorBuilder; import org.hibernate.stat.Statistics; import org.hibernate.type.format.FormatMapper; +import org.hibernate.type.format.FormatMapperCreationContext; import org.hibernate.type.format.jackson.JacksonIntegration; import org.hibernate.type.format.jakartajson.JakartaJsonIntegration; import org.hibernate.type.format.jaxb.JaxbXmlFormatMapper; @@ -313,13 +315,23 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo AvailableSettings.JPA_VALIDATION_FACTORY, configurationSettings.get( AvailableSettings.JAKARTA_VALIDATION_FACTORY ) ); - this.jsonFormatMapper = determineJsonFormatMapper( + + final var formatMapperCreationContext = new FormatMapperCreationContext() { + @Override + public BootstrapContext getBootstrapContext() { + return context; + } + }; + jsonFormatMapper = jsonFormatMapper( configurationSettings.get( AvailableSettings.JSON_FORMAT_MAPPER ), - strategySelector + strategySelector, + formatMapperCreationContext ); - this.xmlFormatMapper = determineXmlFormatMapper( + + xmlFormatMapper = xmlFormatMapper( configurationSettings.get( AvailableSettings.XML_FORMAT_MAPPER ), - strategySelector + strategySelector, + formatMapperCreationContext ); this.sessionFactoryName = (String) configurationSettings.get( SESSION_FACTORY_NAME ); @@ -827,7 +839,7 @@ private static Supplier interceptorSupplier(Class configurationSettings, + Map configurationSettings, StandardServiceRegistry serviceRegistry) { final PhysicalConnectionHandlingMode specifiedHandlingMode = PhysicalConnectionHandlingMode.interpret( configurationSettings.get( CONNECTION_HANDLING ) @@ -840,36 +852,62 @@ private PhysicalConnectionHandlingMode interpretConnectionHandlingMode( return serviceRegistry.requireService( TransactionCoordinatorBuilder.class ).getDefaultConnectionHandlingMode(); } - private static FormatMapper determineJsonFormatMapper(Object setting, StrategySelector strategySelector) { - return strategySelector.resolveDefaultableStrategy( - FormatMapper.class, + private static FormatMapper jsonFormatMapper(Object setting, StrategySelector selector, FormatMapperCreationContext creationContext) { + return formatMapper( setting, - (Callable) () -> { - final FormatMapper jsonJacksonFormatMapper = JacksonIntegration.getJsonJacksonFormatMapperOrNull(); - if (jsonJacksonFormatMapper != null) { - return jsonJacksonFormatMapper; - } - else { - return JakartaJsonIntegration.getJakartaJsonBFormatMapperOrNull(); - } - } + selector, + () -> { + final FormatMapper jsonJacksonFormatMapper = JacksonIntegration.getJsonJacksonFormatMapperOrNull( creationContext ); + return jsonJacksonFormatMapper != null + ? jsonJacksonFormatMapper + : JakartaJsonIntegration.getJakartaJsonBFormatMapperOrNull(); + }, + creationContext ); } - private static FormatMapper determineXmlFormatMapper(Object setting, StrategySelector strategySelector) { - return strategySelector.resolveDefaultableStrategy( - FormatMapper.class, + private static FormatMapper xmlFormatMapper(Object setting, StrategySelector selector, FormatMapperCreationContext creationContext) { + return formatMapper( setting, - (Callable) () -> { - final FormatMapper jacksonFormatMapper = JacksonIntegration.getXMLJacksonFormatMapperOrNull(); - if (jacksonFormatMapper != null) { - return jacksonFormatMapper; - } - return new JaxbXmlFormatMapper(); - } + selector, + () -> { + final FormatMapper jacksonFormatMapper = JacksonIntegration.getXMLJacksonFormatMapperOrNull( creationContext ); + return jacksonFormatMapper != null + ? jacksonFormatMapper + : new JaxbXmlFormatMapper(); + }, + creationContext ); } + private static FormatMapper formatMapper(Object setting, StrategySelector selector, Callable defaultResolver, FormatMapperCreationContext creationContext) { + return selector.resolveStrategy( FormatMapper.class, setting, defaultResolver, strategyClass -> { + try { + final Constructor creationContextConstructor = + strategyClass.getDeclaredConstructor( FormatMapperCreationContext.class ); + return creationContextConstructor.newInstance( creationContext ); + } + catch (NoSuchMethodException e) { + // Ignore + } + catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new StrategySelectionException( + String.format( "Could not instantiate named strategy class [%s]", strategyClass.getName() ), + e + ); + } + try { + return strategyClass.getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + throw new StrategySelectionException( + String.format( "Could not instantiate named strategy class [%s]", strategyClass.getName() ), + e + ); + } + } ); + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SessionFactoryOptionsState diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/FormatMapperCreationContext.java b/hibernate-core/src/main/java/org/hibernate/type/format/FormatMapperCreationContext.java new file mode 100644 index 000000000000..ae0f803ed975 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/format/FormatMapperCreationContext.java @@ -0,0 +1,20 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.format; + +import org.hibernate.Incubating; +import org.hibernate.boot.spi.BootstrapContext; + + +/** + * The creation context for {@link FormatMapper} that is passed as constructor argument to implementations. + */ +@Incubating +public interface FormatMapperCreationContext { + BootstrapContext getBootstrapContext(); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java index 10460b012e51..34c09e0b4253 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java @@ -6,7 +6,9 @@ */ package org.hibernate.type.format.jackson; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.type.format.FormatMapper; +import org.hibernate.type.format.FormatMapperCreationContext; public final class JacksonIntegration { @@ -14,8 +16,7 @@ public final class JacksonIntegration { // when GraalVM native image is initializing them. private static final boolean JACKSON_XML_AVAILABLE = ableToLoadJacksonXMLMapper(); private static final boolean JACKSON_JSON_AVAILABLE = ableToLoadJacksonJSONMapper(); - private static final JacksonXmlFormatMapper XML_FORMAT_MAPPER = JACKSON_XML_AVAILABLE ? new JacksonXmlFormatMapper() : null; - private static final JacksonJsonFormatMapper JSON_FORMAT_MAPPER = JACKSON_JSON_AVAILABLE ? new JacksonJsonFormatMapper() : null; + private JacksonIntegration() { //To not be instantiated: static helpers only @@ -29,12 +30,67 @@ private static boolean ableToLoadJacksonXMLMapper() { return canLoad( "com.fasterxml.jackson.dataformat.xml.XmlMapper" ); } - public static FormatMapper getXMLJacksonFormatMapperOrNull() { - return XML_FORMAT_MAPPER; + /** + * Checks that Jackson is available and that we have the Oracle OSON extension available + * in the classpath. + * @return true if we can load the OSON support, false otherwise. + */ + private static boolean ableToLoadJacksonOSONFactory() { + return ableToLoadJacksonJSONMapper() && + canLoad( "oracle.jdbc.provider.oson.OsonFactory" ); + } + + public static @Nullable FormatMapper getXMLJacksonFormatMapperOrNull(FormatMapperCreationContext creationContext) { + return JACKSON_XML_AVAILABLE + ? createFormatMapper( "org.hibernate.type.format.jackson.JacksonXmlFormatMapper", creationContext ) + : null; } - public static FormatMapper getJsonJacksonFormatMapperOrNull() { - return JSON_FORMAT_MAPPER; + public static @Nullable FormatMapper getJsonJacksonFormatMapperOrNull(FormatMapperCreationContext creationContext) { + return JACKSON_JSON_AVAILABLE + ? createFormatMapper( "org.hibernate.type.format.jackson.JacksonJsonFormatMapper", creationContext ) + : null; + } + + public static @Nullable FormatMapper getXMLJacksonFormatMapperOrNull(boolean legacyFormat) { + if ( JACKSON_XML_AVAILABLE ) { + try { + final Class formatMapperClass = JacksonIntegration.class.getClassLoader() + .loadClass( "org.hibernate.type.format.jackson.JacksonXmlFormatMapper" ); + return (FormatMapper) formatMapperClass.getDeclaredConstructor( boolean.class ) + .newInstance( legacyFormat ); + } + catch (Exception e) { + throw new RuntimeException( "Couldn't instantiate Jackson XML FormatMapper", e ); + } + } + return null; + } + + public static @Nullable FormatMapper getJsonJacksonFormatMapperOrNull() { + return JACKSON_JSON_AVAILABLE + ? createFormatMapper( "org.hibernate.type.format.jackson.JacksonJsonFormatMapper", null ) + : null; + } + + private static FormatMapper createFormatMapper(String className, @Nullable FormatMapperCreationContext creationContext) { + try { + if ( creationContext == null ) { + final Class formatMapperClass = JacksonIntegration.class.getClassLoader() + .loadClass( className ); + return (FormatMapper) formatMapperClass.getDeclaredConstructor().newInstance(); + } + else { + return (FormatMapper) creationContext.getBootstrapContext() + .getClassLoaderAccess() + .classForName( className ) + .getDeclaredConstructor( FormatMapperCreationContext.class ) + .newInstance( creationContext ); + } + } + catch (Exception e) { + throw new RuntimeException( "Couldn't instantiate Jackson FormatMapper", e ); + } } private static boolean canLoad(String name) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java index fe4b41b4f9b9..fac5eb307a7b 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java @@ -6,12 +6,17 @@ */ package org.hibernate.type.format.jackson; +import com.fasterxml.jackson.databind.Module; + +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.type.format.AbstractJsonFormatMapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.hibernate.type.format.FormatMapperCreationContext; import java.lang.reflect.Type; +import java.util.List; /** * @author Christian Beikov @@ -24,7 +29,20 @@ public final class JacksonJsonFormatMapper extends AbstractJsonFormatMapper { private final ObjectMapper objectMapper; public JacksonJsonFormatMapper() { - this(new ObjectMapper().findAndRegisterModules()); + this( ObjectMapper.findModules( JacksonJsonFormatMapper.class.getClassLoader() ) ); + } + + public JacksonJsonFormatMapper(FormatMapperCreationContext creationContext) { + this( + creationContext.getBootstrapContext() + .getServiceRegistry() + .requireService( ClassLoaderService.class ) + .>workWithClassLoader( ObjectMapper::findModules ) + ); + } + + private JacksonJsonFormatMapper(List modules) { + this(new ObjectMapper().registerModules( modules )); } public JacksonJsonFormatMapper(ObjectMapper objectMapper) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java index a463d5903565..2b5e03233c3c 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java @@ -8,8 +8,12 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.util.List; import java.util.ArrayList; +import com.fasterxml.jackson.databind.Module; + +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.format.FormatMapper; @@ -24,6 +28,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import org.hibernate.type.format.FormatMapperCreationContext; /** * @author Christian Beikov @@ -35,17 +40,30 @@ public final class JacksonXmlFormatMapper implements FormatMapper { private final ObjectMapper objectMapper; public JacksonXmlFormatMapper() { - this( createXmlMapper() ); + this( + createXmlMapper( XmlMapper.findModules( JacksonXmlFormatMapper.class.getClassLoader() ) ) + ); + } + + public JacksonXmlFormatMapper(FormatMapperCreationContext creationContext) { + this( + createXmlMapper( + creationContext.getBootstrapContext() + .getServiceRegistry() + .requireService( ClassLoaderService.class ) + .>workWithClassLoader( XmlMapper::findModules ) + ) + ); } public JacksonXmlFormatMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } - private static XmlMapper createXmlMapper() { + private static XmlMapper createXmlMapper(List modules) { final XmlMapper xmlMapper = new XmlMapper(); // needed to automatically find and register Jackson's jsr310 module for java.time support - xmlMapper.findAndRegisterModules(); + xmlMapper.registerModules( modules ); xmlMapper.configure( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false ); xmlMapper.enable( ToXmlGenerator.Feature.WRITE_NULLS_AS_XSI_NIL ); // Workaround for null vs empty string handling inside arrays,