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