HHH-16529 - Verify the version used for enhancement against the version being used

https://hibernate.atlassian.net/browse/HHH-16529
This commit is contained in:
Steve Ebersole 2023-08-15 14:15:52 -05:00
parent ab4ac5a64e
commit c5ccae1b90
5 changed files with 204 additions and 8 deletions

View File

@ -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;
}
}

View File

@ -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<EnhancementInfo> 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<EnhancementInfo> annotation) {
return annotation.load().version();
}
private PersistentAttributeTransformer createTransformer(TypeDescription typeDescription) {
return PersistentAttributeTransformer.collectPersistentFields( typeDescription, enhancementContext, typePool );
}

View File

@ -81,7 +81,7 @@ public final class ByteBuddyState {
private final TypeCache<TypeCache.SimpleKey> proxyCache;
private final TypeCache<TypeCache.SimpleKey> basicProxyCache;
ByteBuddyState() {
public ByteBuddyState() {
this( ClassFileVersion.ofThisVm( ClassFileVersion.JAVA_V11 ) );
}

View File

@ -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) {
}
}

View File

@ -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
}
}
}