• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mybatis / spring / 1832

03 May 2026 04:58AM UTC coverage: 90.26% (+0.02%) from 90.242%
1832

Pull #1236

github

web-flow
Merge ed1b6770f into 85605c1e1
Pull Request #1236: Skip mapper scanning in AOT generated runtime

311 of 372 branches covered (83.6%)

936 of 1037 relevant lines covered (90.26%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.59
/src/main/java/org/mybatis/spring/mapper/MapperScannerConfigurer.java
1
/*
2
 * Copyright 2010-2024 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.mybatis.spring.SqlSessionTemplate;
29
import org.springframework.aot.AotDetector;
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.lang.Nullable;
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
1✔
102
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
103

104
  private String basePackage;
105

106
  private boolean addToConfig = true;
1✔
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;
1✔
150
  }
1✔
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
   * </p>
169
   *
170
   * @param lazyInitialization
171
   *          Set the @{code true} to enable
172
   *
173
   * @since 2.0.2
174
   */
175
  public void setLazyInitialization(String lazyInitialization) {
176
    this.lazyInitialization = lazyInitialization;
1✔
177
  }
1✔
178

179
  /**
180
   * This property specifies the annotation that the scanner will search for.
181
   * <p>
182
   * The scanner will register all interfaces in the base package that also have the specified annotation.
183
   * <p>
184
   * Note this can be combined with markerInterface.
185
   *
186
   * @param annotationClass
187
   *          annotation class
188
   */
189
  public void setAnnotationClass(Class<? extends Annotation> annotationClass) {
190
    this.annotationClass = annotationClass;
1✔
191
  }
1✔
192

193
  /**
194
   * This property specifies the parent that the scanner will search for.
195
   * <p>
196
   * The scanner will register all interfaces in the base package that also have the specified interface class as a
197
   * parent.
198
   * <p>
199
   * Note this can be combined with annotationClass.
200
   *
201
   * @param superClass
202
   *          parent class
203
   */
204
  public void setMarkerInterface(Class<?> superClass) {
205
    this.markerInterface = superClass;
1✔
206
  }
1✔
207

208
  /**
209
   * Specifies which types are not eligible for the mapper scanner.
210
   * <p>
211
   * The scanner will exclude types that define with excludeFilters.
212
   *
213
   * @since 3.0.4
214
   *
215
   * @param excludeFilters
216
   *          list of TypeFilter
217
   */
218
  public void setExcludeFilters(List<TypeFilter> excludeFilters) {
219
    this.excludeFilters = excludeFilters;
1✔
220
  }
1✔
221

222
  /**
223
   * In order to support process PropertyPlaceHolders.
224
   * <p>
225
   * After parsed, it will be added to excludeFilters.
226
   *
227
   * @since 3.0.4
228
   *
229
   * @param rawExcludeFilters
230
   *          list of rawExcludeFilter
231
   */
232
  public void setRawExcludeFilters(List<Map<String, String>> rawExcludeFilters) {
233
    this.rawExcludeFilters = rawExcludeFilters;
1✔
234
  }
1✔
235

236
  /**
237
   * Specifies which {@code SqlSessionTemplate} to use in the case that there is more than one in the spring context.
238
   * Usually this is only needed when you have more than one datasource.
239
   * <p>
240
   *
241
   * @deprecated Use {@link #setSqlSessionTemplateBeanName(String)} instead
242
   *
243
   * @param sqlSessionTemplate
244
   *          a template of SqlSession
245
   */
246
  @Deprecated
247
  public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
248
    this.sqlSessionTemplate = sqlSessionTemplate;
×
249
  }
×
250

251
  /**
252
   * Specifies which {@code SqlSessionTemplate} to use in the case that there is more than one in the spring context.
253
   * Usually this is only needed when you have more than one datasource.
254
   * <p>
255
   * Note bean names are used, not bean references. This is because the scanner loads early during the start process and
256
   * it is too early to build mybatis object instances.
257
   *
258
   * @since 1.1.0
259
   *
260
   * @param sqlSessionTemplateName
261
   *          Bean name of the {@code SqlSessionTemplate}
262
   */
263
  public void setSqlSessionTemplateBeanName(String sqlSessionTemplateName) {
264
    this.sqlSessionTemplateBeanName = sqlSessionTemplateName;
1✔
265
  }
1✔
266

267
  /**
268
   * Specifies which {@code SqlSessionFactory} to use in the case that there is more than one in the spring context.
269
   * Usually this is only needed when you have more than one datasource.
270
   * <p>
271
   *
272
   * @deprecated Use {@link #setSqlSessionFactoryBeanName(String)} instead.
273
   *
274
   * @param sqlSessionFactory
275
   *          a factory of SqlSession
276
   */
277
  @Deprecated
278
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
279
    this.sqlSessionFactory = sqlSessionFactory;
×
280
  }
×
281

282
  /**
283
   * Specifies which {@code SqlSessionFactory} to use in the case that there is more than one in the spring context.
284
   * Usually this is only needed when you have more than one datasource.
285
   * <p>
286
   * Note bean names are used, not bean references. This is because the scanner loads early during the start process and
287
   * it is too early to build mybatis object instances.
288
   *
289
   * @since 1.1.0
290
   *
291
   * @param sqlSessionFactoryName
292
   *          Bean name of the {@code SqlSessionFactory}
293
   */
294
  public void setSqlSessionFactoryBeanName(String sqlSessionFactoryName) {
295
    this.sqlSessionFactoryBeanName = sqlSessionFactoryName;
1✔
296
  }
1✔
297

298
  /**
299
   * Specifies a flag that whether execute a property placeholder processing or not.
300
   * <p>
301
   * The default is {@literal false}. This means that a property placeholder processing does not execute.
302
   *
303
   * @since 1.1.1
304
   *
305
   * @param processPropertyPlaceHolders
306
   *          a flag that whether execute a property placeholder processing or not
307
   */
308
  public void setProcessPropertyPlaceHolders(boolean processPropertyPlaceHolders) {
309
    this.processPropertyPlaceHolders = processPropertyPlaceHolders;
1✔
310
  }
1✔
311

312
  /**
313
   * The class of the {@link MapperFactoryBean} to return a mybatis proxy as spring bean.
314
   *
315
   * @param mapperFactoryBeanClass
316
   *          The class of the MapperFactoryBean
317
   *
318
   * @since 2.0.1
319
   */
320
  public void setMapperFactoryBeanClass(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) {
321
    this.mapperFactoryBeanClass = mapperFactoryBeanClass;
1✔
322
  }
1✔
323

324
  @Override
325
  public void setApplicationContext(ApplicationContext applicationContext) {
326
    this.applicationContext = applicationContext;
1✔
327
  }
1✔
328

329
  @Override
330
  public void setBeanName(String name) {
331
    this.beanName = name;
1✔
332
  }
1✔
333

334
  /**
335
   * Gets beanNameGenerator to be used while running the scanner.
336
   *
337
   * @return the beanNameGenerator BeanNameGenerator that has been configured
338
   *
339
   * @since 1.2.0
340
   */
341
  public BeanNameGenerator getNameGenerator() {
342
    return nameGenerator;
×
343
  }
344

345
  /**
346
   * Sets beanNameGenerator to be used while running the scanner.
347
   *
348
   * @param nameGenerator
349
   *          the beanNameGenerator to set
350
   *
351
   * @since 1.2.0
352
   */
353
  public void setNameGenerator(BeanNameGenerator nameGenerator) {
354
    this.nameGenerator = nameGenerator;
1✔
355
  }
1✔
356

357
  /**
358
   * Sets the default scope of scanned mappers.
359
   * <p>
360
   * Default is {@code null} (equiv to singleton).
361
   * </p>
362
   *
363
   * @param defaultScope
364
   *          the default scope
365
   *
366
   * @since 2.0.6
367
   */
368
  public void setDefaultScope(String defaultScope) {
369
    this.defaultScope = defaultScope;
1✔
370
  }
1✔
371

372
  @Override
373
  public void afterPropertiesSet() throws Exception {
374
    notNull(this.basePackage, "Property 'basePackage' is required");
1✔
375
  }
1✔
376

377
  @Override
378
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
379
    // left intentionally blank
380
  }
1✔
381

382
  @Override
383
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
384
    if (this.processPropertyPlaceHolders) {
1✔
385
      processPropertyPlaceHolders();
1✔
386
    }
387

388
    if (AotDetector.useGeneratedArtifacts()) {
1✔
389
      return;
1✔
390
    }
391

392
    var scanner = new ClassPathMapperScanner(registry, getEnvironment());
1✔
393
    scanner.setAddToConfig(this.addToConfig);
1✔
394
    scanner.setAnnotationClass(this.annotationClass);
1✔
395
    scanner.setMarkerInterface(this.markerInterface);
1✔
396
    scanner.setExcludeFilters(this.excludeFilters = mergeExcludeFilters());
1✔
397
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
1✔
398
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
1✔
399
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
1✔
400
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
1✔
401
    scanner.setResourceLoader(this.applicationContext);
1✔
402
    scanner.setBeanNameGenerator(this.nameGenerator);
1✔
403
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
1✔
404
    if (StringUtils.hasText(lazyInitialization)) {
1✔
405
      scanner.setLazyInitialization(Boolean.parseBoolean(lazyInitialization));
1✔
406
    }
407
    if (StringUtils.hasText(defaultScope)) {
1✔
408
      scanner.setDefaultScope(defaultScope);
1✔
409
    }
410
    scanner.registerFilters();
1✔
411
    scanner.scan(
1✔
412
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
1✔
413
  }
1✔
414

415
  /*
416
   * BeanDefinitionRegistries are called early in application startup, before BeanFactoryPostProcessors. This means that
417
   * PropertyResourceConfigurers will not have been loaded and any property substitution of this class' properties will
418
   * fail. To avoid this, find any PropertyResourceConfigurers defined in the context and run them on this class' bean
419
   * definition. Then update the values.
420
   */
421
  private void processPropertyPlaceHolders() {
422
    Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class,
1✔
423
        false, false);
424

425
    if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
1!
426
      var mapperScannerBean = ((ConfigurableApplicationContext) applicationContext).getBeanFactory()
1✔
427
          .getBeanDefinition(beanName);
1✔
428

429
      // PropertyResourceConfigurer does not expose any methods to explicitly perform
430
      // property placeholder substitution. Instead, create a BeanFactory that just
431
      // contains this mapper scanner and post process the factory.
432
      var factory = new DefaultListableBeanFactory();
1✔
433
      factory.registerBeanDefinition(beanName, mapperScannerBean);
1✔
434

435
      for (PropertyResourceConfigurer prc : prcs.values()) {
1✔
436
        prc.postProcessBeanFactory(factory);
1✔
437
      }
1✔
438

439
      PropertyValues values = mapperScannerBean.getPropertyValues();
1✔
440

441
      this.basePackage = getPropertyValue("basePackage", values);
1✔
442
      this.sqlSessionFactoryBeanName = getPropertyValue("sqlSessionFactoryBeanName", values);
1✔
443
      this.sqlSessionTemplateBeanName = getPropertyValue("sqlSessionTemplateBeanName", values);
1✔
444
      this.lazyInitialization = getPropertyValue("lazyInitialization", values);
1✔
445
      this.defaultScope = getPropertyValue("defaultScope", values);
1✔
446
      this.rawExcludeFilters = getPropertyValueForTypeFilter("rawExcludeFilters", values);
1✔
447
    }
448
    this.basePackage = Optional.ofNullable(this.basePackage).map(getEnvironment()::resolvePlaceholders).orElse(null);
1✔
449
    this.sqlSessionFactoryBeanName = Optional.ofNullable(this.sqlSessionFactoryBeanName)
1✔
450
        .map(getEnvironment()::resolvePlaceholders).orElse(null);
1✔
451
    this.sqlSessionTemplateBeanName = Optional.ofNullable(this.sqlSessionTemplateBeanName)
1✔
452
        .map(getEnvironment()::resolvePlaceholders).orElse(null);
1✔
453
    this.lazyInitialization = Optional.ofNullable(this.lazyInitialization).map(getEnvironment()::resolvePlaceholders)
1✔
454
        .orElse(null);
1✔
455
    this.defaultScope = Optional.ofNullable(this.defaultScope).map(getEnvironment()::resolvePlaceholders).orElse(null);
1✔
456
  }
1✔
457

458
  private Environment getEnvironment() {
459
    return this.applicationContext.getEnvironment();
1✔
460
  }
461

462
  private String getPropertyValue(String propertyName, PropertyValues values) {
463
    var property = values.getPropertyValue(propertyName);
1✔
464

465
    if (property == null) {
1✔
466
      return null;
1✔
467
    }
468

469
    var value = property.getValue();
1✔
470

471
    if (value == null) {
1!
472
      return null;
×
473
    }
474
    if (value instanceof String) {
1✔
475
      return value.toString();
1✔
476
    }
477
    if (value instanceof TypedStringValue) {
1!
478
      return ((TypedStringValue) value).getValue();
1✔
479
    }
480
    return null;
×
481
  }
482

483
  @SuppressWarnings("unchecked")
484
  private List<Map<String, String>> getPropertyValueForTypeFilter(String propertyName, PropertyValues values) {
485
    var property = values.getPropertyValue(propertyName);
1✔
486
    Object value;
487
    if (property == null || (value = property.getValue()) == null || !(value instanceof List<?>)) {
1!
488
      return null;
1✔
489
    }
490
    return (List<Map<String, String>>) value;
1✔
491
  }
492

493
  private List<TypeFilter> mergeExcludeFilters() {
494
    List<TypeFilter> typeFilters = new ArrayList<>();
1✔
495
    if (this.rawExcludeFilters == null || this.rawExcludeFilters.isEmpty()) {
1✔
496
      return this.excludeFilters;
1✔
497
    }
498
    if (this.excludeFilters != null && !this.excludeFilters.isEmpty()) {
1✔
499
      typeFilters.addAll(this.excludeFilters);
1✔
500
    }
501
    try {
502
      for (Map<String, String> typeFilter : this.rawExcludeFilters) {
1✔
503
        typeFilters.add(
1✔
504
            createTypeFilter(typeFilter.get("type"), typeFilter.get("expression"), this.getClass().getClassLoader()));
1✔
505
      }
1✔
506
    } catch (ClassNotFoundException exception) {
1✔
507
      throw new RuntimeException("ClassNotFoundException occur when to load the Specified excludeFilter classes.",
1✔
508
          exception);
509
    }
1✔
510
    return typeFilters;
1✔
511
  }
512

513
  @SuppressWarnings("unchecked")
514
  private TypeFilter createTypeFilter(String filterType, String expression, @Nullable ClassLoader classLoader)
515
      throws ClassNotFoundException {
516

517
    if (this.processPropertyPlaceHolders) {
1✔
518
      expression = this.getEnvironment().resolvePlaceholders(expression);
1✔
519
    }
520

521
    switch (filterType) {
1!
522
      case "annotation":
523
        Class<?> filterAnno = ClassUtils.forName(expression, classLoader);
1✔
524
        if (!Annotation.class.isAssignableFrom(filterAnno)) {
1✔
525
          throw new IllegalArgumentException(
1✔
526
              "Class is not assignable to [" + Annotation.class.getName() + "]: " + expression);
1✔
527
        }
528
        return new AnnotationTypeFilter((Class<Annotation>) filterAnno);
1✔
529
      case "custom":
530
        Class<?> filterClass = ClassUtils.forName(expression, classLoader);
1✔
531
        if (!TypeFilter.class.isAssignableFrom(filterClass)) {
1✔
532
          throw new IllegalArgumentException(
1✔
533
              "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression);
1✔
534
        }
535
        return (TypeFilter) BeanUtils.instantiateClass(filterClass);
1✔
536
      case "assignable":
537
        return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader));
1✔
538
      case "regex":
539
        return new RegexPatternTypeFilter(Pattern.compile(expression));
1✔
540
      case "aspectj":
541
        return new AspectJTypeFilter(expression, classLoader);
1✔
542
      default:
543
        throw new IllegalArgumentException("Unsupported filter type: " + filterType);
×
544
    }
545
  }
546

547
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc