This is an automated email from the ASF dual-hosted git repository.
tandraschko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/openwebbeans.git
The following commit(s) were added to refs/heads/main by this push:
new 43ad9a719 OWB-1234 - generic classes are not correctly proxied when
bridge methods are involved
43ad9a719 is described below
commit 43ad9a719151bebfdb99dac83d95ba583b347f6a
Author: tandraschko <[email protected]>
AuthorDate: Tue Apr 14 18:12:51 2026 +0200
OWB-1234 - generic classes are not correctly proxied when bridge methods
are involved
---
.../webbeans/proxy/AbstractProxyFactory.java | 18 ++-
.../webbeans/proxy/NormalScopeProxyFactory.java | 6 +-
.../java/org/apache/webbeans/util/ClassUtil.java | 92 ++++++++++-
.../proxy/BridgeMethodNormalScopeProxyTest.java | 99 ++++++++++++
.../test/proxy/BridgeMethodOwb923And828Test.java | 180 +++++++++++++++++++++
.../apache/webbeans/test/util/ClassUtilTest.java | 43 +++++
6 files changed, 430 insertions(+), 8 deletions(-)
diff --git
a/webbeans-impl/src/main/java/org/apache/webbeans/proxy/AbstractProxyFactory.java
b/webbeans-impl/src/main/java/org/apache/webbeans/proxy/AbstractProxyFactory.java
index fddd32846..ac9c50671 100644
---
a/webbeans-impl/src/main/java/org/apache/webbeans/proxy/AbstractProxyFactory.java
+++
b/webbeans-impl/src/main/java/org/apache/webbeans/proxy/AbstractProxyFactory.java
@@ -463,12 +463,26 @@ public abstract class AbstractProxyFactory
- protected boolean unproxyableMethod(Method delegatedMethod)
+ /**
+ * Methods that must not be proxied (excluding the {@link
Method#isBridge()} rule).
+ * When a caller uses {@link
org.apache.webbeans.util.ClassUtil#getNonPrivateMethods(Class, boolean,
boolean)} with
+ * {@code includeJvmBridgeMethods == true}, use this for filtering — not
{@link #unproxyableMethod}, which also skips
+ * all bridge methods for the common case where {@link
org.apache.webbeans.util.ClassUtil#getNonPrivateMethods(Class, boolean)} omits
them.
+ */
+ protected boolean unproxyableMethodExceptBridge(Method delegatedMethod)
{
int modifiers = delegatedMethod.getModifiers();
return (modifiers & (Modifier.PRIVATE | Modifier.STATIC |
Modifier.FINAL | Modifier.NATIVE)) > 0 ||
- "finalize".equals(delegatedMethod.getName()) ||
delegatedMethod.isBridge();
+ "finalize".equals(delegatedMethod.getName());
+ }
+
+ /**
+ * For method lists that omit JVM bridges (usual {@link
org.apache.webbeans.util.ClassUtil#getNonPrivateMethods(Class, boolean)}).
+ */
+ protected boolean unproxyableMethod(Method delegatedMethod)
+ {
+ return unproxyableMethodExceptBridge(delegatedMethod) ||
delegatedMethod.isBridge();
}
/**
diff --git
a/webbeans-impl/src/main/java/org/apache/webbeans/proxy/NormalScopeProxyFactory.java
b/webbeans-impl/src/main/java/org/apache/webbeans/proxy/NormalScopeProxyFactory.java
index 4b89518ce..4c3256bbf 100644
---
a/webbeans-impl/src/main/java/org/apache/webbeans/proxy/NormalScopeProxyFactory.java
+++
b/webbeans-impl/src/main/java/org/apache/webbeans/proxy/NormalScopeProxyFactory.java
@@ -236,9 +236,9 @@ public class NormalScopeProxyFactory extends
AbstractProxyFactory
List<Method> protectedMethods = new ArrayList<>();
- for (Method method : ClassUtil.getNonPrivateMethods(classToProxy,
true))
+ for (Method method : ClassUtil.getNonPrivateMethods(classToProxy,
true, true))
{
- if (unproxyableMethod(method))
+ if (unproxyableMethodExceptBridge(method))
{
continue;
}
@@ -382,7 +382,7 @@ public class NormalScopeProxyFactory extends
AbstractProxyFactory
{
if (isIgnoredMethod(delegatedMethod))
{
- return;
+ continue;
}
String methodDescriptor =
Type.getMethodDescriptor(delegatedMethod);
diff --git
a/webbeans-impl/src/main/java/org/apache/webbeans/util/ClassUtil.java
b/webbeans-impl/src/main/java/org/apache/webbeans/util/ClassUtil.java
index 50370f002..3ff2bf0e5 100644
--- a/webbeans-impl/src/main/java/org/apache/webbeans/util/ClassUtil.java
+++ b/webbeans-impl/src/main/java/org/apache/webbeans/util/ClassUtil.java
@@ -263,8 +263,9 @@ public final class ClassUtil
/**
* collect all non-private, non-static and non-abstract methods from the
given class.
* This method removes any overloaded methods from the list automatically.
- * We also do skip bridge methods as they exist for and are handled solely
- * by the JVM itself.
+ * We also skip JVM {@link Method#isBridge() bridge} methods by default;
use
+ * {@link #getNonPrivateMethods(Class, boolean, boolean)} when a caller
(e.g. normal-scope
+ * subclass proxies, OWB-1234/923) must list those signatures too.
*
* The returned Map contains the methods divided by the methodName as key
in the map
* following all the methods with the same methodName in a List.
@@ -277,6 +278,18 @@ public final class ClassUtil
* @param excludeFinalMethods whether final classes should get excluded
from the result
*/
public static List<Method> getNonPrivateMethods(Class<?> topClass, boolean
excludeFinalMethods)
+ {
+ return getNonPrivateMethods(topClass, excludeFinalMethods, false);
+ }
+
+ /**
+ * @param includeJvmBridgeMethods when {@code true} (classes only), after
the usual collection
+ * this method appends JVM bridge methods whose {@code name + JVM
descriptor} is not
+ * already present (descriptor alone is insufficient: e.g. {@code
clone()} and a covariant
+ * {@code getValue()} bridge can share {@code
()Ljava/lang/Object;}, OWB-923).
+ */
+ public static List<Method> getNonPrivateMethods(Class<?> topClass, boolean
excludeFinalMethods,
+ boolean includeJvmBridgeMethods)
{
Map<String, List<Method>> methodMap = new HashMap<>();
List<Method> allMethods = new ArrayList<>(10);
@@ -300,9 +313,82 @@ public final class ClassUtil
}
}
+ if (includeJvmBridgeMethods && !topClass.isAnnotation() &&
!topClass.isInterface())
+ {
+ appendJvmBridgeMethods(topClass, excludeFinalMethods, allMethods);
+ }
+
return allMethods;
}
+ /**
+ * Bridges are merged in a second pass with JVM-level descriptor keys so a
covariant-return
+ * bridge (e.g. {@code ()Ljava/lang/Object;}) is not dropped as an
"override" of
+ * {@code ()Ljava/lang/String;} during single-pass name-based collection.
+ */
+ private static void appendJvmBridgeMethods(Class<?> topClass, boolean
excludeFinalMethods, List<Method> allMethods)
+ {
+ // Name + JVM descriptor: covariant bridge
getValue()Ljava/lang/Object; must not collide with
+ // Object.clone()Ljava/lang/Object; (same descriptor, different name —
see OWB-923).
+ Set<String> signatureKeys = new HashSet<>();
+ for (Method m : allMethods)
+ {
+ signatureKeys.add(jvmBridgeSignatureKey(m));
+ }
+ for (Class<?> c = topClass; c != null && c != Object.class; c =
c.getSuperclass())
+ {
+ for (Method m : SecurityUtil.doPrivilegedGetDeclaredMethods(c))
+ {
+ if (!m.isBridge())
+ {
+ continue;
+ }
+ if (skipBridgeMethodForCollection(m, excludeFinalMethods, c,
topClass))
+ {
+ continue;
+ }
+ String key = jvmBridgeSignatureKey(m);
+ if (signatureKeys.contains(key))
+ {
+ continue;
+ }
+ signatureKeys.add(key);
+ allMethods.add(m);
+ }
+ }
+ }
+
+ private static String jvmBridgeSignatureKey(Method m)
+ {
+ return m.getName() + '\0' +
org.apache.xbean.asm9.Type.getMethodDescriptor(m);
+ }
+
+ private static boolean skipBridgeMethodForCollection(Method m, boolean
excludeFinalMethods,
+ Class<?> declaringClass, Class<?> topClass)
+ {
+ int modifiers = m.getModifiers();
+ if (Modifier.isPrivate(modifiers) || Modifier.isStatic(modifiers))
+ {
+ return true;
+ }
+ if (excludeFinalMethods && Modifier.isFinal(modifiers))
+ {
+ return true;
+ }
+ if ("finalize".equals(m.getName()))
+ {
+ return true;
+ }
+ if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers))
+ {
+ if
(!declaringClass.getPackage().getName().equals(topClass.getPackage().getName()))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
private static void addNonPrivateMethods(Class<?> topClass, boolean
excludeFinalMethods,
Map<String, List<Method>>
methodMap, List<Method> allMethods,
Class<?> clazz)
@@ -325,7 +411,7 @@ public final class ClassUtil
if (method.isBridge())
{
- // we have no interest in generics bridge methods
+ // we have no interest in generics bridge methods for general
API reflection
continue;
}
diff --git
a/webbeans-impl/src/test/java/org/apache/webbeans/test/proxy/BridgeMethodNormalScopeProxyTest.java
b/webbeans-impl/src/test/java/org/apache/webbeans/test/proxy/BridgeMethodNormalScopeProxyTest.java
new file mode 100644
index 000000000..803aa4f7e
--- /dev/null
+++
b/webbeans-impl/src/test/java/org/apache/webbeans/test/proxy/BridgeMethodNormalScopeProxyTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.webbeans.test.proxy;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.webbeans.proxy.NormalScopeProxyFactory;
+import org.apache.webbeans.proxy.OwbNormalScopeProxy;
+import org.apache.webbeans.test.AbstractUnitTest;
+
+/**
+ * OWB-1234: interface dispatch uses JVM bridge methods; normal-scope proxies
must delegate those too.
+ */
+public class BridgeMethodNormalScopeProxyTest extends AbstractUnitTest
+{
+ public interface IntegerContract
+ {
+ void doIt(Integer value);
+ }
+
+ public static class BaseNumberBean<T extends Number>
+ {
+ static volatile Object lastDoItReceiver;
+
+ public void doIt(T param)
+ {
+ lastDoItReceiver = this;
+ }
+ }
+
+ /** Compiler-generated bridge {@code doIt(Integer)} only (no explicit
override). */
+ @ApplicationScoped
+ public static class BridgeOnlyBean extends BaseNumberBean<Integer>
implements IntegerContract
+ {
+ }
+
+ /** Explicit {@code doIt(Integer)} — always worked before OWB-1234 fix. */
+ @ApplicationScoped
+ public static class ExplicitOverrideBean extends BaseNumberBean<Integer>
implements IntegerContract
+ {
+ @Override
+ public void doIt(Integer param)
+ {
+ super.doIt(param);
+ }
+ }
+
+ @Test
+ public void bridgeMethodInvocationDelegatesToContextualInstance()
+ {
+ BaseNumberBean.lastDoItReceiver = null;
+ startContainer(BridgeOnlyBean.class);
+
+ IntegerContract handler = getInstance(IntegerContract.class);
+ Assert.assertTrue(handler instanceof OwbNormalScopeProxy);
+
+ Object contextual = NormalScopeProxyFactory.unwrapInstance(handler);
+ handler.doIt(4711);
+
+ Assert.assertSame(
+ "call through interface must use proxy delegation (contextual
instance as receiver)",
+ contextual,
+ BaseNumberBean.lastDoItReceiver);
+ }
+
+ @Test
+ public void explicitOverrideStillDelegatesToContextualInstance()
+ {
+ BaseNumberBean.lastDoItReceiver = null;
+ startContainer(ExplicitOverrideBean.class);
+
+ IntegerContract handler = getInstance(IntegerContract.class);
+ Assert.assertTrue(handler instanceof OwbNormalScopeProxy);
+
+ Object contextual = NormalScopeProxyFactory.unwrapInstance(handler);
+ handler.doIt(7);
+
+ Assert.assertSame(contextual, BaseNumberBean.lastDoItReceiver);
+ }
+}
diff --git
a/webbeans-impl/src/test/java/org/apache/webbeans/test/proxy/BridgeMethodOwb923And828Test.java
b/webbeans-impl/src/test/java/org/apache/webbeans/test/proxy/BridgeMethodOwb923And828Test.java
new file mode 100644
index 000000000..7e2e7b61d
--- /dev/null
+++
b/webbeans-impl/src/test/java/org/apache/webbeans/test/proxy/BridgeMethodOwb923And828Test.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.webbeans.test.proxy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.interceptor.AroundInvoke;
+import jakarta.interceptor.Interceptor;
+import jakarta.interceptor.InterceptorBinding;
+import jakarta.interceptor.InvocationContext;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.webbeans.proxy.InterceptorDecoratorProxyFactory;
+import org.apache.webbeans.proxy.NormalScopeProxyFactory;
+import org.apache.webbeans.proxy.OwbNormalScopeProxy;
+import org.apache.webbeans.test.AbstractUnitTest;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Regression coverage aligned with historical bridge-method bugs in
interceptors / scoped proxies.
+ *
+ * <p><a href="https://issues.apache.org/jira/browse/OWB-923">OWB-923</a> —
wrong handling when a
+ * bean inherits the implementation from a superclass but implements a
parameterized interface
+ * (compiler bridge methods).</p>
+ *
+ * <p><a href="https://issues.apache.org/jira/browse/OWB-828">OWB-828</a> —
with normal-scoping and
+ * interceptor subclasses, interface dispatch via a bridge could leave the
interceptor proxy as
+ * {@code this} in the business method (broken injection / wrong receiver)
instead of the target
+ * instance.</p>
+ */
+public class BridgeMethodOwb923And828Test extends AbstractUnitTest
+{
+
+ // --- OWB-923 shape: interface ValueContract<T> + abstract base
with String getValue() ---
+
+ public interface ValueContract923<T>
+ {
+ T getValue();
+ }
+
+ public abstract static class ValueBase923
+ {
+ static volatile Object lastGetValueReceiver;
+
+ public String getValue()
+ {
+ lastGetValueReceiver = this;
+ return "owb923";
+ }
+ }
+
+ @ApplicationScoped
+ public static class Owb923StyleBean extends ValueBase923 implements
ValueContract923<String>
+ {
+ }
+
+ @Test
+ public void
owb923_inheritedGetterThroughParameterizedInterface_delegatesToContextualInstance()
+ {
+ ValueBase923.lastGetValueReceiver = null;
+ startContainer(Owb923StyleBean.class);
+
+ ValueContract923<String> viaIface = getInstance(Owb923StyleBean.class);
+ Assert.assertTrue(viaIface instanceof OwbNormalScopeProxy);
+
+ Object contextual = NormalScopeProxyFactory.unwrapInstance(viaIface);
+ Assert.assertEquals("owb923", viaIface.getValue());
+ Assert.assertSame(
+ "OWB-923: call through parameterized interface must reach the
contextual instance",
+ contextual,
+ ValueBase923.lastGetValueReceiver);
+ }
+
+ // --- OWB-828 shape: @ApplicationScoped + interceptor proxy under
normal-scope proxy + bridge ---
+
+ @InterceptorBinding
+ @Retention(RUNTIME)
+ @Target({ TYPE, METHOD })
+ public @interface Owb828InterceptorHitBinding
+ {
+ }
+
+ @Interceptor
+ @Owb828InterceptorHitBinding
+ public static class Owb828HitInterceptor
+ {
+ static final AtomicInteger aroundInvokeCount = new AtomicInteger();
+
+ @AroundInvoke
+ public Object aroundInvoke(InvocationContext ctx) throws Exception
+ {
+ aroundInvokeCount.incrementAndGet();
+ return ctx.proceed();
+ }
+ }
+
+ public interface ActionContract828<T>
+ {
+ void doSomething(T value);
+ }
+
+ public abstract static class ActionBase828
+ {
+ static volatile Object lastDoSomethingReceiver;
+
+ public void doSomething(String value)
+ {
+ lastDoSomethingReceiver = this;
+ }
+ }
+
+ /**
+ * Method-level interceptor binding on an overriding method (see
+ * {@link
org.apache.webbeans.test.interceptors.resolution.InterceptBridgeMethodTest})
+ * matches the historical OWB-828 reproducer: normal-scoped client
reference, interceptor subclass,
+ * and interface dispatch through a bridge.
+ */
+ @ApplicationScoped
+ public static class Owb828AppScopedBean extends ActionBase828 implements
ActionContract828<String>
+ {
+ @Override
+ @Owb828InterceptorHitBinding
+ public void doSomething(String value)
+ {
+ super.doSomething(value);
+ }
+ }
+
+ @Test
+ public void
owb828_applicationScopedInterceptor_bridgeDispatchReachesContextualInstance()
+ {
+ Owb828HitInterceptor.aroundInvokeCount.set(0);
+ ActionBase828.lastDoSomethingReceiver = null;
+
+ addInterceptor(Owb828HitInterceptor.class);
+ startContainer(Owb828AppScopedBean.class);
+
+ ActionContract828<String> viaIface =
getInstance(Owb828AppScopedBean.class);
+ Assert.assertTrue(viaIface instanceof OwbNormalScopeProxy);
+
+ Object afterNormalScope =
NormalScopeProxyFactory.unwrapInstance(viaIface);
+ InterceptorDecoratorProxyFactory intDecFactory =
getWebBeansContext().getInterceptorDecoratorProxyFactory();
+ Object contextual = intDecFactory.unwrapInstance(afterNormalScope);
+
+ viaIface.doSomething("owb828");
+
+ Assert.assertEquals(
+ "OWB-828: interceptor must run exactly once for the bridged
interface dispatch",
+ 1,
+ Owb828HitInterceptor.aroundInvokeCount.get());
+ Assert.assertSame(
+ "OWB-828: business method 'this' must be the real bean behind
scope + intercept proxies",
+ contextual,
+ ActionBase828.lastDoSomethingReceiver);
+ }
+}
diff --git
a/webbeans-impl/src/test/java/org/apache/webbeans/test/util/ClassUtilTest.java
b/webbeans-impl/src/test/java/org/apache/webbeans/test/util/ClassUtilTest.java
index 26b96e45f..e7e83d847 100644
---
a/webbeans-impl/src/test/java/org/apache/webbeans/test/util/ClassUtilTest.java
+++
b/webbeans-impl/src/test/java/org/apache/webbeans/test/util/ClassUtilTest.java
@@ -40,6 +40,20 @@ public class ClassUtilTest {
Assert.assertEquals(1, nonPrivateMethods.size());
}
+ @Test
+ public void testGetNonPrivateMethods_includeJvmBridgeMethods()
+ {
+ List<Method> withoutBridges =
ClassUtil.getNonPrivateMethods(SpecificClass.class, false);
+
withoutBridges.removeAll(Arrays.asList(Object.class.getDeclaredMethods()));
+ Assert.assertEquals(1, withoutBridges.size());
+ Assert.assertFalse(withoutBridges.stream().anyMatch(Method::isBridge));
+
+ List<Method> withBridges =
ClassUtil.getNonPrivateMethods(SpecificClass.class, false, true);
+
withBridges.removeAll(Arrays.asList(Object.class.getDeclaredMethods()));
+ Assert.assertEquals(2, withBridges.size());
+ Assert.assertEquals(1,
withBridges.stream().filter(Method::isBridge).count());
+ }
+
@Test
public void testGetAllNonPrivateMethods_packagePrivate()
{
@@ -96,6 +110,35 @@ public class ClassUtilTest {
Assert.assertFalse(ClassUtil.isMethodDeclared(clazz,
"notExistingMethod"));
}
+ /** Covariant return yields a bridge ()Ljava/lang/Object; — same JVM
descriptor as Object.clone(). */
+ public interface CovariantIface
+ {
+ Object getV();
+ }
+
+ public abstract static class CovariantBase
+ {
+ public String getV()
+ {
+ return "x";
+ }
+ }
+
+ public static final class CovariantImpl extends CovariantBase implements
CovariantIface
+ {
+ }
+
+ @Test
+ public void testGetNonPrivateMethods_jvmBridgeDescriptorDistinctFromClone()
+ {
+ List<Method> methods =
ClassUtil.getNonPrivateMethods(CovariantImpl.class, true, true);
+ long bridgeGetV = methods.stream().filter(m ->
"getV".equals(m.getName()) && m.isBridge()).count();
+ Assert.assertEquals(
+ "covariant bridge shares ()Ljava/lang/Object; with clone —
must still be listed (OWB-923)",
+ 1,
+ bridgeGetV);
+ }
+
private boolean isOverridden(Class subClass, String methodName) throws
Exception
{
Method superClassMethod =
MySuperClass.class.getDeclaredMethod(methodName);