This is an automated email from the ASF dual-hosted git repository. struberg pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/openwebbeans.git
commit 376c0842dd51bdea7ad090842b6a25faf8f20668 Author: Richard Zowalla <[email protected]> AuthorDate: Wed Jul 8 20:31:16 2026 +0200 OWB-1465 - Preserve ACC_BRIDGE / ACC_SYNTHETIC on bridge methods re-declared by normal scope proxies NormalScopeProxyFactory re-declares the JVM bridge methods of the proxied class but masked the method modifiers with PROTECTED | PUBLIC | VARARGS, dropping the ACC_BRIDGE flag. The proxy then exposed two regular methods with the same name and assignable parameter types, which breaks overload resolution in EL implementations (jakarta.el / Tomcat's ReflectionUtil tie-break on Method#isBridge()) with 'Unable to find unambiguous method'. Keep ACC_BRIDGE and ACC_SYNTHETIC in both generation paths (delegateNonInterceptedMethods for public methods, generateDelegationMethod for protected ones). Introduces MODIFIER_BRIDGE / MODIFIER_SYNTHETIC constants next to the existing MODIFIER_VARARGS as java.lang.reflect.Modifier does not expose them. --- .../webbeans/proxy/AbstractProxyFactory.java | 15 +++ .../webbeans/proxy/NormalScopeProxyFactory.java | 10 +- .../factory/ProxyBridgeMethodFlagTest.java | 132 +++++++++++++++++++++ .../test/util/GenericParameterInterface.java | 29 +++++ .../webbeans/test/util/SpecificParameterClass.java | 34 ++++++ 5 files changed, 218 insertions(+), 2 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 51be6a45b..5dba228bc 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 @@ -60,6 +60,21 @@ public abstract class AbstractProxyFactory */ public static final int MODIFIER_VARARGS = 0x00000080; + /** + * This is needed as the Modifier#BRIDGE is not (yet) public. Must be preserved when a proxy + * re-declares a JVM bridge method: overload resolution e.g. in Expression Language + * implementations relies on {@link Method#isBridge()} to disambiguate overloaded methods. + * Note that the bitcode is the same as Modifier#VOLATILE. + * But 'bridge' is only for methods, whereas 'volatile' is only for fields. + */ + public static final int MODIFIER_BRIDGE = 0x00000040; + + /** + * This is needed as the Modifier#SYNTHETIC is not (yet) public. + * JVM bridge methods carry ACC_BRIDGE | ACC_SYNTHETIC, so keep both on re-declared methods. + */ + public static final int MODIFIER_SYNTHETIC = 0x00001000; + protected final Unsafe unsafe; private final DefiningClassService definingService; 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 4c3256bbf..0756b237d 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 @@ -395,7 +395,10 @@ public class NormalScopeProxyFactory extends AbstractProxyFactory exceptionTypeNames[i] = Type.getType(exceptionTypes[i]).getInternalName(); } - int targetModifiers = delegatedMethod.getModifiers() & (Modifier.PROTECTED | Modifier.PUBLIC | MODIFIER_VARARGS); + // preserve ACC_BRIDGE | ACC_SYNTHETIC on re-declared JVM bridge methods: overload resolution + // (e.g. jakarta.el MethodExpressions on proxied beans) relies on Method#isBridge() to disambiguate + int targetModifiers = delegatedMethod.getModifiers() + & (Modifier.PROTECTED | Modifier.PUBLIC | MODIFIER_VARARGS | MODIFIER_BRIDGE | MODIFIER_SYNTHETIC); MethodVisitor mv = cw.visitMethod(targetModifiers, delegatedMethod.getName(), methodDescriptor, null, exceptionTypeNames); @@ -454,7 +457,10 @@ public class NormalScopeProxyFactory extends AbstractProxyFactory int modifiers = method.getModifiers(); // push the method definition - int modifier = modifiers & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_VARARGS); + // preserve ACC_BRIDGE | ACC_SYNTHETIC on re-declared JVM bridge methods: overload resolution + // (e.g. jakarta.el MethodExpressions on proxied beans) relies on Method#isBridge() to disambiguate + int modifier = modifiers + & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_VARARGS | Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC); MethodVisitor mv = cw.visitMethod(modifier, method.getName(), Type.getMethodDescriptor(method), null, null); mv.visitCode(); diff --git a/webbeans-impl/src/test/java/org/apache/webbeans/test/interceptors/factory/ProxyBridgeMethodFlagTest.java b/webbeans-impl/src/test/java/org/apache/webbeans/test/interceptors/factory/ProxyBridgeMethodFlagTest.java new file mode 100644 index 000000000..289233136 --- /dev/null +++ b/webbeans-impl/src/test/java/org/apache/webbeans/test/interceptors/factory/ProxyBridgeMethodFlagTest.java @@ -0,0 +1,132 @@ +/* + * 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.interceptors.factory; + +import org.apache.webbeans.config.WebBeansContext; +import org.apache.webbeans.proxy.InterceptorDecoratorProxyFactory; +import org.apache.webbeans.proxy.NormalScopeProxyFactory; +import org.apache.webbeans.test.util.CustomBaseType; +import org.apache.webbeans.test.util.CustomType; +import org.apache.webbeans.test.util.SpecificParameterClass; +import org.apache.webbeans.util.ClassUtil; +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +/** + * Proxies which materialize JVM bridge methods must keep the {@code ACC_BRIDGE} flag + * on the generated method. + * <p> + * {@link SpecificParameterClass} implements {@code GenericParameterInterface<CustomType>}, + * so javac generates the bridge method {@code consume(CustomBaseType)} next to the declared + * {@code consume(CustomType)}. When a subclass proxy re-declares that bridge method + * <em>without</em> the {@code ACC_BRIDGE} flag, the proxy class exposes two regular methods + * named {@code consume} with assignable parameter types. Expression language implementations + * resolve overloaded methods by preferring the non-bridge variant (see + * {@code jakarta.el}/Tomcat {@code org.apache.el.util.ReflectionUtil}); with the flag dropped + * the invocation of e.g. {@code #{bean.consume(x)}} fails with + * "{@code MethodNotFoundException: Unable to find unambiguous method}". + */ +public class ProxyBridgeMethodFlagTest +{ + + @Test + public void normalScopeProxyPreservesBridgeMethodFlag() throws Exception + { + NormalScopeProxyFactory pf = new NormalScopeProxyFactory(new WebBeansContext()); + + // we take a fresh URLClassLoader to not blur the test classpath with synthetic classes. + ClassLoader classLoader = new URLClassLoader(new URL[0]); + + Class<SpecificParameterClass> proxyClass = pf.createProxyClass(classLoader, SpecificParameterClass.class); + Assert.assertNotNull(proxyClass); + + assertBridgeFlagPreserved(proxyClass); + } + + /** + * Documents the currently correct behaviour of the {@link InterceptorDecoratorProxyFactory}: + * bridge methods are skipped entirely ({@code unproxyableMethod}), so the proxy inherits the + * properly flagged bridge method from the proxied superclass. This test passes and guards + * against regressions; only {@link NormalScopeProxyFactory} re-declares bridge methods and + * loses the flag. + */ + @Test + public void interceptorProxyPreservesBridgeMethodFlag() throws Exception + { + InterceptorDecoratorProxyFactory pf = new InterceptorDecoratorProxyFactory(new WebBeansContext()); + + // we take a fresh URLClassLoader to not blur the test classpath with synthetic classes. + ClassLoader classLoader = new URLClassLoader(new URL[0]); + + List<Method> methods = ClassUtil.getNonPrivateMethods(SpecificParameterClass.class, true, true); + + List<Method> interceptedMethods = new ArrayList<>(); + List<Method> nonInterceptedMethods = new ArrayList<>(); + for (Method m : methods) + { + if (m.isBridge()) + { + // bridge methods only get delegated, they are never intercepted (OWB-1234) + nonInterceptedMethods.add(m); + } + else + { + interceptedMethods.add(m); + } + } + + Class<SpecificParameterClass> proxyClass = pf.createProxyClass( + new InterceptorDecoratorProxyFactoryTest.DummyBean(), classLoader, SpecificParameterClass.class, + interceptedMethods.toArray(new Method[interceptedMethods.size()]), + nonInterceptedMethods.toArray(new Method[nonInterceptedMethods.size()])); + Assert.assertNotNull(proxyClass); + + assertBridgeFlagPreserved(proxyClass); + } + + private void assertBridgeFlagPreserved(Class<? extends SpecificParameterClass> proxyClass) throws Exception + { + // sanity check the fixture: javac generated the bridge method on the proxied class + Method originalBridge = SpecificParameterClass.class.getDeclaredMethod("consume", CustomBaseType.class); + Assert.assertTrue(originalBridge.isBridge()); + Method originalSpecific = SpecificParameterClass.class.getDeclaredMethod("consume", CustomType.class); + Assert.assertFalse(originalSpecific.isBridge()); + + // exactly like on the proxied class itself, only ONE non-bridge consume method may be visible + List<Method> nonBridgeMethods = new ArrayList<>(); + for (Method m : proxyClass.getMethods()) + { + if ("consume".equals(m.getName()) && m.getParameterCount() == 1 && !m.isBridge()) + { + nonBridgeMethods.add(m); + } + } + + Assert.assertEquals("the proxy must not expose the JVM bridge method consume(CustomBaseType) as a" + + " regular method - overload resolution (e.g. jakarta.el MethodExpressions on proxied beans)" + + " relies on the ACC_BRIDGE flag to disambiguate, but found: " + nonBridgeMethods, + 1, nonBridgeMethods.size()); + } +} diff --git a/webbeans-impl/src/test/java/org/apache/webbeans/test/util/GenericParameterInterface.java b/webbeans-impl/src/test/java/org/apache/webbeans/test/util/GenericParameterInterface.java new file mode 100644 index 000000000..b199c8cfb --- /dev/null +++ b/webbeans-impl/src/test/java/org/apache/webbeans/test/util/GenericParameterInterface.java @@ -0,0 +1,29 @@ +/* + * 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.util; + +/** + * A generic interface with a type-variable method <em>parameter</em>. + * Implementations with a concrete type argument get a JVM bridge method + * <code>consume(CustomBaseType)</code> generated by javac. + */ +public interface GenericParameterInterface<T extends CustomBaseType> +{ + String consume(T instance); +} diff --git a/webbeans-impl/src/test/java/org/apache/webbeans/test/util/SpecificParameterClass.java b/webbeans-impl/src/test/java/org/apache/webbeans/test/util/SpecificParameterClass.java new file mode 100644 index 000000000..4f8213351 --- /dev/null +++ b/webbeans-impl/src/test/java/org/apache/webbeans/test/util/SpecificParameterClass.java @@ -0,0 +1,34 @@ +/* + * 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.util; + +/** + * Implements {@link GenericParameterInterface} with a concrete type argument. + * The compiled class therefore contains two methods named <code>consume</code>: + * the declared <code>consume(CustomType)</code> and the javac-generated + * JVM bridge method <code>consume(CustomBaseType)</code>. + */ +public class SpecificParameterClass implements GenericParameterInterface<CustomType> +{ + @Override + public String consume(CustomType instance) + { + return "specific"; + } +}
