This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit baf12a3f8beeafc38260846b5b862a321049f8bb Author: opencode <[email protected]> AuthorDate: Sat Oct 17 14:01:43 2026 +0200 Fix covariant return type overrides of annotated POJO lifecycle methods wrongly rejected as duplicates A sub-class method that overrides an annotated life cycle method of a super class is permitted by the JLS to use a covariant return type. isMethodOverride() in PojoMethodMapping compared return types using equals() so such overrides were not recognised as overrides. Re- declaring the annotation on the override therefore triggered a deployment failure ("Duplicate annotations [...]") even though an override with an identical return type re-declaring the annotation was accepted. isMethodOverride() now checks that the return types are compatible rather than identical. The methods are passed to isMethodOverride() in different orders by the various call sites so the check tests assignability in both directions. Methods with the same name and parameter types within a single class hierarchy are override equivalent by definition, since the compiler enforces return type covariance, so this cannot wrongly match methods that are not related by overriding. Adds regression tests covering an annotated covariant override that re-declares the annotation (now accepted, the sub-class method is the one mapped), a non-annotated covariant override (the super class method is ignored) and a sub-class method that is not an override (different name, same payload type) which continues to be rejected as a duplicate annotation. --- .../tomcat/websocket/pojo/PojoMethodMapping.java | 9 +- .../websocket/pojo/TestPojoMethodMapping.java | 98 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java index 10af09174b..60d8ec457a 100644 --- a/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java +++ b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java @@ -223,7 +223,14 @@ public class PojoMethodMapping { private boolean isMethodOverride(Method method1, Method method2) { - return method1.getName().equals(method2.getName()) && method1.getReturnType().equals(method2.getReturnType()) && + // An overriding method may use a covariant return type so the return + // types do not have to be equal. They do need to be compatible. The + // methods may be passed in either order so both directions are + // checked. + Class<?> returnType1 = method1.getReturnType(); + Class<?> returnType2 = method2.getReturnType(); + return method1.getName().equals(method2.getName()) && + (returnType1.isAssignableFrom(returnType2) || returnType2.isAssignableFrom(returnType1)) && Arrays.equals(method1.getParameterTypes(), method2.getParameterTypes()); } diff --git a/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java b/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java index 9e402bcb69..da4367059d 100644 --- a/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java +++ b/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java @@ -123,6 +123,104 @@ public class TestPojoMethodMapping extends TomcatBaseTest { } + /* + * A sub-class method that overrides an annotated super class method using a covariant return + * type is still an override. Re-declaring the annotation on the override must not be treated + * as a duplicate annotation and the sub-class method must be the one that is mapped. + */ + @Test + public void testCovariantReturnOverride() throws Exception { + PojoMethodMapping mapping = new PojoMethodMapping(CovariantReturnServer.class, new ArrayList<>(), + "/covariantReturn", null); + Assert.assertEquals(CovariantReturnServer.class.getMethod("onOpen", Session.class), mapping.getOnOpen()); + } + + + /* + * A non-annotated sub-class method that overrides an annotated super class method using a + * covariant return type should result in the super class method being ignored. Note that a + * compiler generated bridge method previously masked this in compiled code. + */ + @Test + public void testCovariantReturnOverrideWithoutAnnotation() throws Exception { + PojoMethodMapping mapping = new PojoMethodMapping(CovariantReturnOverrideServer.class, new ArrayList<>(), + "/covariantReturnOverride", null); + Assert.assertNull(mapping.getOnOpen()); + } + + + /* + * A sub-class method that is not an override of the annotated super class method must still + * be rejected as a duplicate annotation if it handles the same message type. + */ + @Test + public void testAnnotatedMethodNotAnOverride() throws Exception { + try { + new PojoMethodMapping(NotAnOverrideServer.class, new ArrayList<>(), "/notAnOverride", null); + Assert.fail("Expected a DeploymentException"); + } catch (DeploymentException de) { + Assert.assertTrue("Unexpected message: " + de.getMessage(), + de.getMessage().contains("Duplicate annotations")); + } + } + + + public static class CovariantReturnBase { + + @OnOpen + public Object onOpen(Session session) { + return session; + } + + @OnMessage + public Object onMessage(String message) { + return message; + } + } + + + @ServerEndpoint("/covariantReturn") + public static final class CovariantReturnServer extends CovariantReturnBase { + + @Override + @OnOpen + public String onOpen(Session session) { + return session.toString(); + } + + @Override + @OnMessage + public String onMessage(String message) { + return message; + } + } + + + @ServerEndpoint("/covariantReturnOverride") + public static final class CovariantReturnOverrideServer extends CovariantReturnBase { + + @Override + public String onOpen(Session session) { + return session.toString(); + } + + @Override + public String onMessage(String message) { + return message; + } + } + + + @ServerEndpoint("/notAnOverride") + public static final class NotAnOverrideServer extends CovariantReturnBase { + + @OnMessage + public String otherMessage(String message) { + return message; + } + } + + @ServerEndpoint(value = "/{one}/{two}/{three}", configurator = SingletonConfigurator.class) public static final class Server { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
