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