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 
&quot;override&quot; 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&lt;T&gt; + 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);

Reply via email to