diff --git a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/Constants.java b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/Constants.java index 3846b001a259..327e4ff1072c 100644 --- a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/Constants.java +++ b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/Constants.java @@ -41,4 +41,24 @@ public interface Constants { * From FactoryBean.OBJECT_TYPE_ATTRIBUTE of Spring 5.2. */ String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType"; + + /** + * Environment property that, when set to {@code true}, makes auto-derived reference bean + * names (i.e. the ones taken from the annotated field/method property name, when no explicit + * {@code id} is given) unable to collide with the name of an unrelated bean registered + * elsewhere in the same Spring context, such as one looked up by an independent + * {@code @Resource} field with a default name. + *
+ * This is opt-in and defaults to {@code false} to preserve the existing reference bean + * naming behavior for applications that already rely on it. + * + * @see org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor#registerReferenceBean + */ + String QUALIFY_REFERENCE_BEAN_NAME_KEY = "dubbo.application.qualify-reference-bean-name"; + + /** + * Suffix appended to an auto-derived reference bean name when + * {@link #QUALIFY_REFERENCE_BEAN_NAME_KEY} is enabled. + */ + String REFERENCE_BEAN_NAME_QUALIFIER = "#dubbo-reference"; } diff --git a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java index 643a12f91678..da1aca0d8ebc 100644 --- a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java +++ b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/beans/factory/annotation/ReferenceAnnotationBeanPostProcessor.java @@ -422,6 +422,17 @@ public String registerReferenceBean( renameable = false; } else { referenceBeanName = propertyName; + if (isQualifyReferenceBeanNameEnabled()) { + // An auto-derived name is just the bare field/property name, which can silently + // collide with an unrelated bean registered elsewhere in the same Spring context + // (e.g. a @Resource field with a default name pointing at a different type). + // Spring's CommonAnnotationBeanPostProcessor#autowireResource only falls back to + // by-type resolution when no bean of that name exists yet, so once this reference + // bean claims the bare name first, such a collision surfaces as a confusing + // BeanNotOfRequiredTypeException instead of a successful by-type lookup. + // See: https://github.com/apache/dubbo/issues/12637 + referenceBeanName = referenceBeanName + Constants.REFERENCE_BEAN_NAME_QUALIFIER; + } } String checkLocation = "Please check " + member.toString(); @@ -567,6 +578,12 @@ public String registerReferenceBean( return referenceBeanName; } + private boolean isQualifyReferenceBeanNameEnabled() { + return applicationContext + .getEnvironment() + .getProperty(Constants.QUALIFY_REFERENCE_BEAN_NAME_KEY, Boolean.class, Boolean.FALSE); + } + @Override protected Object doGetInjectedBean( AnnotationAttributes attributes, diff --git a/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/ReferenceBeanNameCollisionFixedTest.java b/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/ReferenceBeanNameCollisionFixedTest.java new file mode 100644 index 000000000000..c9a89e29b9b6 --- /dev/null +++ b/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/ReferenceBeanNameCollisionFixedTest.java @@ -0,0 +1,81 @@ +/* + * 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.dubbo.config.spring; + +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.bootstrap.DubboBootstrap; +import org.apache.dubbo.config.spring.api.DemoService; +import org.apache.dubbo.config.spring.beans.factory.annotation.ServiceAnnotationTestConfiguration; +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD; + +/** + * Companion to {@link ReferenceBeanNameCollisionTest}: same reproduction scenario for + * https://github.com/apache/dubbo/issues/12637, but with the opt-in + * {@code dubbo.application.qualify-reference-bean-name} flag enabled. Verifies that the same + * by-name lookup of an unrelated type no longer collides with the {@code @DubboReference} + * field's auto-derived bean name once qualification is turned on. + */ +@EnableDubbo(scanBasePackages = "org.apache.dubbo.config.spring.context.annotation.provider") +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {ServiceAnnotationTestConfiguration.class, ReferenceBeanNameCollisionFixedTest.class}) +@TestPropertySource(properties = "dubbo.application.qualify-reference-bean-name=true") +@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) +class ReferenceBeanNameCollisionFixedTest { + + @BeforeAll + static void setUp() { + DubboBootstrap.reset(); + } + + @AfterEach + void tearDown() { + DubboBootstrap.reset(); + } + + // With qualification enabled, this no longer registers under the bare name + // "collisionProbe" - it's suffixed, so it can no longer collide with an unrelated + // by-name lookup for "collisionProbe" elsewhere in the context. + @DubboReference(version = "2", url = "dubbo://127.0.0.1:12345?version=2") + private DemoService collisionProbe; + + static class UnrelatedType {} + + @Test + void unrelatedByNameLookupNoLongerCollidesWhenQualificationEnabled(ApplicationContext context) { + Assertions.assertFalse( + context.containsBean("collisionProbe"), + "bare name should no longer be claimed by the reference bean when qualification is enabled"); + Assertions.assertThrows( + org.springframework.beans.factory.NoSuchBeanDefinitionException.class, + () -> context.getBean("collisionProbe", UnrelatedType.class), + "no bean should exist under the bare name at all now - a real @Resource field would " + + "correctly fall back to by-type resolution instead of hitting a type mismatch"); + } +} diff --git a/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/ReferenceBeanNameCollisionTest.java b/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/ReferenceBeanNameCollisionTest.java new file mode 100644 index 000000000000..75da253984be --- /dev/null +++ b/dubbo-config/dubbo-config-spring/src/test/java/org/apache/dubbo/config/spring/ReferenceBeanNameCollisionTest.java @@ -0,0 +1,92 @@ +/* + * 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.dubbo.config.spring; + +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.bootstrap.DubboBootstrap; +import org.apache.dubbo.config.spring.api.DemoService; +import org.apache.dubbo.config.spring.beans.factory.annotation.ServiceAnnotationTestConfiguration; +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD; + +/** + * Reproduces https://github.com/apache/dubbo/issues/12637: an auto-derived reference bean name + * (the bare field/property name, used when no explicit {@code id} is given) can collide with the + * default name a plain {@code @Resource}-style by-name lookup elsewhere in the same Spring + * context uses for an unrelated bean. Once the reference bean claims that name, any other lookup + * of that exact name expecting a different type fails with a confusing + * {@link BeanNotOfRequiredTypeException} instead of resolving the unrelated bean by type. + * + *Reuses the same {@link EnableDubbo} + {@link ServiceAnnotationTestConfiguration} bootstrap + * as {@link org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessorTest}, + * which is already proven to correctly activate {@code @DubboReference} processing. + */ +@EnableDubbo(scanBasePackages = "org.apache.dubbo.config.spring.context.annotation.provider") +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {ServiceAnnotationTestConfiguration.class, ReferenceBeanNameCollisionTest.class}) +@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) +class ReferenceBeanNameCollisionTest { + + @BeforeAll + static void setUp() { + DubboBootstrap.reset(); + } + + @AfterEach + void tearDown() { + DubboBootstrap.reset(); + } + + // Auto-derived reference bean name will be the bare field name: "collisionProbe". + // No other bean is declared under this name - unlike a bean already registered at + // definition time (which Dubbo's existing rename-on-collision logic already handles), + // this mirrors an independent @Resource field elsewhere with the same default name: + // a pure runtime by-name lookup that Dubbo cannot see coming at registration time. + @DubboReference(version = "2", url = "dubbo://127.0.0.1:12345?version=2") + private DemoService collisionProbe; + + // Unrelated marker type, standing in for whatever type an independent @Resource + // field with the same default name would declare. + static class UnrelatedType {} + + @Test + void lookupByNameWithUnrelatedTypeAfterReferenceBeanClaimsName(ApplicationContext context) { + Assertions.assertTrue( + context.containsBean("collisionProbe"), + "the @DubboReference field should have auto-registered a reference bean " + + "under its bare property name"); + Assertions.assertThrows( + BeanNotOfRequiredTypeException.class, + () -> context.getBean("collisionProbe", UnrelatedType.class), + "documents the known limitation: without opting into " + + "dubbo.application.qualify-reference-bean-name, an unrelated by-name lookup " + + "(e.g. an independent @Resource field with the same default name) collides " + + "with the reference bean instead of falling back to by-type resolution"); + } +}