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 static org.springframework.util.Assert.notNull;
19  
20  import java.lang.annotation.Annotation;
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Optional;
25  import java.util.regex.Pattern;
26  
27  import org.apache.ibatis.session.SqlSessionFactory;
28  import org.jspecify.annotations.Nullable;
29  import org.mybatis.spring.SqlSessionTemplate;
30  import org.springframework.aot.AotDetector;
31  import org.springframework.beans.BeanUtils;
32  import org.springframework.beans.PropertyValues;
33  import org.springframework.beans.factory.BeanNameAware;
34  import org.springframework.beans.factory.InitializingBean;
35  import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
36  import org.springframework.beans.factory.config.PropertyResourceConfigurer;
37  import org.springframework.beans.factory.config.TypedStringValue;
38  import org.springframework.beans.factory.support.BeanDefinitionRegistry;
39  import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
40  import org.springframework.beans.factory.support.BeanNameGenerator;
41  import org.springframework.beans.factory.support.DefaultListableBeanFactory;
42  import org.springframework.context.ApplicationContext;
43  import org.springframework.context.ApplicationContextAware;
44  import org.springframework.context.ConfigurableApplicationContext;
45  import org.springframework.core.env.Environment;
46  import org.springframework.core.type.filter.AnnotationTypeFilter;
47  import org.springframework.core.type.filter.AspectJTypeFilter;
48  import org.springframework.core.type.filter.AssignableTypeFilter;
49  import org.springframework.core.type.filter.RegexPatternTypeFilter;
50  import org.springframework.core.type.filter.TypeFilter;
51  import org.springframework.util.ClassUtils;
52  import org.springframework.util.StringUtils;
53  
54  /**
55   * BeanDefinitionRegistryPostProcessor that searches recursively starting from a base package for interfaces and
56   * registers them as {@code MapperFactoryBean}. Note that only interfaces with at least one method will be registered;
57   * concrete classes will be ignored.
58   * <p>
59   * This class was a {code BeanFactoryPostProcessor} until 1.0.1 version. It changed to
60   * {@code BeanDefinitionRegistryPostProcessor} in 1.0.2. See https://jira.springsource.org/browse/SPR-8269 for the
61   * details.
62   * <p>
63   * The {@code basePackage} property can contain more than one package name, separated by either commas or semicolons.
64   * <p>
65   * This class supports filtering the mappers created by either specifying a marker interface or an annotation. The
66   * {@code annotationClass} property specifies an annotation to search for. The {@code markerInterface} property
67   * specifies a parent interface to search for. If both properties are specified, mappers are added for interfaces that
68   * match <em>either</em> criteria. By default, these two properties are null, so all interfaces in the given
69   * {@code basePackage} are added as mappers.
70   * <p>
71   * This configurer enables autowire for all the beans that it creates so that they are automatically autowired with the
72   * proper {@code SqlSessionFactory} or {@code SqlSessionTemplate}. If there is more than one {@code SqlSessionFactory}
73   * in the application, however, autowiring cannot be used. In this case you must explicitly specify either an
74   * {@code SqlSessionFactory} or an {@code SqlSessionTemplate} to use via the <em>bean name</em> properties. Bean names
75   * are used rather than actual objects because Spring does not initialize property placeholders until after this class
76   * is processed.
77   * <p>
78   * Passing in an actual object which may require placeholders (i.e. DB user password) will fail. Using bean names defers
79   * actual object creation until later in the startup process, after all placeholder substitution is completed. However,
80   * note that this configurer does support property placeholders of its <em>own</em> properties. The
81   * <code>basePackage</code> and bean name properties all support <code>${property}</code> style substitution.
82   * <p>
83   * Configuration sample:
84   *
85   * <pre class="code">
86   * {@code
87   *   <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
88   *       <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
89   *       <!-- optional unless there are multiple session factories defined -->
90   *       <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
91   *   </bean>
92   * }
93   * </pre>
94   *
95   * @author Hunter Presnall
96   * @author Eduardo Macarron
97   *
98   * @see MapperFactoryBean
99   * @see ClassPathMapperScanner
100  */
101 public class MapperScannerConfigurer
102     implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
103 
104   private String basePackage;
105 
106   private boolean addToConfig = true;
107 
108   private String lazyInitialization;
109 
110   private SqlSessionFactory sqlSessionFactory;
111 
112   private SqlSessionTemplate sqlSessionTemplate;
113 
114   private String sqlSessionFactoryBeanName;
115 
116   private String sqlSessionTemplateBeanName;
117 
118   private Class<? extends Annotation> annotationClass;
119 
120   private Class<?> markerInterface;
121 
122   private List<TypeFilter> excludeFilters;
123 
124   private List<Map<String, String>> rawExcludeFilters;
125 
126   private Class<? extends MapperFactoryBean> mapperFactoryBeanClass;
127 
128   private ApplicationContext applicationContext;
129 
130   private String beanName;
131 
132   private boolean processPropertyPlaceHolders;
133 
134   private BeanNameGenerator nameGenerator;
135 
136   private String defaultScope;
137 
138   /**
139    * This property lets you set the base package for your mapper interface files.
140    * <p>
141    * You can set more than one package by using a semicolon or comma as a separator.
142    * <p>
143    * Mappers will be searched for recursively starting in the specified package(s).
144    *
145    * @param basePackage
146    *          base package name
147    */
148   public void setBasePackage(String basePackage) {
149     this.basePackage = basePackage;
150   }
151 
152   /**
153    * Same as {@code MapperFactoryBean#setAddToConfig(boolean)}.
154    *
155    * @param addToConfig
156    *          a flag that whether add mapper to MyBatis or not
157    *
158    * @see MapperFactoryBean#setAddToConfig(boolean)
159    */
160   public void setAddToConfig(boolean addToConfig) {
161     this.addToConfig = addToConfig;
162   }
163 
164   /**
165    * Set whether enable lazy initialization for mapper bean.
166    * <p>
167    * Default is {@code false}.
168    *
169    * @param lazyInitialization
170    *          Set the @{code true} to enable
171    *
172    * @since 2.0.2
173    */
174   public void setLazyInitialization(String lazyInitialization) {
175     this.lazyInitialization = lazyInitialization;
176   }
177 
178   /**
179    * This property specifies the annotation that the scanner will search for.
180    * <p>
181    * The scanner will register all interfaces in the base package that also have the specified annotation.
182    * <p>
183    * Note this can be combined with markerInterface.
184    *
185    * @param annotationClass
186    *          annotation class
187    */
188   public void setAnnotationClass(Class<? extends Annotation> annotationClass) {
189     this.annotationClass = annotationClass;
190   }
191 
192   /**
193    * This property specifies the parent that the scanner will search for.
194    * <p>
195    * The scanner will register all interfaces in the base package that also have the specified interface class as a
196    * parent.
197    * <p>
198    * Note this can be combined with annotationClass.
199    *
200    * @param superClass
201    *          parent class
202    */
203   public void setMarkerInterface(Class<?> superClass) {
204     this.markerInterface = superClass;
205   }
206 
207   /**
208    * Specifies which types are not eligible for the mapper scanner.
209    * <p>
210    * The scanner will exclude types that define with excludeFilters.
211    *
212    * @since 3.0.4
213    *
214    * @param excludeFilters
215    *          list of TypeFilter
216    */
217   public void setExcludeFilters(List<TypeFilter> excludeFilters) {
218     this.excludeFilters = excludeFilters;
219   }
220 
221   /**
222    * In order to support process PropertyPlaceHolders.
223    * <p>
224    * After parsed, it will be added to excludeFilters.
225    *
226    * @since 3.0.4
227    *
228    * @param rawExcludeFilters
229    *          list of rawExcludeFilter
230    */
231   public void setRawExcludeFilters(List<Map<String, String>> rawExcludeFilters) {
232     this.rawExcludeFilters = rawExcludeFilters;
233   }
234 
235   /**
236    * Specifies which {@code SqlSessionTemplate} to use in the case that there is more than one in the spring context.
237    * Usually this is only needed when you have more than one datasource.
238    *
239    * @deprecated Use {@link #setSqlSessionTemplateBeanName(String)} instead
240    *
241    * @param sqlSessionTemplate
242    *          a template of SqlSession
243    */
244   @Deprecated
245   public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
246     this.sqlSessionTemplate = sqlSessionTemplate;
247   }
248 
249   /**
250    * Specifies which {@code SqlSessionTemplate} to use in the case that there is more than one in the spring context.
251    * Usually this is only needed when you have more than one datasource.
252    * <p>
253    * Note bean names are used, not bean references. This is because the scanner loads early during the start process and
254    * it is too early to build mybatis object instances.
255    *
256    * @since 1.1.0
257    *
258    * @param sqlSessionTemplateName
259    *          Bean name of the {@code SqlSessionTemplate}
260    */
261   public void setSqlSessionTemplateBeanName(String sqlSessionTemplateName) {
262     this.sqlSessionTemplateBeanName = sqlSessionTemplateName;
263   }
264 
265   /**
266    * Specifies which {@code SqlSessionFactory} to use in the case that there is more than one in the spring context.
267    * Usually this is only needed when you have more than one datasource.
268    *
269    * @deprecated Use {@link #setSqlSessionFactoryBeanName(String)} instead.
270    *
271    * @param sqlSessionFactory
272    *          a factory of SqlSession
273    */
274   @Deprecated
275   public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
276     this.sqlSessionFactory = sqlSessionFactory;
277   }
278 
279   /**
280    * Specifies which {@code SqlSessionFactory} to use in the case that there is more than one in the spring context.
281    * Usually this is only needed when you have more than one datasource.
282    * <p>
283    * Note bean names are used, not bean references. This is because the scanner loads early during the start process and
284    * it is too early to build mybatis object instances.
285    *
286    * @since 1.1.0
287    *
288    * @param sqlSessionFactoryName
289    *          Bean name of the {@code SqlSessionFactory}
290    */
291   public void setSqlSessionFactoryBeanName(String sqlSessionFactoryName) {
292     this.sqlSessionFactoryBeanName = sqlSessionFactoryName;
293   }
294 
295   /**
296    * Specifies a flag that whether execute a property placeholder processing or not.
297    * <p>
298    * The default is {@literal false}. This means that a property placeholder processing does not execute.
299    *
300    * @since 1.1.1
301    *
302    * @param processPropertyPlaceHolders
303    *          a flag that whether execute a property placeholder processing or not
304    */
305   public void setProcessPropertyPlaceHolders(boolean processPropertyPlaceHolders) {
306     this.processPropertyPlaceHolders = processPropertyPlaceHolders;
307   }
308 
309   /**
310    * The class of the {@link MapperFactoryBean} to return a mybatis proxy as spring bean.
311    *
312    * @param mapperFactoryBeanClass
313    *          The class of the MapperFactoryBean
314    *
315    * @since 2.0.1
316    */
317   public void setMapperFactoryBeanClass(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) {
318     this.mapperFactoryBeanClass = mapperFactoryBeanClass;
319   }
320 
321   @Override
322   public void setApplicationContext(ApplicationContext applicationContext) {
323     this.applicationContext = applicationContext;
324   }
325 
326   @Override
327   public void setBeanName(String name) {
328     this.beanName = name;
329   }
330 
331   /**
332    * Gets beanNameGenerator to be used while running the scanner.
333    *
334    * @return the beanNameGenerator BeanNameGenerator that has been configured
335    *
336    * @since 1.2.0
337    */
338   public BeanNameGenerator getNameGenerator() {
339     return nameGenerator;
340   }
341 
342   /**
343    * Sets beanNameGenerator to be used while running the scanner.
344    *
345    * @param nameGenerator
346    *          the beanNameGenerator to set
347    *
348    * @since 1.2.0
349    */
350   public void setNameGenerator(BeanNameGenerator nameGenerator) {
351     this.nameGenerator = nameGenerator;
352   }
353 
354   /**
355    * Sets the default scope of scanned mappers.
356    * <p>
357    * Default is {@code null} (equiv to singleton).
358    *
359    * @param defaultScope
360    *          the default scope
361    *
362    * @since 2.0.6
363    */
364   public void setDefaultScope(String defaultScope) {
365     this.defaultScope = defaultScope;
366   }
367 
368   @Override
369   public void afterPropertiesSet() throws Exception {
370     notNull(this.basePackage, "Property 'basePackage' is required");
371   }
372 
373   @Override
374   public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
375     // left intentionally blank
376   }
377 
378   @Override
379   public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
380     if (this.processPropertyPlaceHolders) {
381       processPropertyPlaceHolders();
382     }
383 
384     if (AotDetector.useGeneratedArtifacts()) {
385       return;
386     }
387 
388     var scanner = new ClassPathMapperScanner(registry, getEnvironment());
389     scanner.setAddToConfig(this.addToConfig);
390     scanner.setAnnotationClass(this.annotationClass);
391     scanner.setMarkerInterface(this.markerInterface);
392     scanner.setExcludeFilters(this.excludeFilters = mergeExcludeFilters());
393     scanner.setSqlSessionFactory(this.sqlSessionFactory);
394     scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
395     scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
396     scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
397     scanner.setResourceLoader(this.applicationContext);
398     scanner.setBeanNameGenerator(this.nameGenerator);
399     scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
400     if (StringUtils.hasText(lazyInitialization)) {
401       scanner.setLazyInitialization(Boolean.parseBoolean(lazyInitialization));
402     }
403     if (StringUtils.hasText(defaultScope)) {
404       scanner.setDefaultScope(defaultScope);
405     }
406     scanner.registerFilters();
407     scanner.scan(
408         StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
409   }
410 
411   /*
412    * BeanDefinitionRegistries are called early in application startup, before BeanFactoryPostProcessors. This means that
413    * PropertyResourceConfigurers will not have been loaded and any property substitution of this class' properties will
414    * fail. To avoid this, find any PropertyResourceConfigurers defined in the context and run them on this class' bean
415    * definition. Then update the values.
416    */
417   private void processPropertyPlaceHolders() {
418     Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class,
419         false, false);
420 
421     if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
422       var mapperScannerBean = ((ConfigurableApplicationContext) applicationContext).getBeanFactory()
423           .getBeanDefinition(beanName);
424 
425       // PropertyResourceConfigurer does not expose any methods to explicitly perform
426       // property placeholder substitution. Instead, create a BeanFactory that just
427       // contains this mapper scanner and post process the factory.
428       var factory = new DefaultListableBeanFactory();
429       factory.registerBeanDefinition(beanName, mapperScannerBean);
430 
431       for (PropertyResourceConfigurer prc : prcs.values()) {
432         prc.postProcessBeanFactory(factory);
433       }
434 
435       PropertyValues values = mapperScannerBean.getPropertyValues();
436 
437       this.basePackage = getPropertyValue("basePackage", values);
438       this.sqlSessionFactoryBeanName = getPropertyValue("sqlSessionFactoryBeanName", values);
439       this.sqlSessionTemplateBeanName = getPropertyValue("sqlSessionTemplateBeanName", values);
440       this.lazyInitialization = getPropertyValue("lazyInitialization", values);
441       this.defaultScope = getPropertyValue("defaultScope", values);
442       this.rawExcludeFilters = getPropertyValueForTypeFilter("rawExcludeFilters", values);
443     }
444     this.basePackage = Optional.ofNullable(this.basePackage).map(getEnvironment()::resolvePlaceholders).orElse(null);
445     this.sqlSessionFactoryBeanName = Optional.ofNullable(this.sqlSessionFactoryBeanName)
446         .map(getEnvironment()::resolvePlaceholders).orElse(null);
447     this.sqlSessionTemplateBeanName = Optional.ofNullable(this.sqlSessionTemplateBeanName)
448         .map(getEnvironment()::resolvePlaceholders).orElse(null);
449     this.lazyInitialization = Optional.ofNullable(this.lazyInitialization).map(getEnvironment()::resolvePlaceholders)
450         .orElse(null);
451     this.defaultScope = Optional.ofNullable(this.defaultScope).map(getEnvironment()::resolvePlaceholders).orElse(null);
452   }
453 
454   private Environment getEnvironment() {
455     return this.applicationContext.getEnvironment();
456   }
457 
458   private String getPropertyValue(String propertyName, PropertyValues values) {
459     var property = values.getPropertyValue(propertyName);
460 
461     if (property == null) {
462       return null;
463     }
464 
465     var value = property.getValue();
466 
467     if (value == null) {
468       return null;
469     }
470     if (value instanceof String) {
471       return value.toString();
472     }
473     if (value instanceof TypedStringValue) {
474       return ((TypedStringValue) value).getValue();
475     }
476     return null;
477   }
478 
479   @SuppressWarnings("unchecked")
480   private List<Map<String, String>> getPropertyValueForTypeFilter(String propertyName, PropertyValues values) {
481     var property = values.getPropertyValue(propertyName);
482     Object value;
483     if (property == null || (value = property.getValue()) == null || !(value instanceof List<?>)) {
484       return null;
485     }
486     return (List<Map<String, String>>) value;
487   }
488 
489   private List<TypeFilter> mergeExcludeFilters() {
490     List<TypeFilter> typeFilters = new ArrayList<>();
491     if (this.rawExcludeFilters == null || this.rawExcludeFilters.isEmpty()) {
492       return this.excludeFilters;
493     }
494     if (this.excludeFilters != null && !this.excludeFilters.isEmpty()) {
495       typeFilters.addAll(this.excludeFilters);
496     }
497     try {
498       for (Map<String, String> typeFilter : this.rawExcludeFilters) {
499         typeFilters.add(
500             createTypeFilter(typeFilter.get("type"), typeFilter.get("expression"), this.getClass().getClassLoader()));
501       }
502     } catch (ClassNotFoundException exception) {
503       throw new RuntimeException("ClassNotFoundException occur when to load the Specified excludeFilter classes.",
504           exception);
505     }
506     return typeFilters;
507   }
508 
509   @SuppressWarnings("unchecked")
510   private TypeFilter createTypeFilter(String filterType, String expression, @Nullable ClassLoader classLoader)
511       throws ClassNotFoundException {
512 
513     if (this.processPropertyPlaceHolders) {
514       expression = this.getEnvironment().resolvePlaceholders(expression);
515     }
516 
517     switch (filterType) {
518       case "annotation":
519         Class<?> filterAnno = ClassUtils.forName(expression, classLoader);
520         if (!Annotation.class.isAssignableFrom(filterAnno)) {
521           throw new IllegalArgumentException(
522               "Class is not assignable to [" + Annotation.class.getName() + "]: " + expression);
523         }
524         return new AnnotationTypeFilter((Class<Annotation>) filterAnno);
525       case "custom":
526         Class<?> filterClass = ClassUtils.forName(expression, classLoader);
527         if (!TypeFilter.class.isAssignableFrom(filterClass)) {
528           throw new IllegalArgumentException(
529               "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression);
530         }
531         return (TypeFilter) BeanUtils.instantiateClass(filterClass);
532       case "assignable":
533         return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader));
534       case "regex":
535         return new RegexPatternTypeFilter(Pattern.compile(expression));
536       case "aspectj":
537         return new AspectJTypeFilter(expression, classLoader);
538       default:
539         throw new IllegalArgumentException("Unsupported filter type: " + filterType);
540     }
541   }
542 
543 }