From 82564eb73afca311e4cfad1d170dabe0b187dc73 Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:19:16 +0900 Subject: [PATCH] fix: exclude annotation types from mapper candidate components ClassPathMapperScanner#isCandidateComponent accepted any independent interface. Annotation types are interfaces at the bytecode level, so an annotation declared in a scanned package was treated as a mapper candidate and wrongly registered as a MapperFactoryBean. Exclude annotation metadata so only real mapper interfaces are scanned. Fixes gh-1032 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../spring/mapper/ClassPathMapperScanner.java | 4 +- .../ClassPathMapperScannerAnnotationTest.java | 48 +++++++++++++++++++ .../spring/scancandidate/ScanMapper.java | 21 ++++++++ .../scancandidate/ScanMarkerAnnotation.java | 25 ++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/mybatis/spring/mapper/ClassPathMapperScannerAnnotationTest.java create mode 100644 src/test/java/org/mybatis/spring/scancandidate/ScanMapper.java create mode 100644 src/test/java/org/mybatis/spring/scancandidate/ScanMarkerAnnotation.java diff --git a/src/main/java/org/mybatis/spring/mapper/ClassPathMapperScanner.java b/src/main/java/org/mybatis/spring/mapper/ClassPathMapperScanner.java index e9df9a40c8..244ff70ba9 100644 --- a/src/main/java/org/mybatis/spring/mapper/ClassPathMapperScanner.java +++ b/src/main/java/org/mybatis/spring/mapper/ClassPathMapperScanner.java @@ -424,7 +424,9 @@ private void processBeanDefinitions(Set beanDefinitions) { @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { - return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent(); + var metadata = beanDefinition.getMetadata(); + // annotation types are interfaces too, but must never be treated as mapper candidates + return metadata.isInterface() && metadata.isIndependent() && !metadata.isAnnotation(); } private boolean shouldUseClassConstructorArgument(Class mapperFactoryBeanClass) { diff --git a/src/test/java/org/mybatis/spring/mapper/ClassPathMapperScannerAnnotationTest.java b/src/test/java/org/mybatis/spring/mapper/ClassPathMapperScannerAnnotationTest.java new file mode 100644 index 0000000000..4081ca62b4 --- /dev/null +++ b/src/test/java/org/mybatis/spring/mapper/ClassPathMapperScannerAnnotationTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2026 the original author or authors. + * + * Licensed 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 + * + * https://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.mybatis.spring.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mybatis.spring.scancandidate.ScanMapper; +import org.springframework.context.support.GenericApplicationContext; + +class ClassPathMapperScannerAnnotationTest { + + /** + * Annotation types are interfaces at the bytecode level, so they used to pass {@code isCandidateComponent} and were + * wrongly registered as mapper beans. They must be excluded while regular mapper interfaces in the same package are + * still scanned. See gh-1032. + */ + @Test + void annotationTypeIsNotRegisteredAsMapper() { + try (var applicationContext = new GenericApplicationContext()) { + var scanner = new ClassPathMapperScanner(applicationContext, applicationContext.getEnvironment()); + scanner.registerFilters(); + scanner.scan(ScanMapper.class.getPackageName()); + + var beanDefinitionNames = List.of(applicationContext.getBeanDefinitionNames()); + + // the regular mapper interface is still picked up + assertThat(beanDefinitionNames).contains("scanMapper"); + // the annotation type living in the same package must not be registered as a mapper + assertThat(applicationContext.containsBeanDefinition("scanMarkerAnnotation")).isFalse(); + } + } +} diff --git a/src/test/java/org/mybatis/spring/scancandidate/ScanMapper.java b/src/test/java/org/mybatis/spring/scancandidate/ScanMapper.java new file mode 100644 index 0000000000..ee01b85e09 --- /dev/null +++ b/src/test/java/org/mybatis/spring/scancandidate/ScanMapper.java @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2026 the original author or authors. + * + * Licensed 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 + * + * https://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.mybatis.spring.scancandidate; + +// a regular mapper interface that should be picked up by ClassPathMapperScanner +public interface ScanMapper { + void method(); +} diff --git a/src/test/java/org/mybatis/spring/scancandidate/ScanMarkerAnnotation.java b/src/test/java/org/mybatis/spring/scancandidate/ScanMarkerAnnotation.java new file mode 100644 index 0000000000..25caf623fa --- /dev/null +++ b/src/test/java/org/mybatis/spring/scancandidate/ScanMarkerAnnotation.java @@ -0,0 +1,25 @@ +/* + * Copyright 2010-2026 the original author or authors. + * + * Licensed 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 + * + * https://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.mybatis.spring.scancandidate; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +// an annotation type living in a scanned package; it is an interface at the bytecode level +// but must NOT be registered as a mapper by ClassPathMapperScanner +@Retention(RetentionPolicy.RUNTIME) +public @interface ScanMarkerAnnotation { +}