View Javadoc
1   /*
2    * Copyright 2010-2026 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.mybatis.spring.mapper;
17  
18  import java.lang.annotation.Annotation;
19  import java.util.Arrays;
20  import java.util.List;
21  import java.util.Optional;
22  import java.util.Set;
23  
24  import org.apache.ibatis.io.Resources;
25  import org.apache.ibatis.session.SqlSessionFactory;
26  import org.mybatis.logging.Logger;
27  import org.mybatis.logging.LoggerFactory;
28  import org.mybatis.spring.SqlSessionTemplate;
29  import org.springframework.aop.scope.ScopedProxyFactoryBean;
30  import org.springframework.aop.scope.ScopedProxyUtils;
31  import org.springframework.aot.AotDetector;
32  import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
33  import org.springframework.beans.factory.config.BeanDefinition;
34  import org.springframework.beans.factory.config.BeanDefinitionHolder;
35  import org.springframework.beans.factory.config.ConfigurableBeanFactory;
36  import org.springframework.beans.factory.config.ConstructorArgumentValues;
37  import org.springframework.beans.factory.config.RuntimeBeanReference;
38  import org.springframework.beans.factory.support.AbstractBeanDefinition;
39  import org.springframework.beans.factory.support.BeanDefinitionRegistry;
40  import org.springframework.beans.factory.support.RootBeanDefinition;
41  import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
42  import org.springframework.core.NativeDetector;
43  import org.springframework.core.env.Environment;
44  import org.springframework.core.type.filter.AnnotationTypeFilter;
45  import org.springframework.core.type.filter.AssignableTypeFilter;
46  import org.springframework.core.type.filter.TypeFilter;
47  import org.springframework.util.StringUtils;
48  
49  /**
50   * A {@link ClassPathBeanDefinitionScanner} that registers Mappers by {@code basePackage}, {@code annotationClass}, or
51   * {@code markerInterface}. If an {@code annotationClass} and/or {@code markerInterface} is specified, only the
52   * specified types will be searched (searching for all interfaces will be disabled).
53   * <p>
54   * This functionality was previously a private class of {@link MapperScannerConfigurer}, but was broken out in version
55   * 1.2.0.
56   *
57   * @author Hunter Presnall
58   * @author Eduardo Macarron
59   *
60   * @see MapperFactoryBean
61   *
62   * @since 1.2.0
63   */
64  public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
65  
66    private static final Logger LOGGER = LoggerFactory.getLogger(ClassPathMapperScanner.class);
67  
68    // Copy of FactoryBean#OBJECT_TYPE_ATTRIBUTE which was added in Spring 5.2
69    static final String FACTORY_BEAN_OBJECT_TYPE = "factoryBeanObjectType";
70  
71    private boolean addToConfig = true;
72  
73    private boolean lazyInitialization;
74  
75    private boolean printWarnLogIfNotFoundMappers = true;
76  
77    private SqlSessionFactory sqlSessionFactory;
78  
79    private SqlSessionTemplate sqlSessionTemplate;
80  
81    private String sqlSessionTemplateBeanName;
82  
83    private String sqlSessionFactoryBeanName;
84  
85    private Class<? extends Annotation> annotationClass;
86  
87    private Class<?> markerInterface;
88  
89    private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;
90  
91    private String defaultScope;
92    private List<TypeFilter> excludeFilters;
93  
94    /**
95     * Instantiates a new class path mapper scanner.
96     *
97     * @param registry
98     *          the registry
99     * @param environment
100    *          the environment
101    */
102   public ClassPathMapperScanner(BeanDefinitionRegistry registry, Environment environment) {
103     super(registry, false, environment);
104     setIncludeAnnotationConfig(!AotDetector.useGeneratedArtifacts());
105     setPrintWarnLogIfNotFoundMappers(!NativeDetector.inNativeImage());
106   }
107 
108   /**
109    * Instantiates a new class path mapper scanner.
110    *
111    * @param registry
112    *          the registry
113    *
114    * @deprecated Please use the {@link #ClassPathMapperScanner(BeanDefinitionRegistry, Environment)}.
115    */
116   @Deprecated(since = "3.0.4", forRemoval = true)
117   public ClassPathMapperScanner(BeanDefinitionRegistry registry) {
118     super(registry, false);
119     setIncludeAnnotationConfig(!AotDetector.useGeneratedArtifacts());
120     setPrintWarnLogIfNotFoundMappers(!NativeDetector.inNativeImage());
121   }
122 
123   /**
124    * Sets the adds the to config.
125    *
126    * @param addToConfig
127    *          the new adds the to config
128    */
129   public void setAddToConfig(boolean addToConfig) {
130     this.addToConfig = addToConfig;
131   }
132 
133   /**
134    * Sets the annotation class.
135    *
136    * @param annotationClass
137    *          the new annotation class
138    */
139   public void setAnnotationClass(Class<? extends Annotation> annotationClass) {
140     this.annotationClass = annotationClass;
141   }
142 
143   /**
144    * Set whether enable lazy initialization for mapper bean.
145    * <p>
146    * Default is {@code false}.
147    *
148    * @param lazyInitialization
149    *          Set the @{code true} to enable
150    *
151    * @since 2.0.2
152    */
153   public void setLazyInitialization(boolean lazyInitialization) {
154     this.lazyInitialization = lazyInitialization;
155   }
156 
157   /**
158    * Set whether print warning log if not found mappers that matches conditions.
159    * <p>
160    * Default is {@code true}. But {@code false} when running in native image.
161    *
162    * @param printWarnLogIfNotFoundMappers
163    *          Set the @{code true} to print
164    *
165    * @since 3.0.1
166    */
167   public void setPrintWarnLogIfNotFoundMappers(boolean printWarnLogIfNotFoundMappers) {
168     this.printWarnLogIfNotFoundMappers = printWarnLogIfNotFoundMappers;
169   }
170 
171   /**
172    * Sets the marker interface.
173    *
174    * @param markerInterface
175    *          the new marker interface
176    */
177   public void setMarkerInterface(Class<?> markerInterface) {
178     this.markerInterface = markerInterface;
179   }
180 
181   /**
182    * Sets the exclude filters.
183    *
184    * @param excludeFilters
185    *          the new exclude filters
186    */
187   public void setExcludeFilters(List<TypeFilter> excludeFilters) {
188     this.excludeFilters = excludeFilters;
189   }
190 
191   /**
192    * Sets the sql session factory.
193    *
194    * @param sqlSessionFactory
195    *          the new sql session factory
196    */
197   public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
198     this.sqlSessionFactory = sqlSessionFactory;
199   }
200 
201   /**
202    * Sets the sql session template.
203    *
204    * @param sqlSessionTemplate
205    *          the new sql session template
206    */
207   public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
208     this.sqlSessionTemplate = sqlSessionTemplate;
209   }
210 
211   /**
212    * Sets the sql session template bean name.
213    *
214    * @param sqlSessionTemplateBeanName
215    *          the new sql session template bean name
216    */
217   public void setSqlSessionTemplateBeanName(String sqlSessionTemplateBeanName) {
218     this.sqlSessionTemplateBeanName = sqlSessionTemplateBeanName;
219   }
220 
221   /**
222    * Sets the sql session factory bean name.
223    *
224    * @param sqlSessionFactoryBeanName
225    *          the new sql session factory bean name
226    */
227   public void setSqlSessionFactoryBeanName(String sqlSessionFactoryBeanName) {
228     this.sqlSessionFactoryBeanName = sqlSessionFactoryBeanName;
229   }
230 
231   /**
232    * Sets the mapper factory bean.
233    *
234    * @param mapperFactoryBean
235    *          the new mapper factory bean
236    *
237    * @deprecated Since 2.0.1, Please use the {@link #setMapperFactoryBeanClass(Class)}.
238    */
239   @Deprecated(since = "2.0.1", forRemoval = true)
240   public void setMapperFactoryBean(MapperFactoryBean<?> mapperFactoryBean) {
241     this.mapperFactoryBeanClass = mapperFactoryBean == null ? MapperFactoryBean.class : mapperFactoryBean.getClass();
242   }
243 
244   /**
245    * Set the {@code MapperFactoryBean} class.
246    *
247    * @param mapperFactoryBeanClass
248    *          the {@code MapperFactoryBean} class
249    *
250    * @since 2.0.1
251    */
252   public void setMapperFactoryBeanClass(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) {
253     this.mapperFactoryBeanClass = mapperFactoryBeanClass == null ? MapperFactoryBean.class : mapperFactoryBeanClass;
254   }
255 
256   /**
257    * Set the default scope of scanned mappers.
258    * <p>
259    * Default is {@code null} (equiv to singleton).
260    *
261    * @param defaultScope
262    *          the scope
263    *
264    * @since 2.0.6
265    */
266   public void setDefaultScope(String defaultScope) {
267     this.defaultScope = defaultScope;
268   }
269 
270   /**
271    * Configures parent scanner to search for the right interfaces. It can search for all interfaces or just for those
272    * that extends a markerInterface or/and those annotated with the annotationClass
273    */
274   public void registerFilters() {
275     var acceptAllInterfaces = true;
276 
277     // if specified, use the given annotation and / or marker interface
278     if (this.annotationClass != null) {
279       addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
280       acceptAllInterfaces = false;
281     }
282 
283     // override AssignableTypeFilter to ignore matches on the actual marker interface
284     if (this.markerInterface != null) {
285       addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
286         @Override
287         protected boolean matchClassName(String className) {
288           return false;
289         }
290       });
291       acceptAllInterfaces = false;
292     }
293 
294     if (acceptAllInterfaces) {
295       // default include filter that accepts all classes
296       addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
297     }
298 
299     // exclude package-info.java
300     addExcludeFilter((metadataReader, metadataReaderFactory) -> {
301       var className = metadataReader.getClassMetadata().getClassName();
302       return className.endsWith("package-info");
303     });
304 
305     // exclude types declared by MapperScan.excludeFilters
306     if (excludeFilters != null && excludeFilters.size() > 0) {
307       for (TypeFilter excludeFilter : excludeFilters) {
308         addExcludeFilter(excludeFilter);
309       }
310     }
311   }
312 
313   /**
314    * Calls the parent search that will search and register all the candidates. Then the registered objects are post
315    * processed to set them as MapperFactoryBeans
316    */
317   @Override
318   public Set<BeanDefinitionHolder> doScan(String... basePackages) {
319     var beanDefinitions = super.doScan(basePackages);
320 
321     if (beanDefinitions.isEmpty()) {
322       if (printWarnLogIfNotFoundMappers) {
323         LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
324             + "' package. Please check your configuration.");
325       }
326     } else {
327       processBeanDefinitions(beanDefinitions);
328     }
329 
330     return beanDefinitions;
331   }
332 
333   private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
334     AbstractBeanDefinition definition;
335     var registry = getRegistry();
336     for (BeanDefinitionHolder holder : beanDefinitions) {
337       definition = (AbstractBeanDefinition) holder.getBeanDefinition();
338       var scopedProxy = false;
339       if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
340         definition = (AbstractBeanDefinition) Optional
341             .ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition())
342             .map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException(
343                 "The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]"));
344         scopedProxy = true;
345       }
346       var beanClassName = definition.getBeanClassName();
347       LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
348           + "' mapperInterface");
349 
350       // the mapper interface is the original class of the bean
351       // but, the actual class of the bean is MapperFactoryBean
352       var constructorArgument = new ConstructorArgumentValues.ValueHolder(beanClassName); // issue #59
353       definition.getConstructorArgumentValues().addGenericArgumentValue(constructorArgument);
354       try {
355         Class<?> beanClass = Resources.classForName(beanClassName);
356         if (shouldUseClassConstructorArgument(this.mapperFactoryBeanClass)) {
357           constructorArgument.setValue(beanClass);
358         }
359         // Attribute for MockitoPostProcessor
360         // https://github.com/mybatis/spring-boot-starter/issues/475
361         definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClass);
362         // for spring-native
363         definition.getPropertyValues().add("mapperInterface", beanClass);
364       } catch (ClassNotFoundException ignore) {
365         // ignore
366       }
367 
368       definition.setBeanClass(this.mapperFactoryBeanClass);
369 
370       definition.getPropertyValues().add("addToConfig", this.addToConfig);
371 
372       var explicitFactoryUsed = false;
373       if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
374         definition.getPropertyValues().add("sqlSessionFactory",
375             new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
376         explicitFactoryUsed = true;
377       } else if (this.sqlSessionFactory != null) {
378         definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
379         explicitFactoryUsed = true;
380       }
381 
382       if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
383         if (explicitFactoryUsed) {
384           LOGGER.warn(
385               () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
386         }
387         definition.getPropertyValues().add("sqlSessionTemplate",
388             new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
389         explicitFactoryUsed = true;
390       } else if (this.sqlSessionTemplate != null) {
391         if (explicitFactoryUsed) {
392           LOGGER.warn(
393               () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
394         }
395         definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
396         explicitFactoryUsed = true;
397       }
398 
399       if (!explicitFactoryUsed) {
400         LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
401         definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
402       }
403 
404       definition.setLazyInit(lazyInitialization);
405 
406       if (scopedProxy) {
407         continue;
408       }
409 
410       if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
411         definition.setScope(defaultScope);
412       }
413 
414       if (!definition.isSingleton()) {
415         var proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
416         if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
417           registry.removeBeanDefinition(proxyHolder.getBeanName());
418         }
419         registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
420       }
421 
422     }
423   }
424 
425   @Override
426   protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
427     var metadata = beanDefinition.getMetadata();
428     // annotation types are interfaces too, but must never be treated as mapper candidates
429     return metadata.isInterface() && metadata.isIndependent() && !metadata.isAnnotation();
430   }
431 
432   private boolean shouldUseClassConstructorArgument(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) {
433     return hasSingleArgumentConstructorAccepting(mapperFactoryBeanClass, Class.class)
434         && !hasSingleArgumentConstructorAccepting(mapperFactoryBeanClass, String.class);
435   }
436 
437   private boolean hasSingleArgumentConstructorAccepting(Class<? extends MapperFactoryBean> mapperFactoryBeanClass,
438       Class<?> argumentType) {
439     return Arrays.stream(mapperFactoryBeanClass.getDeclaredConstructors())
440         .anyMatch(constructor -> constructor.getParameterCount() == 1
441             && constructor.getParameterTypes()[0].isAssignableFrom(argumentType));
442   }
443 
444   @Override
445   protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
446     if (super.checkCandidate(beanName, beanDefinition)) {
447       return true;
448     }
449     LOGGER.warn(() -> "Skipping MapperFactoryBean with name '" + beanName + "' and '"
450         + beanDefinition.getBeanClassName() + "' mapperInterface" + ". Bean already defined with the same name!");
451     return false;
452   }
453 
454 }