From c5ccae1b908eee2a10801537703cdbf8ee97a004 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Tue, 15 Aug 2023 14:15:52 -0500 Subject: [PATCH] HHH-16529 - Verify the version used for enhancement against the version being used https://hibernate.atlassian.net/browse/HHH-16529 --- .../enhance/VersionMismatchException.java | 56 +++++++++++++++++ .../internal/bytebuddy/EnhancerImpl.java | 46 +++++++++++--- .../internal/bytebuddy/ByteBuddyState.java | 2 +- .../enhance/version/SimpleEntity.java | 62 +++++++++++++++++++ .../enhance/version/VersionMismatchTests.java | 46 ++++++++++++++ 5 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/bytecode/enhance/VersionMismatchException.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/SimpleEntity.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/VersionMismatchTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/VersionMismatchException.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/VersionMismatchException.java new file mode 100644 index 0000000000..52a104fcc8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/VersionMismatchException.java @@ -0,0 +1,56 @@ +/* + * 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.bytecode.enhance; + +import java.util.Locale; + +import org.hibernate.bytecode.enhance.spi.EnhancementException; + +import net.bytebuddy.description.type.TypeDescription; + +/** + * Indicates that the version of Hibernate used to enhance + * a class is different from the version being used at runtime. + * + * @author Steve Ebersole + */ +public class VersionMismatchException extends EnhancementException { + private final String typeName; + private final String enhancementVersion; + private final String runtimeVersion; + + public VersionMismatchException( + TypeDescription typeDescription, + String enhancementVersion, + String runtimeVersion) { + super( + String.format( + Locale.ROOT, + "Mismatch between Hibernate version used for bytecode enhancement (%s) and runtime (%s) for `%s`", + enhancementVersion, + runtimeVersion, + typeDescription.getName() + ) + ); + + this.typeName = typeDescription.getName(); + this.enhancementVersion = enhancementVersion; + this.runtimeVersion = runtimeVersion; + } + + public String getTypeName() { + return typeName; + } + + public String getEnhancementVersion() { + return enhancementVersion; + } + + public String getRuntimeVersion() { + return runtimeVersion; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java index 0f5bd82c65..0448773417 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java @@ -6,8 +6,6 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; -import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer; - import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; @@ -18,11 +16,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import jakarta.persistence.Access; -import jakarta.persistence.AccessType; -import jakarta.persistence.Transient; - import org.hibernate.Version; +import org.hibernate.bytecode.enhance.VersionMismatchException; import org.hibernate.bytecode.enhance.internal.tracker.CompositeOwnerTracker; import org.hibernate.bytecode.enhance.internal.tracker.DirtyTracker; import org.hibernate.bytecode.enhance.spi.CollectionTracker; @@ -48,6 +43,9 @@ import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Transient; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.annotation.AnnotationDescription; import net.bytebuddy.description.annotation.AnnotationList; @@ -67,6 +65,8 @@ import net.bytebuddy.implementation.Implementation; import net.bytebuddy.implementation.StubMethod; import net.bytebuddy.pool.TypePool; +import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer; + public class EnhancerImpl implements Enhancer { private static final CoreMessageLogger log = CoreLogging.messageLogger( Enhancer.class ); @@ -153,6 +153,9 @@ public class EnhancerImpl implements Enhancer { typeDescription ) ); } + catch (EnhancementException e) { + throw e; + } catch (RuntimeException e) { throw new EnhancementException( "Failed to enhance class " + className, e ); } @@ -168,13 +171,17 @@ public class EnhancerImpl implements Enhancer { log.debugf( "Skipping enhancement of [%s]: it's an interface", managedCtClass.getName() ); return null; } + // can't effectively enhance records if ( managedCtClass.isRecord() ) { log.debugf( "Skipping enhancement of [%s]: it's a record", managedCtClass.getName() ); return null; } - // skip already enhanced classes + + // handle already enhanced classes if ( alreadyEnhanced( managedCtClass ) ) { + verifyVersions( managedCtClass, enhancementContext ); + log.debugf( "Skipping enhancement of [%s]: already enhanced", managedCtClass.getName() ); return null; } @@ -380,6 +387,31 @@ public class EnhancerImpl implements Enhancer { } } + private static void verifyVersions(TypeDescription managedCtClass, ByteBuddyEnhancementContext enhancementContext) { + final AnnotationDescription.Loadable existingInfo = managedCtClass + .getDeclaredAnnotations() + .ofType( EnhancementInfo.class ); + if ( existingInfo == null ) { + // There is an edge case here where a user manually adds `implement Managed` to + // their domain class, in which case there will most likely not be a + // `EnhancementInfo` annotation. Such cases should simply not do version checking. + // + // However, there is also ambiguity in this case with classes that were enhanced + // with old versions of Hibernate which did not add that annotation as part of + // enhancement. But overall we consider this condition to be acceptable + return; + } + + final String enhancementVersion = extractVersion( existingInfo ); + if ( !Version.getVersionString().equals( enhancementVersion ) ) { + throw new VersionMismatchException( managedCtClass, enhancementVersion, Version.getVersionString() ); + } + } + + private static String extractVersion(AnnotationDescription.Loadable annotation) { + return annotation.load().version(); + } + private PersistentAttributeTransformer createTransformer(TypeDescription typeDescription) { return PersistentAttributeTransformer.collectPersistentFields( typeDescription, enhancementContext, typePool ); } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java index 333f331593..bc3b0779da 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java @@ -81,7 +81,7 @@ public final class ByteBuddyState { private final TypeCache proxyCache; private final TypeCache basicProxyCache; - ByteBuddyState() { + public ByteBuddyState() { this( ClassFileVersion.ofThisVm( ClassFileVersion.JAVA_V11 ) ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/SimpleEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/SimpleEntity.java new file mode 100644 index 0000000000..e32cf99e9c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/SimpleEntity.java @@ -0,0 +1,62 @@ +/* + * 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.bytecode.enhance.version; + +import org.hibernate.bytecode.enhance.spi.EnhancementInfo; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.ManagedEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity(name = "SimpleEntity") +@Table(name = "SimpleEntity") +@EnhancementInfo(version = "5.3.0.Final") +public class SimpleEntity implements ManagedEntity { + @Id + private Integer id; + private String name; + + @Override + public Object $$_hibernate_getEntityInstance() { + return null; + } + + @Override + public EntityEntry $$_hibernate_getEntityEntry() { + return null; + } + + @Override + public void $$_hibernate_setEntityEntry(EntityEntry entityEntry) { + + } + + @Override + public ManagedEntity $$_hibernate_getPreviousManagedEntity() { + return null; + } + + @Override + public void $$_hibernate_setPreviousManagedEntity(ManagedEntity previous) { + + } + + @Override + public ManagedEntity $$_hibernate_getNextManagedEntity() { + return null; + } + + @Override + public void $$_hibernate_setNextManagedEntity(ManagedEntity next) { + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/VersionMismatchTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/VersionMismatchTests.java new file mode 100644 index 0000000000..53632bda1e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/version/VersionMismatchTests.java @@ -0,0 +1,46 @@ +/* + * 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.bytecode.enhance.version; + +import java.io.IOException; +import java.io.InputStream; + +import org.hibernate.bytecode.enhance.VersionMismatchException; +import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerImpl; +import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext; +import org.hibernate.bytecode.enhance.spi.Enhancer; +import org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState; +import org.hibernate.bytecode.spi.ByteCodeHelper; + +import org.hibernate.testing.orm.junit.Jira; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-16529" ) +public class VersionMismatchTests { + @Test + void testVersionMismatch() throws IOException { + final DefaultEnhancementContext enhancementContext = new DefaultEnhancementContext(); + final ByteBuddyState byteBuddyState = new ByteBuddyState(); + final Enhancer enhancer = new EnhancerImpl( enhancementContext, byteBuddyState ); + + final String classFile = SimpleEntity.class.getName().replace( '.', '/' ) + ".class"; + try (InputStream classFileStream = SimpleEntity.class.getClassLoader().getResourceAsStream( classFile )) { + final byte[] originalBytes = ByteCodeHelper.readByteCode( classFileStream ); + enhancer.enhance( SimpleEntity.class.getName(), originalBytes ); + fail( "Expecting failure" ); + } + catch (VersionMismatchException expected) { + // expected + } + } + +}