From 8062350cc01019f79362b90014eb6c52910dbadc Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Thu, 11 Jan 2024 16:41:20 +0100 Subject: [PATCH] HHH-17643 Load `BytecodeProvider` as a java service Also allow `SerializableProxy` deserialization even when no session factory is available. --- .../internal/BytecodeProviderInitiator.java | 44 +++++- .../bytecode/spi/BytecodeProvider.java | 2 + .../org/hibernate/cfg/BytecodeSettings.java | 5 + .../pojo/bytebuddy/SerializableProxy.java | 31 ++-- ...rg.hibernate.bytecode.spi.BytecodeProvider | 1 + ...roxySerializationNoSessionFactoryTest.java | 143 ++++++++++++++++++ 6 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 hibernate-core/src/main/resources/META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/serialization/ProxySerializationNoSessionFactoryTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeProviderInitiator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeProviderInitiator.java index 243852abaf..9bcfd20a9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeProviderInitiator.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/BytecodeProviderInitiator.java @@ -6,23 +6,40 @@ */ package org.hibernate.bytecode.internal; +import java.util.Collection; +import java.util.Iterator; import java.util.Map; +import java.util.ServiceLoader; import org.hibernate.Internal; import org.hibernate.boot.registry.StandardServiceInitiator; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.bytecode.spi.BytecodeProvider; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.util.config.ConfigurationHelper; import org.hibernate.service.spi.ServiceRegistryImplementor; import org.jboss.logging.Logger; -import static org.hibernate.cfg.BytecodeSettings.BYTECODE_PROVIDER; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; public final class BytecodeProviderInitiator implements StandardServiceInitiator { + /** + * @deprecated Register a {@link BytecodeProvider} through Java {@linkplain java.util.ServiceLoader services}. + */ + @Deprecated( forRemoval = true ) public static final String BYTECODE_PROVIDER_NAME_BYTEBUDDY = "bytebuddy"; + + /** + * @deprecated Register a {@link BytecodeProvider} through Java {@linkplain java.util.ServiceLoader services}. + */ + @Deprecated( forRemoval = true ) public static final String BYTECODE_PROVIDER_NAME_NONE = "none"; + + /** + * @deprecated Deprecated with no replacement + */ + @Deprecated( forRemoval = true ) public static final String BYTECODE_PROVIDER_NAME_DEFAULT = BYTECODE_PROVIDER_NAME_BYTEBUDDY; /** @@ -32,8 +49,9 @@ public final class BytecodeProviderInitiator implements StandardServiceInitiator @Override public BytecodeProvider initiateService(Map configurationValues, ServiceRegistryImplementor registry) { - String provider = ConfigurationHelper.getString( BYTECODE_PROVIDER, configurationValues, BYTECODE_PROVIDER_NAME_DEFAULT ); - return buildBytecodeProvider( provider ); + final ClassLoaderService classLoaderService = castNonNull( registry.getService( ClassLoaderService.class ) ); + final Collection bytecodeProviders = classLoaderService.loadJavaServices( BytecodeProvider.class ); + return getBytecodeProvider( bytecodeProviders ); } @Override @@ -43,12 +61,26 @@ public final class BytecodeProviderInitiator implements StandardServiceInitiator @Internal public static BytecodeProvider buildDefaultBytecodeProvider() { - return buildBytecodeProvider( BYTECODE_PROVIDER_NAME_BYTEBUDDY ); + return getBytecodeProvider( ServiceLoader.load( BytecodeProvider.class ) ); + } + + @Internal + public static BytecodeProvider getBytecodeProvider(Iterable bytecodeProviders) { + final Iterator iterator = bytecodeProviders.iterator(); + if ( !iterator.hasNext() ) { + // If no BytecodeProvider service is available, default to the "no-op" enhancer + return new org.hibernate.bytecode.internal.none.BytecodeProviderImpl(); + } + + final BytecodeProvider provider = iterator.next(); + if ( iterator.hasNext() ) { + throw new IllegalStateException( "Found multiple BytecodeProvider service registrations, cannot determine which one to use" ); + } + return provider; } @Internal public static BytecodeProvider buildBytecodeProvider(String providerName) { - CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, BytecodeProviderInitiator.class.getName() ); LOG.bytecodeProvider( providerName ); diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/spi/BytecodeProvider.java b/hibernate-core/src/main/java/org/hibernate/bytecode/spi/BytecodeProvider.java index 2b9c336535..077c9e5869 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/spi/BytecodeProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/spi/BytecodeProvider.java @@ -11,6 +11,7 @@ import java.util.Map; import org.hibernate.bytecode.enhance.spi.EnhancementContext; import org.hibernate.bytecode.enhance.spi.Enhancer; import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.service.JavaServiceLoadable; import org.hibernate.service.Service; import org.checkerframework.checker.nullness.qual.Nullable; @@ -25,6 +26,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; * * @author Steve Ebersole */ +@JavaServiceLoadable public interface BytecodeProvider extends Service { /** * Retrieve the specific factory for this provider capable of diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java index 1f1b4efdcb..df5d7801e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/BytecodeSettings.java @@ -20,7 +20,12 @@ public interface BytecodeSettings { * At present only bytebuddy is supported, bytebuddy being the default since version 5.3. * * @settingDefault {@code "bytebuddy"} + * @deprecated Will be removed, Hibernate ORM will use the BytecodeProvider implementation it finds on the + * classpath loading it via the standard ServiceLoader mechanism. Currently, there is only a single + * implementation which is included in Hibernate ORM, so it's not possible to override this. + * See HHH-17643 */ + @Deprecated( forRemoval = true ) String BYTECODE_PROVIDER = "hibernate.bytecode.provider"; /** diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/SerializableProxy.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/SerializableProxy.java index ad0113ac92..f34d8eb631 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/SerializableProxy.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/SerializableProxy.java @@ -17,6 +17,8 @@ import org.hibernate.proxy.AbstractSerializableProxy; import org.hibernate.proxy.HibernateProxy; import org.hibernate.type.CompositeType; +import static org.hibernate.bytecode.internal.BytecodeProviderInitiator.buildDefaultBytecodeProvider; + public final class SerializableProxy extends AbstractSerializableProxy { private final Class persistentClass; private final Class[] interfaces; @@ -30,6 +32,8 @@ public final class SerializableProxy extends AbstractSerializableProxy { private final CompositeType componentIdType; + private static volatile BytecodeProviderImpl fallbackBytecodeProvider; + public SerializableProxy( String entityName, Class persistentClass, @@ -120,17 +124,27 @@ public final class SerializableProxy extends AbstractSerializableProxy { private static SessionFactoryImplementor retrieveMatchingSessionFactory(final String sessionFactoryUuid, final String sessionFactoryName) { Objects.requireNonNull( sessionFactoryUuid ); - final SessionFactoryImplementor sessionFactory = SessionFactoryRegistry.INSTANCE.findSessionFactory( sessionFactoryUuid, sessionFactoryName ); - if ( sessionFactory != null ) { - return sessionFactory; - } - else { - throw new IllegalStateException( "Could not identify any active SessionFactory having UUID " + sessionFactoryUuid ); - } + return SessionFactoryRegistry.INSTANCE.findSessionFactory( sessionFactoryUuid, sessionFactoryName ); } private static BytecodeProviderImpl retrieveByteBuddyBytecodeProvider(final SessionFactoryImplementor sessionFactory) { - final BytecodeProvider bytecodeProvider = sessionFactory.getServiceRegistry().getService( BytecodeProvider.class ); + if ( sessionFactory == null ) { + // When the session factory is not available fallback to local bytecode provider + return getFallbackBytecodeProvider(); + } + + return castBytecodeProvider( sessionFactory.getServiceRegistry().getService( BytecodeProvider.class ) ); + } + + private static BytecodeProviderImpl getFallbackBytecodeProvider() { + BytecodeProviderImpl provider = fallbackBytecodeProvider; + if ( provider == null ) { + provider = fallbackBytecodeProvider = castBytecodeProvider( buildDefaultBytecodeProvider() ); + } + return provider; + } + + private static BytecodeProviderImpl castBytecodeProvider(BytecodeProvider bytecodeProvider) { if ( bytecodeProvider instanceof BytecodeProviderImpl ) { return (BytecodeProviderImpl) bytecodeProvider; } @@ -138,5 +152,4 @@ public final class SerializableProxy extends AbstractSerializableProxy { throw new IllegalStateException( "Unable to deserialize a SerializableProxy proxy: the bytecode provider is not ByteBuddy." ); } } - } diff --git a/hibernate-core/src/main/resources/META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider b/hibernate-core/src/main/resources/META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider new file mode 100644 index 0000000000..a2e4f6d2a5 --- /dev/null +++ b/hibernate-core/src/main/resources/META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider @@ -0,0 +1 @@ +org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/serialization/ProxySerializationNoSessionFactoryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/serialization/ProxySerializationNoSessionFactoryTest.java new file mode 100644 index 0000000000..721884c88d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/serialization/ProxySerializationNoSessionFactoryTest.java @@ -0,0 +1,143 @@ +/* + * 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 http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.serialization; + +import java.io.Serializable; + +import org.hibernate.Hibernate; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.internal.SessionFactoryRegistry; +import org.hibernate.internal.util.SerializationHelper; +import org.hibernate.proxy.HibernateProxy; + +import org.hibernate.testing.junit4.BaseUnitTestCase; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Marco Belladelli + */ +public class ProxySerializationNoSessionFactoryTest extends BaseUnitTestCase { + @Test + public void testUninitializedProxy() { + executeTest( false ); + } + + @Test + public void testInitializedProxy() { + executeTest( true ); + } + + private void executeTest(boolean initializeProxy) { + final Configuration cfg = new Configuration() + .setProperty( AvailableSettings.HBM2DDL_AUTO, "create-drop" ) + .addAnnotatedClass( SimpleEntity.class ) + .addAnnotatedClass( ChildEntity.class ); + ServiceRegistryUtil.applySettings( cfg.getStandardServiceRegistryBuilder() ); + final SimpleEntity parent; + try (final SessionFactory factory = cfg.buildSessionFactory()) { + doInHibernate( () -> factory, session -> { + final SimpleEntity entity = new SimpleEntity(); + entity.setId( 1L ); + entity.setName( "TheParent" ); + session.persist( entity ); + + final ChildEntity child = new ChildEntity(); + child.setId( 1L ); + child.setParent( entity ); + session.persist( child ); + } ); + + parent = doInHibernate( () -> factory, session -> { + final ChildEntity childEntity = session.find( ChildEntity.class, 1L ); + final SimpleEntity entity = childEntity.getParent(); + if ( initializeProxy ) { + assertEquals( "TheParent",entity.getName() ); + } + return entity; + } ); + } + + // The session factory is not available anymore + assertFalse( SessionFactoryRegistry.INSTANCE.hasRegistrations() ); + + assertTrue( parent instanceof HibernateProxy ); + assertEquals( initializeProxy, Hibernate.isInitialized( parent ) ); + + // Serialization and deserialization should still work + final SimpleEntity clone = (SimpleEntity) SerializationHelper.clone( parent ); + assertNotNull( clone ); + assertEquals( parent.getId(), clone.getId() ); + if ( initializeProxy ) { + assertEquals( parent.getName(), clone.getName() ); + } + } + + @Entity( name = "SimpleEntity" ) + static class SimpleEntity implements Serializable { + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(final Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntity" ) + static class ChildEntity { + @Id + private Long id; + + @ManyToOne( fetch = FetchType.LAZY ) + @JoinColumn + private SimpleEntity parent; + + public Long getId() { + return id; + } + + public void setId(final Long id) { + this.id = id; + } + + public SimpleEntity getParent() { + return parent; + } + + public void setParent(SimpleEntity parent) { + this.parent = parent; + } + } +}