MapperScannerConfigurer.java
/*
* Copyright 2010-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.spring.mapper;
import static org.springframework.util.Assert.notNull;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyResourceConfigurer;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AspectJTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* BeanDefinitionRegistryPostProcessor that searches recursively starting from a base package for interfaces and
* registers them as {@code MapperFactoryBean}. Note that only interfaces with at least one method will be registered;
* concrete classes will be ignored.
* <p>
* This class was a {code BeanFactoryPostProcessor} until 1.0.1 version. It changed to
* {@code BeanDefinitionRegistryPostProcessor} in 1.0.2. See https://jira.springsource.org/browse/SPR-8269 for the
* details.
* <p>
* The {@code basePackage} property can contain more than one package name, separated by either commas or semicolons.
* <p>
* This class supports filtering the mappers created by either specifying a marker interface or an annotation. The
* {@code annotationClass} property specifies an annotation to search for. The {@code markerInterface} property
* specifies a parent interface to search for. If both properties are specified, mappers are added for interfaces that
* match <em>either</em> criteria. By default, these two properties are null, so all interfaces in the given
* {@code basePackage} are added as mappers.
* <p>
* This configurer enables autowire for all the beans that it creates so that they are automatically autowired with the
* proper {@code SqlSessionFactory} or {@code SqlSessionTemplate}. If there is more than one {@code SqlSessionFactory}
* in the application, however, autowiring cannot be used. In this case you must explicitly specify either an
* {@code SqlSessionFactory} or an {@code SqlSessionTemplate} to use via the <em>bean name</em> properties. Bean names
* are used rather than actual objects because Spring does not initialize property placeholders until after this class
* is processed.
* <p>
* Passing in an actual object which may require placeholders (i.e. DB user password) will fail. Using bean names defers
* actual object creation until later in the startup process, after all placeholder substitution is completed. However,
* note that this configurer does support property placeholders of its <em>own</em> properties. The
* <code>basePackage</code> and bean name properties all support <code>${property}</code> style substitution.
* <p>
* Configuration sample:
*
* <pre class="code">
* {@code
* <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
* <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
* <!-- optional unless there are multiple session factories defined -->
* <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
* </bean>
* }
* </pre>
*
* @author Hunter Presnall
* @author Eduardo Macarron
*
* @see MapperFactoryBean
* @see ClassPathMapperScanner
*/
public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
private String basePackage;
private boolean addToConfig = true;
private String lazyInitialization;
private SqlSessionFactory sqlSessionFactory;
private SqlSessionTemplate sqlSessionTemplate;
private String sqlSessionFactoryBeanName;
private String sqlSessionTemplateBeanName;
private Class<? extends Annotation> annotationClass;
private Class<?> markerInterface;
private List<TypeFilter> excludeFilters;
private List<Map<String, String>> rawExcludeFilters;
private Class<? extends MapperFactoryBean> mapperFactoryBeanClass;
private ApplicationContext applicationContext;
private String beanName;
private boolean processPropertyPlaceHolders;
private BeanNameGenerator nameGenerator;
private String defaultScope;
/**
* This property lets you set the base package for your mapper interface files.
* <p>
* You can set more than one package by using a semicolon or comma as a separator.
* <p>
* Mappers will be searched for recursively starting in the specified package(s).
*
* @param basePackage
* base package name
*/
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
/**
* Same as {@code MapperFactoryBean#setAddToConfig(boolean)}.
*
* @param addToConfig
* a flag that whether add mapper to MyBatis or not
*
* @see MapperFactoryBean#setAddToConfig(boolean)
*/
public void setAddToConfig(boolean addToConfig) {
this.addToConfig = addToConfig;
}
/**
* Set whether enable lazy initialization for mapper bean.
* <p>
* Default is {@code false}.
* </p>
*
* @param lazyInitialization
* Set the @{code true} to enable
*
* @since 2.0.2
*/
public void setLazyInitialization(String lazyInitialization) {
this.lazyInitialization = lazyInitialization;
}
/**
* This property specifies the annotation that the scanner will search for.
* <p>
* The scanner will register all interfaces in the base package that also have the specified annotation.
* <p>
* Note this can be combined with markerInterface.
*
* @param annotationClass
* annotation class
*/
public void setAnnotationClass(Class<? extends Annotation> annotationClass) {
this.annotationClass = annotationClass;
}
/**
* This property specifies the parent that the scanner will search for.
* <p>
* The scanner will register all interfaces in the base package that also have the specified interface class as a
* parent.
* <p>
* Note this can be combined with annotationClass.
*
* @param superClass
* parent class
*/
public void setMarkerInterface(Class<?> superClass) {
this.markerInterface = superClass;
}
/**
* Specifies which types are not eligible for the mapper scanner.
* <p>
* The scanner will exclude types that define with excludeFilters.
*
* @since 3.0.3
*
* @param excludeFilters
* list of TypeFilter
*/
public void setExcludeFilters(List<TypeFilter> excludeFilters) {
this.excludeFilters = excludeFilters;
}
/**
* In order to support process PropertyPlaceHolders.
* <p>
* After parsed, it will be added to excludeFilters.
*
* @since 3.0.3
*
* @param rawExcludeFilters
* list of rawExcludeFilter
*/
public void setRawExcludeFilters(List<Map<String, String>> rawExcludeFilters) {
this.rawExcludeFilters = rawExcludeFilters;
}
/**
* Specifies which {@code SqlSessionTemplate} to use in the case that there is more than one in the spring context.
* Usually this is only needed when you have more than one datasource.
* <p>
*
* @deprecated Use {@link #setSqlSessionTemplateBeanName(String)} instead
*
* @param sqlSessionTemplate
* a template of SqlSession
*/
@Deprecated
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}
/**
* Specifies which {@code SqlSessionTemplate} to use in the case that there is more than one in the spring context.
* Usually this is only needed when you have more than one datasource.
* <p>
* Note bean names are used, not bean references. This is because the scanner loads early during the start process and
* it is too early to build mybatis object instances.
*
* @since 1.1.0
*
* @param sqlSessionTemplateName
* Bean name of the {@code SqlSessionTemplate}
*/
public void setSqlSessionTemplateBeanName(String sqlSessionTemplateName) {
this.sqlSessionTemplateBeanName = sqlSessionTemplateName;
}
/**
* Specifies which {@code SqlSessionFactory} to use in the case that there is more than one in the spring context.
* Usually this is only needed when you have more than one datasource.
* <p>
*
* @deprecated Use {@link #setSqlSessionFactoryBeanName(String)} instead.
*
* @param sqlSessionFactory
* a factory of SqlSession
*/
@Deprecated
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
/**
* Specifies which {@code SqlSessionFactory} to use in the case that there is more than one in the spring context.
* Usually this is only needed when you have more than one datasource.
* <p>
* Note bean names are used, not bean references. This is because the scanner loads early during the start process and
* it is too early to build mybatis object instances.
*
* @since 1.1.0
*
* @param sqlSessionFactoryName
* Bean name of the {@code SqlSessionFactory}
*/
public void setSqlSessionFactoryBeanName(String sqlSessionFactoryName) {
this.sqlSessionFactoryBeanName = sqlSessionFactoryName;
}
/**
* Specifies a flag that whether execute a property placeholder processing or not.
* <p>
* The default is {@literal false}. This means that a property placeholder processing does not execute.
*
* @since 1.1.1
*
* @param processPropertyPlaceHolders
* a flag that whether execute a property placeholder processing or not
*/
public void setProcessPropertyPlaceHolders(boolean processPropertyPlaceHolders) {
this.processPropertyPlaceHolders = processPropertyPlaceHolders;
}
/**
* The class of the {@link MapperFactoryBean} to return a mybatis proxy as spring bean.
*
* @param mapperFactoryBeanClass
* The class of the MapperFactoryBean
*
* @since 2.0.1
*/
public void setMapperFactoryBeanClass(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) {
this.mapperFactoryBeanClass = mapperFactoryBeanClass;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
/**
* Gets beanNameGenerator to be used while running the scanner.
*
* @return the beanNameGenerator BeanNameGenerator that has been configured
*
* @since 1.2.0
*/
public BeanNameGenerator getNameGenerator() {
return nameGenerator;
}
/**
* Sets beanNameGenerator to be used while running the scanner.
*
* @param nameGenerator
* the beanNameGenerator to set
*
* @since 1.2.0
*/
public void setNameGenerator(BeanNameGenerator nameGenerator) {
this.nameGenerator = nameGenerator;
}
/**
* Sets the default scope of scanned mappers.
* <p>
* Default is {@code null} (equiv to singleton).
* </p>
*
* @param defaultScope
* the default scope
*
* @since 2.0.6
*/
public void setDefaultScope(String defaultScope) {
this.defaultScope = defaultScope;
}
@Override
public void afterPropertiesSet() throws Exception {
notNull(this.basePackage, "Property 'basePackage' is required");
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// left intentionally blank
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
var scanner = new ClassPathMapperScanner(registry, getEnvironment());
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setExcludeFilters(this.excludeFilters = mergeExcludeFilters());
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.parseBoolean(lazyInitialization));
}
if (StringUtils.hasText(defaultScope)) {
scanner.setDefaultScope(defaultScope);
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
/*
* BeanDefinitionRegistries are called early in application startup, before BeanFactoryPostProcessors. This means that
* PropertyResourceConfigurers will not have been loaded and any property substitution of this class' properties will
* fail. To avoid this, find any PropertyResourceConfigurers defined in the context and run them on this class' bean
* definition. Then update the values.
*/
private void processPropertyPlaceHolders() {
Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class,
false, false);
if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
var mapperScannerBean = ((ConfigurableApplicationContext) applicationContext).getBeanFactory()
.getBeanDefinition(beanName);
// PropertyResourceConfigurer does not expose any methods to explicitly perform
// property placeholder substitution. Instead, create a BeanFactory that just
// contains this mapper scanner and post process the factory.
var factory = new DefaultListableBeanFactory();
factory.registerBeanDefinition(beanName, mapperScannerBean);
for (PropertyResourceConfigurer prc : prcs.values()) {
prc.postProcessBeanFactory(factory);
}
PropertyValues values = mapperScannerBean.getPropertyValues();
this.basePackage = getPropertyValue("basePackage", values);
this.sqlSessionFactoryBeanName = getPropertyValue("sqlSessionFactoryBeanName", values);
this.sqlSessionTemplateBeanName = getPropertyValue("sqlSessionTemplateBeanName", values);
this.lazyInitialization = getPropertyValue("lazyInitialization", values);
this.defaultScope = getPropertyValue("defaultScope", values);
this.rawExcludeFilters = getPropertyValueForTypeFilter("rawExcludeFilters", values);
}
this.basePackage = Optional.ofNullable(this.basePackage).map(getEnvironment()::resolvePlaceholders).orElse(null);
this.sqlSessionFactoryBeanName = Optional.ofNullable(this.sqlSessionFactoryBeanName)
.map(getEnvironment()::resolvePlaceholders).orElse(null);
this.sqlSessionTemplateBeanName = Optional.ofNullable(this.sqlSessionTemplateBeanName)
.map(getEnvironment()::resolvePlaceholders).orElse(null);
this.lazyInitialization = Optional.ofNullable(this.lazyInitialization).map(getEnvironment()::resolvePlaceholders)
.orElse(null);
this.defaultScope = Optional.ofNullable(this.defaultScope).map(getEnvironment()::resolvePlaceholders).orElse(null);
}
private Environment getEnvironment() {
return this.applicationContext.getEnvironment();
}
private String getPropertyValue(String propertyName, PropertyValues values) {
var property = values.getPropertyValue(propertyName);
if (property == null) {
return null;
}
var value = property.getValue();
if (value == null) {
return null;
}
if (value instanceof String) {
return value.toString();
}
if (value instanceof TypedStringValue) {
return ((TypedStringValue) value).getValue();
}
return null;
}
@SuppressWarnings("unchecked")
private List<Map<String, String>> getPropertyValueForTypeFilter(String propertyName, PropertyValues values) {
var property = values.getPropertyValue(propertyName);
Object value;
if (property == null || (value = property.getValue()) == null || !(value instanceof List<?>)) {
return null;
}
return (List<Map<String, String>>) value;
}
private List<TypeFilter> mergeExcludeFilters() {
List<TypeFilter> typeFilters = new ArrayList<>();
if (this.rawExcludeFilters == null || this.rawExcludeFilters.isEmpty()) {
return this.excludeFilters;
}
if (this.excludeFilters != null && !this.excludeFilters.isEmpty()) {
typeFilters.addAll(this.excludeFilters);
}
try {
for (Map<String, String> typeFilter : this.rawExcludeFilters) {
typeFilters.add(
createTypeFilter(typeFilter.get("type"), typeFilter.get("expression"), this.getClass().getClassLoader()));
}
} catch (ClassNotFoundException exception) {
throw new RuntimeException("ClassNotFoundException occur when to load the Specified excludeFilter classes.",
exception);
}
return typeFilters;
}
@SuppressWarnings("unchecked")
private TypeFilter createTypeFilter(String filterType, String expression, @Nullable ClassLoader classLoader)
throws ClassNotFoundException {
if (this.processPropertyPlaceHolders) {
expression = this.getEnvironment().resolvePlaceholders(expression);
}
switch (filterType) {
case "annotation":
Class<?> filterAnno = ClassUtils.forName(expression, classLoader);
if (!Annotation.class.isAssignableFrom(filterAnno)) {
throw new IllegalArgumentException(
"Class is not assignable to [" + Annotation.class.getName() + "]: " + expression);
}
return new AnnotationTypeFilter((Class<Annotation>) filterAnno);
case "custom":
Class<?> filterClass = ClassUtils.forName(expression, classLoader);
if (!TypeFilter.class.isAssignableFrom(filterClass)) {
throw new IllegalArgumentException(
"Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression);
}
return (TypeFilter) BeanUtils.instantiateClass(filterClass);
case "assignable":
return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader));
case "regex":
return new RegexPatternTypeFilter(Pattern.compile(expression));
case "aspectj":
return new AspectJTypeFilter(expression, classLoader);
default:
throw new IllegalArgumentException("Unsupported filter type: " + filterType);
}
}
}