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..c5f52cdc6 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,31 @@ 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; + + /** + * The method modifiers a generated proxy keeps when re-declaring a method of the proxied class; + * everything else (e.g. ABSTRACT, SYNCHRONIZED, NATIVE) must not appear on a generated + * delegation method. ACC_BRIDGE / ACC_SYNTHETIC are preserved so that overload resolution + * (e.g. jakarta.el MethodExpressions on proxied beans) can still detect re-declared JVM bridge + * methods via {@link Method#isBridge()}. + */ + public static final int PROXYABLE_METHOD_MODIFIERS = + Modifier.PUBLIC | Modifier.PROTECTED | MODIFIER_VARARGS | MODIFIER_BRIDGE | MODIFIER_SYNTHETIC; + protected final Unsafe unsafe; private final DefiningClassService definingService; diff --git a/webbeans-impl/src/main/java/org/apache/webbeans/proxy/InterceptorDecoratorProxyFactory.java b/webbeans-impl/src/main/java/org/apache/webbeans/proxy/InterceptorDecoratorProxyFactory.java index 8defa2279..2fa944d48 100644 --- a/webbeans-impl/src/main/java/org/apache/webbeans/proxy/InterceptorDecoratorProxyFactory.java +++ b/webbeans-impl/src/main/java/org/apache/webbeans/proxy/InterceptorDecoratorProxyFactory.java @@ -355,7 +355,7 @@ protected void delegateNonInterceptedMethods(ClassLoader classLoader, ClassWrite exceptionTypeNames[i] = Type.getType(exceptionTypes[i]).getInternalName(); } - int targetModifiers = modifiers & (Modifier.PROTECTED | Modifier.PUBLIC | MODIFIER_VARARGS); + int targetModifiers = modifiers & PROXYABLE_METHOD_MODIFIERS; MethodVisitor mv = cw.visitMethod(targetModifiers, delegatedMethod.getName(), methodDescriptor, null, exceptionTypeNames); @@ -423,7 +423,7 @@ private void generateInterceptorHandledMethod(ClassWriter cw, Method method, int } // push the method definition - int modifier = modifiers & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_VARARGS); + int modifier = modifiers & PROXYABLE_METHOD_MODIFIERS; MethodVisitor mv = cw.visitMethod(modifier, method.getName(), Type.getMethodDescriptor(method), null, null); mv.visitCode(); 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..f31ddc319 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,7 @@ protected void delegateNonInterceptedMethods(ClassLoader classLoader, ClassWrite exceptionTypeNames[i] = Type.getType(exceptionTypes[i]).getInternalName(); } - int targetModifiers = delegatedMethod.getModifiers() & (Modifier.PROTECTED | Modifier.PUBLIC | MODIFIER_VARARGS); + int targetModifiers = delegatedMethod.getModifiers() & PROXYABLE_METHOD_MODIFIERS; MethodVisitor mv = cw.visitMethod(targetModifiers, delegatedMethod.getName(), methodDescriptor, null, exceptionTypeNames); @@ -454,7 +454,7 @@ private void generateDelegationMethod(ClassWriter cw, Method method, int methodI int modifiers = method.getModifiers(); // push the method definition - int modifier = modifiers & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_VARARGS); + int modifier = modifiers & PROXYABLE_METHOD_MODIFIERS; 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. + *

+ * {@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 + * without 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 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 methods = ClassUtil.getNonPrivateMethods(SpecificParameterClass.class, true, true); + + List interceptedMethods = new ArrayList<>(); + List 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 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 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 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 parameter. + * Implementations with a concrete type argument get a JVM bridge method + * consume(CustomBaseType) generated by javac. + */ +public interface GenericParameterInterface +{ + 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 consume: + * the declared consume(CustomType) and the javac-generated + * JVM bridge method consume(CustomBaseType). + */ +public class SpecificParameterClass implements GenericParameterInterface +{ + @Override + public String consume(CustomType instance) + { + return "specific"; + } +}