Skip to content

Commit abfe6b9

Browse files
committed
HHH-18833 Introduce EnhancementContext#getUnsupportedEnhancementStrategy
This method allows custom contexts to pick the behavior they want when a class contains getters/setters that do not have a matching field, making enhancement impossible. Three behaviors are available: * SKIP (the default), which will skip enhancement of such classes. * FAIL, which will throw an exception upon encountering such classes. * LEGACY, which will restore the pre-HHH-16572 behavior. I do not think LEGACY is useful at the moment, but I wanted to have that option in case it turns out HHH-16572 does more harm than good in Quarkus 3.15.
1 parent 0ceada1 commit abfe6b9

File tree

5 files changed

+225
-8
lines changed

5 files changed

+225
-8
lines changed

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import net.bytebuddy.dynamic.scaffold.MethodGraph;
2525
import net.bytebuddy.matcher.ElementMatcher;
2626
import net.bytebuddy.pool.TypePool;
27+
import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy;
2728

2829
import static net.bytebuddy.matcher.ElementMatchers.isGetter;
2930

@@ -106,6 +107,10 @@ public void registerDiscoveredType(TypeDescription typeDescription, Type.Persist
106107
enhancementContext.registerDiscoveredType( new UnloadedTypeDescription( typeDescription ), type );
107108
}
108109

110+
public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() {
111+
return enhancementContext.getUnsupportedEnhancementStrategy();
112+
}
113+
109114
public void discoverCompositeTypes(TypeDescription managedCtClass, TypePool typePool) {
110115
if ( isDiscoveredType( managedCtClass ) ) {
111116
return;

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import net.bytebuddy.implementation.FixedValue;
2626
import net.bytebuddy.implementation.Implementation;
2727
import net.bytebuddy.implementation.StubMethod;
28+
import org.hibernate.AssertionFailure;
2829
import org.hibernate.Version;
2930
import org.hibernate.bytecode.enhance.VersionMismatchException;
3031
import org.hibernate.bytecode.enhance.internal.tracker.CompositeOwnerTracker;
@@ -35,6 +36,7 @@
3536
import org.hibernate.bytecode.enhance.spi.Enhancer;
3637
import org.hibernate.bytecode.enhance.spi.EnhancerConstants;
3738
import org.hibernate.bytecode.enhance.spi.UnloadedField;
39+
import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy;
3840
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
3941
import org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState;
4042
import org.hibernate.engine.spi.CompositeOwner;
@@ -173,7 +175,7 @@ private DynamicType.Builder<?> doEnhance(Supplier<DynamicType.Builder<?>> builde
173175
}
174176

175177
if ( enhancementContext.isEntityClass( managedCtClass ) ) {
176-
if ( hasUnsupportedAttributeNaming( managedCtClass ) ) {
178+
if ( checkUnsupportedAttributeNaming( managedCtClass ) ) {
177179
// do not enhance classes with mismatched names for PROPERTY-access persistent attributes
178180
return null;
179181
}
@@ -337,7 +339,7 @@ private DynamicType.Builder<?> doEnhance(Supplier<DynamicType.Builder<?>> builde
337339
return createTransformer( managedCtClass ).applyTo( builder );
338340
}
339341
else if ( enhancementContext.isCompositeClass( managedCtClass ) ) {
340-
if ( hasUnsupportedAttributeNaming( managedCtClass ) ) {
342+
if ( checkUnsupportedAttributeNaming( managedCtClass ) ) {
341343
// do not enhance classes with mismatched names for PROPERTY-access persistent attributes
342344
return null;
343345
}
@@ -377,7 +379,7 @@ else if ( enhancementContext.isCompositeClass( managedCtClass ) ) {
377379
else if ( enhancementContext.isMappedSuperclassClass( managedCtClass ) ) {
378380

379381
// Check for HHH-16572 (PROPERTY attributes with mismatched field and method names)
380-
if ( hasUnsupportedAttributeNaming( managedCtClass ) ) {
382+
if ( checkUnsupportedAttributeNaming( managedCtClass ) ) {
381383
return null;
382384
}
383385

@@ -401,8 +403,22 @@ else if ( enhancementContext.doExtendedEnhancement( managedCtClass ) ) {
401403
* Check whether an entity class ({@code managedCtClass}) has mismatched names between a persistent field and its
402404
* getter/setter when using {@link AccessType#PROPERTY}, which Hibernate does not currently support for enhancement.
403405
* See https://hibernate.atlassian.net/browse/HHH-16572
406+
*
407+
* @return {@code true} if enhancement of the class must be {@link org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy#SKIP skipped}
408+
* because it has mismatched names.
409+
* {@code false} if enhancement of the class must proceed, either because it doesn't have any mismatched names,
410+
* or because {@link org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy#LEGACY legacy mode} was opted into.
411+
* @throws EnhancementException if enhancement of the class must {@link org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy#FAIL abort} because it has mismatched names.
404412
*/
405-
private boolean hasUnsupportedAttributeNaming(TypeDescription managedCtClass) {
413+
@SuppressWarnings("deprecation")
414+
private boolean checkUnsupportedAttributeNaming(TypeDescription managedCtClass) {
415+
var strategy = enhancementContext.getUnsupportedEnhancementStrategy();
416+
if ( UnsupportedEnhancementStrategy.LEGACY.equals( strategy ) ) {
417+
// Don't check anything and act as if there was nothing unsupported in the class.
418+
// This is unsafe but that's what LEGACY is about.
419+
return false;
420+
}
421+
406422
// For process access rules, See https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#default-access-type
407423
// and https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#a122
408424
//
@@ -464,10 +480,23 @@ else if (methodName.startsWith("get") ||
464480
}
465481
}
466482
}
467-
if (propertyHasAnnotation && !propertyNameMatchesFieldName) {
468-
log.debugf("Skipping enhancement of [%s]: due to class [%s] not having a property accessor method name matching field name [%s]",
469-
managedCtClass, methodDescription.getDeclaringType().getActualName(), methodFieldName);
470-
return true;
483+
if ( propertyHasAnnotation && !propertyNameMatchesFieldName ) {
484+
switch ( strategy ) {
485+
case SKIP:
486+
log.debugf(
487+
"Skipping enhancement of [%s] because no field named [%s] could be found for property accessor method [%s]."
488+
+ " To fix this, make sure all property accessor methods have a matching field.",
489+
managedCtClass.getName(), methodFieldName, methodDescription.getName() );
490+
return true;
491+
case FAIL:
492+
throw new EnhancementException( String.format(
493+
"Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s]."
494+
+ " To fix this, make sure all property accessor methods have a matching field.",
495+
managedCtClass.getName(), methodFieldName, methodDescription.getName() ) );
496+
default:
497+
// We shouldn't even be in this method if using LEGACY, see top of this method.
498+
throw new AssertionFailure( "Unexpected strategy at this point: " + strategy );
499+
}
471500
}
472501
}
473502
return false;

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package org.hibernate.bytecode.enhance.spi;
88

99
import jakarta.persistence.metamodel.Type;
10+
import org.hibernate.Incubating;
1011

1112
/**
1213
* The context for performing an enhancement. Enhancement can happen in any number of ways:<ul>
@@ -146,4 +147,15 @@ public interface EnhancementContext {
146147
boolean isDiscoveredType(UnloadedClass classDescriptor);
147148

148149
void registerDiscoveredType(UnloadedClass classDescriptor, Type.PersistenceType type);
150+
151+
/**
152+
* @return The expected behavior when encountering a class that cannot be enhanced,
153+
* in particular when attribute names don't match field names.
154+
* @see <a href="https://hibernate.atlassian.net/browse/HHH-16572">HHH-16572</a>
155+
* @see <a href="https://hibernate.atlassian.net/browse/HHH-18833">HHH-18833</a>
156+
*/
157+
@Incubating
158+
default UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() {
159+
return UnsupportedEnhancementStrategy.SKIP;
160+
}
149161
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.bytecode.enhance.spi;
8+
9+
import org.hibernate.Incubating;
10+
11+
/**
12+
* The expected behavior when encountering a class that cannot be enhanced,
13+
* in particular when attribute names don't match field names.
14+
*
15+
* @see org.hibernate.bytecode.enhance.spi.EnhancementContext#getUnsupportedEnhancementStrategy
16+
*/
17+
@Incubating
18+
public enum UnsupportedEnhancementStrategy {
19+
20+
/**
21+
* When a class cannot be enhanced, skip enhancement for that class only.
22+
*/
23+
SKIP,
24+
/**
25+
* When a class cannot be enhanced, throw an exception with an actionable message.
26+
*/
27+
FAIL,
28+
/**
29+
* Legacy behavior: when a class cannot be enhanced, ignore that fact and try to enhance it anyway.
30+
* <p>
31+
* <strong>This is utterly unsafe and may cause errors, unpredictable behavior, and data loss.</strong>
32+
* <p>
33+
* Intended only for internal use in contexts with rigid backwards compatibility requirements.
34+
*
35+
* @deprecated Use {@link #SKIP} or {@link #FAIL} instead.
36+
*/
37+
@Deprecated
38+
LEGACY
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.bytecode.enhancement.access;
6+
7+
import jakarta.persistence.Access;
8+
import jakarta.persistence.AccessType;
9+
import jakarta.persistence.Basic;
10+
import jakarta.persistence.Entity;
11+
import jakarta.persistence.Id;
12+
import jakarta.persistence.Table;
13+
import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerImpl;
14+
import org.hibernate.bytecode.enhance.spi.EnhancementContext;
15+
import org.hibernate.bytecode.enhance.spi.EnhancementException;
16+
import org.hibernate.bytecode.enhance.spi.Enhancer;
17+
import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy;
18+
import org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState;
19+
import org.hibernate.bytecode.spi.ByteCodeHelper;
20+
import org.hibernate.testing.bytecode.enhancement.EnhancerTestContext;
21+
import org.hibernate.testing.orm.junit.JiraKey;
22+
import org.junit.jupiter.api.Test;
23+
24+
import java.io.IOException;
25+
import java.io.InputStream;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
29+
30+
@JiraKey("HHH-18833")
31+
public class UnsupportedEnhancementStrategyTest {
32+
33+
@Test
34+
public void skip() throws IOException {
35+
var context = new EnhancerTestContext() {
36+
@Override
37+
public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() {
38+
// This is currently the default, but we don't care about what's the default here
39+
return UnsupportedEnhancementStrategy.SKIP;
40+
}
41+
};
42+
byte[] originalBytes = getAsBytes( SomeEntity.class );
43+
byte[] enhancedBytes = doEnhance( SomeEntity.class, originalBytes, context );
44+
assertThat( enhancedBytes ).isNull(); // null means "not enhanced"
45+
}
46+
47+
@Test
48+
public void fail() throws IOException {
49+
var context = new EnhancerTestContext() {
50+
@Override
51+
public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() {
52+
return UnsupportedEnhancementStrategy.FAIL;
53+
}
54+
};
55+
byte[] originalBytes = getAsBytes( SomeEntity.class );
56+
assertThatThrownBy( () -> doEnhance( SomeEntity.class, originalBytes, context ) )
57+
.isInstanceOf( EnhancementException.class )
58+
.hasMessageContainingAll(
59+
String.format(
60+
"Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s].",
61+
SomeEntity.class.getName(), "propertyMethod", "getPropertyMethod" ),
62+
"To fix this, make sure all property accessor methods have a matching field."
63+
);
64+
}
65+
66+
@Test
67+
@SuppressWarnings("deprecation")
68+
public void legacy() throws IOException {
69+
var context = new EnhancerTestContext() {
70+
@Override
71+
public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() {
72+
// This is currently the default, but we don't care about what's the default here
73+
return UnsupportedEnhancementStrategy.LEGACY;
74+
}
75+
};
76+
byte[] originalBytes = getAsBytes( SomeEntity.class );
77+
byte[] enhancedBytes = doEnhance( SomeEntity.class, originalBytes, context );
78+
assertThat( enhancedBytes ).isNotNull(); // non-null means enhancement _was_ performed
79+
}
80+
81+
private byte[] doEnhance(Class<SomeEntity> someEntityClass, byte[] originalBytes, EnhancementContext context) {
82+
final ByteBuddyState byteBuddyState = new ByteBuddyState();
83+
final Enhancer enhancer = new EnhancerImpl( context, byteBuddyState );
84+
return enhancer.enhance( someEntityClass.getName(), originalBytes );
85+
}
86+
87+
private byte[] getAsBytes(Class<?> clazz) throws IOException {
88+
final String classFile = clazz.getName().replace( '.', '/' ) + ".class";
89+
try (InputStream classFileStream = clazz.getClassLoader().getResourceAsStream( classFile )) {
90+
return ByteCodeHelper.readByteCode( classFileStream );
91+
}
92+
}
93+
94+
@Entity
95+
@Table(name = "SOME_ENTITY")
96+
static class SomeEntity {
97+
@Id
98+
Long id;
99+
100+
@Basic
101+
String field;
102+
103+
String property;
104+
105+
public SomeEntity() {
106+
}
107+
108+
public SomeEntity(Long id, String field, String property) {
109+
this.id = id;
110+
this.field = field;
111+
this.property = property;
112+
}
113+
114+
/**
115+
* The following property accessor methods are purposely named incorrectly to
116+
* not match the "property" field. The HHH-16572 change ensures that
117+
* this entity is not (bytecode) enhanced. Eventually further changes will be made
118+
* such that this entity is enhanced in which case the FailureExpected can be removed
119+
* and the cleanup() uncommented.
120+
*/
121+
@Basic
122+
@Access(AccessType.PROPERTY)
123+
public String getPropertyMethod() {
124+
return "from getter: " + property;
125+
}
126+
127+
public void setPropertyMethod(String property) {
128+
this.property = property;
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)