+ * 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
+ *
+ * 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 com.mybatisflex.spring.boot;
+
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
+import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.env.*;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Iterator;
+
+/**
+ *
判断是否有 MyBatis-Flex 的多数据源配置。
+ *
如果配置文件中有 MyBatis-Flex 的多数据源配置,就加载 MyBatis-Flex 多数据源自动配置类。
+ *
+ * @author michael
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Conditional(ConditionalOnMybatisFlexDatasource.OnMybatisFlexDataSourceCondition.class)
+public @interface ConditionalOnMybatisFlexDatasource {
+
+ @Order(Ordered.HIGHEST_PRECEDENCE)
+ class OnMybatisFlexDataSourceCondition extends SpringBootCondition {
+
+ @Override
+ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ Environment env = context.getEnvironment();
+ if (env instanceof AbstractEnvironment) {
+ MutablePropertySources propertySources = ((AbstractEnvironment) env).getPropertySources();
+ Iterator> it = propertySources.stream().iterator();
+ while (it.hasNext()) {
+ PropertySource> ps = it.next();
+ if (ps instanceof EnumerablePropertySource) {
+ for (String propertyName : ((EnumerablePropertySource>) ps).getPropertyNames()) {
+ if (propertyName.startsWith("mybatis-flex.datasource.")) {
+ return ConditionOutcome.match();
+ }
+ }
+ }
+ }
+ }
+ return ConditionOutcome.noMatch("'mybatis-flex.datasource' is necessary.");
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/mybatisflex/spring/boot/ConfigurationCustomizer.java b/src/main/java/com/mybatisflex/spring/boot/ConfigurationCustomizer.java
new file mode 100644
index 0000000..4938f2d
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/ConfigurationCustomizer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.mybatisflex.spring.boot;
+
+import com.mybatisflex.core.mybatis.FlexConfiguration;
+
+/**
+ * 为 {@link FlexConfiguration} 做自定义的配置支持。
+ * @author michael
+ */
+@FunctionalInterface
+public interface ConfigurationCustomizer {
+
+ /**
+ * 自定义配置 {@link FlexConfiguration}。
+ *
+ * @param configuration MyBatis Flex Configuration
+ */
+ void customize(FlexConfiguration configuration);
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/mybatisflex/spring/boot/FlexTransactionAutoConfiguration.java b/src/main/java/com/mybatisflex/spring/boot/FlexTransactionAutoConfiguration.java
new file mode 100644
index 0000000..c999785
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/FlexTransactionAutoConfiguration.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 需要保证两个对象的一致性。
+ */
+ private final FlexTransactionManager flexTransactionManager = new FlexTransactionManager();
+
+ @NonNull
+ @Override
+ @Bean(name = "transactionManager")
+ public PlatformTransactionManager annotationDrivenTransactionManager() {
+ return flexTransactionManager;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/mybatisflex/spring/boot/MultiDataSourceAutoConfiguration.java b/src/main/java/com/mybatisflex/spring/boot/MultiDataSourceAutoConfiguration.java
new file mode 100644
index 0000000..802d399
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/MultiDataSourceAutoConfiguration.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2022-2024, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.mybatisflex.spring.boot;
+
+import com.mybatisflex.core.datasource.DataSourceBuilder;
+import com.mybatisflex.core.datasource.DataSourceDecipher;
+import com.mybatisflex.core.datasource.DataSourceManager;
+import com.mybatisflex.core.datasource.FlexDataSource;
+import com.mybatisflex.core.dialect.DbType;
+import com.mybatisflex.core.dialect.DbTypeUtil;
+import com.mybatisflex.core.exception.FlexExceptions;
+import com.mybatisflex.core.util.MapUtil;
+import com.mybatisflex.spring.boot.MybatisFlexProperties.SeataConfig;
+import com.mybatisflex.spring.datasource.DataSourceAdvice;
+import io.seata.rm.datasource.DataSourceProxy;
+import io.seata.rm.datasource.xa.DataSourceProxyXA;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
+import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+
+import javax.sql.DataSource;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * MyBatis-Flex 多数据源的配置支持。
+ *
+ * @author michael
+ * @author 王帅
+ */
+@ConditionalOnMybatisFlexDatasource()
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(MybatisFlexProperties.class)
+@ConditionalOnClass(
+ value = {SqlSessionFactory.class, SqlSessionFactoryBean.class},
+ name = {
+ "org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
+ "org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration",
+ }
+)
+@AutoConfigureBefore(value = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class}
+ , name = {"com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure",
+ "com.alibaba.druid.spring.boot4.autoconfigure.DruidDataSourceAutoConfigure"})
+public class MultiDataSourceAutoConfiguration {
+
+ private final String master;
+
+ private final Map> dataSourceProperties;
+
+ private final SeataConfig seataConfig;
+
+ // 数据源解密器
+ protected final DataSourceDecipher dataSourceDecipher;
+
+
+ public MultiDataSourceAutoConfiguration(MybatisFlexProperties properties
+ , ObjectProvider dataSourceDecipherProvider
+ ) {
+ dataSourceProperties = properties.getDatasource();
+ dataSourceDecipher = dataSourceDecipherProvider.getIfAvailable();
+ seataConfig = properties.getSeataConfig();
+ master = properties.getDefaultDatasourceKey();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public DataSource dataSource() {
+
+ FlexDataSource flexDataSource = null;
+
+ if (dataSourceProperties != null && !dataSourceProperties.isEmpty()) {
+
+ if (dataSourceDecipher != null) {
+ DataSourceManager.setDecipher(dataSourceDecipher);
+ }
+
+ if (master != null) {
+ Map map = dataSourceProperties.remove(master);
+ if (map != null) {
+ // 这里创建master时,flexDataSource一定是null
+ flexDataSource = addDataSource(MapUtil.entry(master, map), null);
+ } else {
+ throw FlexExceptions.wrap("没有找到默认数据源 \"%s\" 对应的配置,请检查您的多数据源配置。", master);
+ }
+ }
+
+ for (Map.Entry> entry : dataSourceProperties.entrySet()) {
+ flexDataSource = addDataSource(entry, flexDataSource);
+ }
+ }
+
+ return flexDataSource;
+ }
+
+ private FlexDataSource addDataSource(Map.Entry> entry, FlexDataSource flexDataSource) {
+ DataSource dataSource = new DataSourceBuilder(entry.getValue()).build();
+ DataSourceManager.decryptDataSource(dataSource);
+
+ // 数据库类型
+ DbType dbType = null;
+ if (seataConfig != null && seataConfig.isEnable()) {
+ if (seataConfig.getSeataMode() == MybatisFlexProperties.SeataMode.XA) {
+ DataSourceProxyXA sourceProxyXa = new DataSourceProxyXA(dataSource);
+ dbType = DbType.findByName(sourceProxyXa.getDbType());
+ dataSource = sourceProxyXa;
+ } else {
+ DataSourceProxy dataSourceProxy = new DataSourceProxy(dataSource);
+ dbType = DbType.findByName(dataSourceProxy.getDbType());
+ dataSource = dataSourceProxy;
+ }
+ }
+
+ // 如果没有构建成功dbType,需要自解析
+ final DataSource lambdaInnerDataSource = dataSource;
+ dbType = Optional.ofNullable(dbType).orElseGet(() -> DbTypeUtil.getDbType(lambdaInnerDataSource));
+ if (flexDataSource == null) {
+ flexDataSource = new FlexDataSource(entry.getKey(), dataSource, dbType, false);
+ } else {
+ flexDataSource.addDataSource(entry.getKey(), dataSource, dbType, false);
+ }
+ return flexDataSource;
+ }
+
+
+ /**
+ * {@link com.mybatisflex.annotation.UseDataSource} 注解切换数据源切面。
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ public DataSourceAdvice dataSourceAdvice() {
+ return new DataSourceAdvice();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/mybatisflex/spring/boot/MyBatisFlexCustomizer.java b/src/main/java/com/mybatisflex/spring/boot/MyBatisFlexCustomizer.java
new file mode 100644
index 0000000..d1e9a21
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/MyBatisFlexCustomizer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.mybatisflex.spring.boot;
+
+import com.mybatisflex.core.FlexGlobalConfig;
+
+/**
+ *
+ * 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
+ *
+ * 1、替换配置为 mybatis-flex 的配置前缀
+ * 2、修改 SqlSessionFactory 为 FlexSqlSessionFactoryBean
+ * 3、修改 Configuration 为 FlexConfiguration
+ *
+ * @author Eddú Meléndez
+ * @author Josh Long
+ * @author Kazuki Shimizu
+ * @author Eduardo Macarrón
+ * @author michael
+ * @author 王帅
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(
+ value = {SqlSessionFactory.class, SqlSessionFactoryBean.class},
+ name = {
+ "org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
+ }
+)
+@ConditionalOnSingleCandidate(DataSource.class)
+@EnableConfigurationProperties(MybatisFlexProperties.class)
+@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
+public class MybatisFlexAutoConfiguration implements InitializingBean {
+
+ protected static final Logger logger = LoggerFactory.getLogger(MybatisFlexAutoConfiguration.class);
+
+ protected final MybatisFlexProperties properties;
+
+ protected final Interceptor[] interceptors;
+
+ protected final TypeHandler[] typeHandlers;
+
+ protected final LanguageDriver[] languageDrivers;
+
+ protected final ResourceLoader resourceLoader;
+
+ protected final DatabaseIdProvider databaseIdProvider;
+
+ protected final List configurationCustomizers;
+
+ protected final List sqlSessionFactoryBeanCustomizers;
+
+ //数据源解密器
+ protected final DataSourceDecipher dataSourceDecipher;
+
+ //动态表名
+ protected final DynamicTableProcessor dynamicTableProcessor;
+
+ //动态 schema 处理器
+ protected final DynamicSchemaProcessor dynamicSchemaProcessor;
+
+ //多租户
+ protected final TenantFactory tenantFactory;
+
+ //自定义逻辑删除处理器
+ protected final LogicDeleteProcessor logicDeleteProcessor;
+
+ //初始化监听
+ protected final List mybatisFlexCustomizers;
+
+
+ public MybatisFlexAutoConfiguration(MybatisFlexProperties properties, ObjectProvider interceptorsProvider,
+ ObjectProvider typeHandlersProvider, ObjectProvider languageDriversProvider,
+ ResourceLoader resourceLoader, ObjectProvider databaseIdProvider,
+ ObjectProvider> configurationCustomizersProvider,
+ ObjectProvider> sqlSessionFactoryBeanCustomizers,
+ ObjectProvider dataSourceDecipherProvider,
+ ObjectProvider dynamicTableProcessorProvider,
+ ObjectProvider dynamicSchemaProcessorProvider,
+ ObjectProvider tenantFactoryProvider,
+ ObjectProvider logicDeleteProcessorProvider,
+ ObjectProvider mybatisFlexCustomizerProviders
+ ) {
+ this.properties = properties;
+ this.interceptors = interceptorsProvider.getIfAvailable();
+ this.typeHandlers = typeHandlersProvider.getIfAvailable();
+ this.languageDrivers = languageDriversProvider.getIfAvailable();
+ this.resourceLoader = resourceLoader;
+ this.databaseIdProvider = databaseIdProvider.getIfAvailable();
+ this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
+ this.sqlSessionFactoryBeanCustomizers = sqlSessionFactoryBeanCustomizers.getIfAvailable();
+
+ //数据源解密器
+ this.dataSourceDecipher = dataSourceDecipherProvider.getIfAvailable();
+
+ //动态表名
+ this.dynamicTableProcessor = dynamicTableProcessorProvider.getIfAvailable();
+
+ //动态 schema 处理器
+ this.dynamicSchemaProcessor = dynamicSchemaProcessorProvider.getIfAvailable();
+
+ //多租户
+ this.tenantFactory = tenantFactoryProvider.getIfAvailable();
+
+ //逻辑删除处理器
+ this.logicDeleteProcessor = logicDeleteProcessorProvider.getIfAvailable();
+
+ //初始化监听器
+ this.mybatisFlexCustomizers = mybatisFlexCustomizerProviders.orderedStream().collect(Collectors.toList());
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ // 检测 MyBatis 原生配置文件是否存在
+ checkConfigFileExists();
+
+ // 添加 MyBatis-Flex 全局配置
+ if (properties.getGlobalConfig() != null) {
+ properties.getGlobalConfig().applyTo(FlexGlobalConfig.getDefaultConfig());
+ }
+
+ //数据源解密器
+ if (dataSourceDecipher != null) {
+ DataSourceManager.setDecipher(dataSourceDecipher);
+ }
+
+ // 动态表名配置
+ if (dynamicTableProcessor != null) {
+ TableManager.setDynamicTableProcessor(dynamicTableProcessor);
+ }
+
+ // 动态 schema 处理器配置
+ if (dynamicSchemaProcessor != null) {
+ TableManager.setDynamicSchemaProcessor(dynamicSchemaProcessor);
+ }
+
+ //多租户
+ if (tenantFactory != null) {
+ TenantManager.setTenantFactory(tenantFactory);
+ }
+
+ //逻辑删除处理器
+ if (logicDeleteProcessor != null) {
+ LogicDeleteManager.setProcessor(logicDeleteProcessor);
+ }
+
+ //初始化监听器
+ if (mybatisFlexCustomizers != null) {
+ mybatisFlexCustomizers.forEach(myBatisFlexCustomizer -> myBatisFlexCustomizer.customize(FlexGlobalConfig.getDefaultConfig()));
+ }
+ }
+
+ private void checkConfigFileExists() {
+ if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
+ Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
+ Assert.state(resource.exists(),
+ "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
+ }
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
+
+ SqlSessionFactoryBean factory = new FlexSqlSessionFactoryBean();
+ factory.setDataSource(dataSource);
+ if (properties.getConfiguration() == null || properties.getConfiguration().getVfsImpl() == null) {
+ factory.setVfs(SpringBootVFS.class);
+ }
+ if (StringUtils.hasText(this.properties.getConfigLocation())) {
+ factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
+ }
+ applyConfiguration(factory);
+ if (this.properties.getConfigurationProperties() != null) {
+ factory.setConfigurationProperties(this.properties.getConfigurationProperties());
+ }
+ if (!ObjectUtils.isEmpty(this.interceptors)) {
+ factory.setPlugins(this.interceptors);
+ }
+ if (this.databaseIdProvider != null) {
+ factory.setDatabaseIdProvider(this.databaseIdProvider);
+ }
+ if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
+ factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
+ }
+ if (this.properties.getTypeAliasesSuperType() != null) {
+ factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
+ }
+ if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
+ factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
+ }
+ if (!ObjectUtils.isEmpty(this.typeHandlers)) {
+ factory.setTypeHandlers(this.typeHandlers);
+ }
+ Resource[] mapperLocations = this.properties.resolveMapperLocations();
+ if (!ObjectUtils.isEmpty(mapperLocations)) {
+ factory.setMapperLocations(mapperLocations);
+ }
+ Set factoryPropertyNames = Stream
+ .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
+ .collect(Collectors.toSet());
+ Class extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
+ if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
+ // Need to mybatis-spring 2.0.2+
+ factory.setScriptingLanguageDrivers(this.languageDrivers);
+ if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
+ defaultLanguageDriver = this.languageDrivers[0].getClass();
+ }
+ }
+ if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
+ // Need to mybatis-spring 2.0.2+
+ factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
+ }
+ applySqlSessionFactoryBeanCustomizers(factory);
+ return factory.getObject();
+ }
+
+ protected void applyConfiguration(SqlSessionFactoryBean factory) {
+ MybatisFlexProperties.CoreConfiguration coreConfiguration = this.properties.getConfiguration();
+ FlexConfiguration configuration = null;
+ if (coreConfiguration != null || !StringUtils.hasText(this.properties.getConfigLocation())) {
+ configuration = new FlexConfiguration();
+ }
+ if (configuration != null && coreConfiguration != null) {
+ coreConfiguration.applyTo(configuration);
+ }
+ if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
+ for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
+ customizer.customize(configuration);
+ }
+ }
+ factory.setConfiguration(configuration);
+ }
+
+ protected void applySqlSessionFactoryBeanCustomizers(SqlSessionFactoryBean factory) {
+ if (!CollectionUtils.isEmpty(this.sqlSessionFactoryBeanCustomizers)) {
+ for (SqlSessionFactoryBeanCustomizer customizer : this.sqlSessionFactoryBeanCustomizers) {
+ customizer.customize(factory);
+ }
+ }
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
+ ExecutorType executorType = this.properties.getExecutorType();
+ if (executorType != null) {
+ return new SqlSessionTemplate(sqlSessionFactory, executorType);
+ } else {
+ return new SqlSessionTemplate(sqlSessionFactory);
+ }
+ }
+
+ /**
+ * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
+ * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
+ * similar to using Spring Data JPA repositories.
+ */
+ public static class AutoConfiguredMapperScannerRegistrar
+ implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
+
+ private BeanFactory beanFactory;
+ private Environment environment;
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+
+ if (!AutoConfigurationPackages.has(this.beanFactory)) {
+ logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
+ return;
+ }
+
+ logger.debug("Searching for mappers annotated with @Mapper");
+
+ List packages = AutoConfigurationPackages.get(this.beanFactory);
+ if (logger.isDebugEnabled()) {
+ packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
+ }
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
+ builder.addPropertyValue("processPropertyPlaceHolders", true);
+ builder.addPropertyValue("annotationClass", Mapper.class);
+ builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
+ BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
+ Set propertyNames = Stream.of(beanWrapper.getPropertyDescriptors()).map(PropertyDescriptor::getName)
+ .collect(Collectors.toSet());
+ if (propertyNames.contains("lazyInitialization")) {
+ // Need to mybatis-spring 2.0.2+
+ builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
+ }
+ if (propertyNames.contains("defaultScope")) {
+ // Need to mybatis-spring 2.0.6+
+ builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
+ }
+
+ // for spring-native
+ boolean injectSqlSession = environment.getProperty("mybatis.inject-sql-session-on-mapper-scan", Boolean.class,
+ Boolean.TRUE);
+ if (injectSqlSession && this.beanFactory instanceof ListableBeanFactory) {
+ ListableBeanFactory listableBeanFactory = (ListableBeanFactory) this.beanFactory;
+ Optional sqlSessionTemplateBeanName = Optional
+ .ofNullable(getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory));
+ Optional sqlSessionFactoryBeanName = Optional
+ .ofNullable(getBeanNameForType(SqlSessionFactory.class, listableBeanFactory));
+ if (sqlSessionTemplateBeanName.isPresent() || !sqlSessionFactoryBeanName.isPresent()) {
+ builder.addPropertyValue("sqlSessionTemplateBeanName",
+ sqlSessionTemplateBeanName.orElse("sqlSessionTemplate"));
+ } else {
+ builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryBeanName.get());
+ }
+ }
+ builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+
+ registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
+ }
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+ @Override
+ public void setEnvironment(Environment environment) {
+ this.environment = environment;
+ }
+
+ private String getBeanNameForType(Class> type, ListableBeanFactory factory) {
+ String[] beanNames = factory.getBeanNamesForType(type);
+ return beanNames.length > 0 ? beanNames[0] : null;
+ }
+
+ }
+
+ /**
+ * If mapper registering configuration or mapper scanning configuration not present, this configuration allow to scan
+ * mappers based on the same component-scanning path as Spring Boot itself.
+ */
+ @org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
+ @Import(AutoConfiguredMapperScannerRegistrar.class)
+ @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
+ public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
+
+ @Override
+ public void afterPropertiesSet() {
+ logger.debug(
+ "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/mybatisflex/spring/boot/MybatisFlexDependsOnDatabaseInitializationDetector.java b/src/main/java/com/mybatisflex/spring/boot/MybatisFlexDependsOnDatabaseInitializationDetector.java
new file mode 100644
index 0000000..9ccd022
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/MybatisFlexDependsOnDatabaseInitializationDetector.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015-2022 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 com.mybatisflex.spring.boot;
+
+import org.mybatis.spring.SqlSessionTemplate;
+import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector;
+import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 参考:...
+ * {@link DependsOnDatabaseInitializationDetector} for Mybatis-Flex.
+ *
+ * @author Eddú Meléndez
+ */
+class MybatisFlexDependsOnDatabaseInitializationDetector
+ extends AbstractBeansOfTypeDependsOnDatabaseInitializationDetector {
+
+ @Override
+ protected Set> getDependsOnDatabaseInitializationBeanTypes() {
+ return Collections.singleton(SqlSessionTemplate.class);
+ }
+
+}
diff --git a/src/main/java/com/mybatisflex/spring/boot/MybatisFlexProperties.java b/src/main/java/com/mybatisflex/spring/boot/MybatisFlexProperties.java
new file mode 100644
index 0000000..46fa45c
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/MybatisFlexProperties.java
@@ -0,0 +1,1022 @@
+/*
+ * Copyright (c) 2022-2024, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * 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
+ *
+ * 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 com.mybatisflex.spring.boot;
+
+import com.mybatisflex.core.FlexConsts;
+import com.mybatisflex.core.FlexGlobalConfig;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.logging.Log;
+import org.apache.ibatis.mapping.ResultSetType;
+import org.apache.ibatis.scripting.LanguageDriver;
+import org.apache.ibatis.session.AutoMappingBehavior;
+import org.apache.ibatis.session.AutoMappingUnknownColumnBehavior;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.LocalCacheScope;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.TypeHandler;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+import org.springframework.boot.context.properties.PropertyMapper;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * Mybatis-Flex 的配置属性。
+ * 参考:...
+ *
+ * @author Eddú Meléndez
+ * @author Kazuki Shimizu
+ * @author micahel
+ * @author 王帅
+ */
+@ConfigurationProperties(prefix = "mybatis-flex")
+public class MybatisFlexProperties {
+
+ private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+
+ private String defaultDatasourceKey;
+
+ /**
+ *
多数据源的配置。
+ *
+ *
+ * mybatis-flex.datasource.ds1.url=***
+ * mybatis-flex.datasource.ds2.url=***
+ */
+ private Map> datasource;
+
+ /**
+ * 全局配置。
+ */
+ private GlobalConfig globalConfig;
+
+ /**
+ * MyBatis-Flex-Admin 配置。
+ */
+ private AdminConfig adminConfig;
+
+ /**
+ * Location of MyBatis xml config file.
+ */
+ private String configLocation;
+
+ /**
+ * Locations of MyBatis mapper files.
+ */
+ private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"};
+
+ /**
+ * Packages to search type aliases. (Package delimiters are ",; \t\n")
+ */
+ private String typeAliasesPackage;
+
+ /**
+ * The super class for filtering type alias. If this not specifies, the MyBatis deal as type alias all classes that
+ * searched from typeAliasesPackage.
+ */
+ private Class> typeAliasesSuperType;
+
+ /**
+ * Packages to search for type handlers. (Package delimiters are ",; \t\n")
+ */
+ private String typeHandlersPackage;
+
+ /**
+ * Indicates whether perform presence check of the MyBatis xml config file.
+ */
+ private boolean checkConfigLocation = false;
+
+ /**
+ * Execution mode for {@link org.mybatis.spring.SqlSessionTemplate}.
+ */
+ private ExecutorType executorType;
+
+ /**
+ * The default scripting language driver class. (Available when use together with mybatis-spring 2.0.2+)
+ */
+ private Class extends LanguageDriver> defaultScriptingLanguageDriver;
+
+ /**
+ * Externalized properties for MyBatis configuration.
+ */
+ private Properties configurationProperties;
+
+ /**
+ * A Configuration object for customize default settings. If {@link #configLocation} is specified, this property is
+ * not used.
+ */
+ private CoreConfiguration configuration;
+
+ /**
+ * A Configuration object for seata
+ */
+ private SeataConfig seataConfig;
+
+ public SeataConfig getSeataConfig() {
+ return seataConfig;
+ }
+
+ public void setSeataConfig(SeataConfig seataConfig) {
+ this.seataConfig = seataConfig;
+ }
+
+ public Map> getDatasource() {
+ return datasource;
+ }
+
+ public void setDatasource(Map> datasource) {
+ this.datasource = datasource;
+ }
+
+ public GlobalConfig getGlobalConfig() {
+ return globalConfig;
+ }
+
+ public void setGlobalConfig(GlobalConfig globalConfig) {
+ this.globalConfig = globalConfig;
+ }
+
+ public AdminConfig getAdminConfig() {
+ return adminConfig;
+ }
+
+ public void setAdminConfig(AdminConfig adminConfig) {
+ this.adminConfig = adminConfig;
+ }
+
+ public String getDefaultDatasourceKey() {
+ return defaultDatasourceKey;
+ }
+
+ public void setDefaultDatasourceKey(String defaultDatasourceKey) {
+ this.defaultDatasourceKey = defaultDatasourceKey;
+ }
+
+ /**
+ * @since 1.1.0
+ */
+ public String getConfigLocation() {
+ return this.configLocation;
+ }
+
+ /**
+ * @since 1.1.0
+ */
+ public void setConfigLocation(String configLocation) {
+ this.configLocation = configLocation;
+ }
+
+ public String[] getMapperLocations() {
+ return this.mapperLocations;
+ }
+
+ public void setMapperLocations(String[] mapperLocations) {
+ this.mapperLocations = mapperLocations;
+ }
+
+ public String getTypeHandlersPackage() {
+ return this.typeHandlersPackage;
+ }
+
+ public void setTypeHandlersPackage(String typeHandlersPackage) {
+ this.typeHandlersPackage = typeHandlersPackage;
+ }
+
+ public String getTypeAliasesPackage() {
+ return this.typeAliasesPackage;
+ }
+
+ public void setTypeAliasesPackage(String typeAliasesPackage) {
+ this.typeAliasesPackage = typeAliasesPackage;
+ }
+
+ /**
+ * @since 1.3.3
+ */
+ public Class> getTypeAliasesSuperType() {
+ return typeAliasesSuperType;
+ }
+
+ /**
+ * @since 1.3.3
+ */
+ public void setTypeAliasesSuperType(Class> typeAliasesSuperType) {
+ this.typeAliasesSuperType = typeAliasesSuperType;
+ }
+
+ public boolean isCheckConfigLocation() {
+ return this.checkConfigLocation;
+ }
+
+ public void setCheckConfigLocation(boolean checkConfigLocation) {
+ this.checkConfigLocation = checkConfigLocation;
+ }
+
+ public ExecutorType getExecutorType() {
+ return this.executorType;
+ }
+
+ public void setExecutorType(ExecutorType executorType) {
+ this.executorType = executorType;
+ }
+
+ /**
+ * @since 2.1.0
+ */
+ public Class extends LanguageDriver> getDefaultScriptingLanguageDriver() {
+ return defaultScriptingLanguageDriver;
+ }
+
+ /**
+ * @since 2.1.0
+ */
+ public void setDefaultScriptingLanguageDriver(Class extends LanguageDriver> defaultScriptingLanguageDriver) {
+ this.defaultScriptingLanguageDriver = defaultScriptingLanguageDriver;
+ }
+
+ /**
+ * @since 1.2.0
+ */
+ public Properties getConfigurationProperties() {
+ return configurationProperties;
+ }
+
+ /**
+ * @since 1.2.0
+ */
+ public void setConfigurationProperties(Properties configurationProperties) {
+ this.configurationProperties = configurationProperties;
+ }
+
+ public CoreConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(CoreConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ public Resource[] resolveMapperLocations() {
+ return Stream.of(Optional.ofNullable(this.mapperLocations).orElse(new String[0]))
+ .flatMap(location -> Stream.of(getResources(location))).toArray(Resource[]::new);
+ }
+
+ private Resource[] getResources(String location) {
+ try {
+ return resourceResolver.getResources(location);
+ } catch (IOException e) {
+ return new Resource[0];
+ }
+ }
+
+ /**
+ * The configuration properties for mybatis core module.
+ *
+ * @since 3.0.0
+ */
+ public static class CoreConfiguration {
+
+ /**
+ * Allows using RowBounds on nested statements. If allow, set the false. Default is false.
+ */
+ private Boolean safeRowBoundsEnabled;
+
+ /**
+ * Allows using ResultHandler on nested statements. If allow, set the false. Default is true.
+ */
+ private Boolean safeResultHandlerEnabled;
+
+ /**
+ * Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names
+ * aColumn. Default is true.
+ */
+ private Boolean mapUnderscoreToCamelCase = true;
+
+ /**
+ * When enabled, any method call will load all the lazy properties of the object. Otherwise, each property is loaded
+ * on demand (see also lazyLoadTriggerMethods). Default is false.
+ */
+ private Boolean aggressiveLazyLoading;
+
+ /**
+ * Allows or disallows multiple ResultSets to be returned from a single statement (compatible driver required).
+ * Default is true.
+ */
+ private Boolean multipleResultSetsEnabled;
+
+ /**
+ * Allows JDBC support for generated keys. A compatible driver is required. This setting forces generated keys to be
+ * used if set to true, as some drivers deny compatibility but still work (e.g. Derby). Default is false.
+ */
+ private Boolean useGeneratedKeys;
+
+ /**
+ * Uses the column label instead of the column name. Different drivers behave differently in this respect. Refer to
+ * the driver documentation, or test out both modes to determine how your driver behaves. Default is true.
+ */
+ private Boolean useColumnLabel;
+
+ /**
+ * Globally enables or disables any caches configured in any mapper under this configuration. Default is true.
+ */
+ private Boolean cacheEnabled;
+
+ /**
+ * Specifies if setters or map's put method will be called when a retrieved value is null. It is useful when you
+ * rely on Map.keySet() or null value initialization. Note primitives such as (int,boolean,etc.) will not be set to
+ * null. Default is false.
+ */
+ private Boolean callSettersOnNulls;
+
+ /**
+ * Allow referencing statement parameters by their actual names declared in the method signature. To use this
+ * feature, your project must be compiled in Java 8 with -parameters option. Default is true.
+ */
+ private Boolean useActualParamName;
+
+ /**
+ * MyBatis, by default, returns null when all the columns of a returned row are NULL. When this setting is enabled,
+ * MyBatis returns an empty instance instead. Note that it is also applied to nested results (i.e. collectioin and
+ * association). Default is false.
+ */
+ private Boolean returnInstanceForEmptyRow;
+
+ /**
+ * Removes extra whitespace characters from the SQL. Note that this also affects literal strings in SQL. Default is
+ * false.
+ */
+ private Boolean shrinkWhitespacesInSql;
+
+ /**
+ * Specifies the default value of 'nullable' attribute on 'foreach' tag. Default is false.
+ */
+ private Boolean nullableOnForEach;
+
+ /**
+ * When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on
+ * the column order. Default is false.
+ */
+ private Boolean argNameBasedConstructorAutoMapping;
+
+ /**
+ * Globally enables or disables lazy loading. When enabled, all relations will be lazily loaded. This value can be
+ * superseded for a specific relation by using the fetchType attribute on it. Default is False.
+ */
+ private Boolean lazyLoadingEnabled;
+
+ /**
+ * Sets the number of seconds the driver will wait for a response from the database.
+ */
+ private Integer defaultStatementTimeout;
+
+ /**
+ * Sets the driver a hint as to control fetching size for return results. This parameter value can be override by a
+ * query setting.
+ */
+ private Integer defaultFetchSize;
+
+ /**
+ * MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default
+ * (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be
+ * used just for statement execution, no data will be shared between two different calls to the same SqlSession.
+ * Default is SESSION.
+ */
+ private LocalCacheScope localCacheScope;
+
+ /**
+ * Specifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers
+ * require specifying the column JDBC type but others work with generic values like NULL, VARCHAR or OTHER. Default
+ * is OTHER.
+ */
+ private JdbcType jdbcTypeForNull;
+
+ /**
+ * Specifies a scroll strategy when omit it per statement settings.
+ */
+ private ResultSetType defaultResultSetType;
+
+ /**
+ * Configures the default executor. SIMPLE executor does nothing special. REUSE executor reuses prepared statements.
+ * BATCH executor reuses statements and batches updates. Default is SIMPLE.
+ */
+ private ExecutorType defaultExecutorType;
+
+ /**
+ * Specifies if and how MyBatis should automatically map columns to fields/properties. NONE disables auto-mapping.
+ * PARTIAL will only auto-map results with no nested result mappings defined inside. FULL will auto-map result
+ * mappings of any complexity (containing nested or otherwise). Default is PARTIAL.
+ */
+ private AutoMappingBehavior autoMappingBehavior;
+
+ /**
+ * Specify the behavior when detects an unknown column (or unknown property type) of automatic mapping target.
+ * Default is NONE.
+ */
+ private AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior;
+
+ /**
+ * Specifies the prefix string that MyBatis will add to the logger names.
+ */
+ private String logPrefix;
+
+ /**
+ * Specifies which Object's methods trigger a lazy load. Default is [equals,clone,hashCode,toString].
+ */
+ private Set lazyLoadTriggerMethods;
+
+ /**
+ * Specifies which logging implementation MyBatis should use. If this setting is not present logging implementation
+ * will be autodiscovered.
+ */
+ private Class extends Log> logImpl;
+
+ /**
+ * Specifies VFS implementations.
+ */
+ private Class extends VFS> vfsImpl;
+
+ /**
+ * Specifies an sql provider class that holds provider method. This class apply to the type(or value) attribute on
+ * sql provider annotation(e.g. @SelectProvider), when these attribute was omitted.
+ */
+ private Class> defaultSqlProviderType;
+
+ /**
+ * Specifies the TypeHandler used by default for Enum.
+ */
+ Class extends TypeHandler> defaultEnumTypeHandler;
+
+ /**
+ * Specifies the class that provides an instance of Configuration. The returned Configuration instance is used to
+ * load lazy properties of deserialized objects. This class must have a method with a signature static Configuration
+ * getConfiguration().
+ */
+ private Class> configurationFactory;
+
+ /**
+ * Specify any configuration variables.
+ */
+ private Properties variables;
+
+ public Boolean getSafeRowBoundsEnabled() {
+ return safeRowBoundsEnabled;
+ }
+
+ public void setSafeRowBoundsEnabled(Boolean safeRowBoundsEnabled) {
+ this.safeRowBoundsEnabled = safeRowBoundsEnabled;
+ }
+
+ public Boolean getSafeResultHandlerEnabled() {
+ return safeResultHandlerEnabled;
+ }
+
+ public void setSafeResultHandlerEnabled(Boolean safeResultHandlerEnabled) {
+ this.safeResultHandlerEnabled = safeResultHandlerEnabled;
+ }
+
+ public Boolean getMapUnderscoreToCamelCase() {
+ return mapUnderscoreToCamelCase;
+ }
+
+ public void setMapUnderscoreToCamelCase(Boolean mapUnderscoreToCamelCase) {
+ this.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase;
+ }
+
+ public Boolean getAggressiveLazyLoading() {
+ return aggressiveLazyLoading;
+ }
+
+ public void setAggressiveLazyLoading(Boolean aggressiveLazyLoading) {
+ this.aggressiveLazyLoading = aggressiveLazyLoading;
+ }
+
+ public Boolean getMultipleResultSetsEnabled() {
+ return multipleResultSetsEnabled;
+ }
+
+ public void setMultipleResultSetsEnabled(Boolean multipleResultSetsEnabled) {
+ this.multipleResultSetsEnabled = multipleResultSetsEnabled;
+ }
+
+ public Boolean getUseGeneratedKeys() {
+ return useGeneratedKeys;
+ }
+
+ public void setUseGeneratedKeys(Boolean useGeneratedKeys) {
+ this.useGeneratedKeys = useGeneratedKeys;
+ }
+
+ public Boolean getUseColumnLabel() {
+ return useColumnLabel;
+ }
+
+ public void setUseColumnLabel(Boolean useColumnLabel) {
+ this.useColumnLabel = useColumnLabel;
+ }
+
+ public Boolean getCacheEnabled() {
+ return cacheEnabled;
+ }
+
+ public void setCacheEnabled(Boolean cacheEnabled) {
+ this.cacheEnabled = cacheEnabled;
+ }
+
+ public Boolean getCallSettersOnNulls() {
+ return callSettersOnNulls;
+ }
+
+ public void setCallSettersOnNulls(Boolean callSettersOnNulls) {
+ this.callSettersOnNulls = callSettersOnNulls;
+ }
+
+ public Boolean getUseActualParamName() {
+ return useActualParamName;
+ }
+
+ public void setUseActualParamName(Boolean useActualParamName) {
+ this.useActualParamName = useActualParamName;
+ }
+
+ public Boolean getReturnInstanceForEmptyRow() {
+ return returnInstanceForEmptyRow;
+ }
+
+ public void setReturnInstanceForEmptyRow(Boolean returnInstanceForEmptyRow) {
+ this.returnInstanceForEmptyRow = returnInstanceForEmptyRow;
+ }
+
+ public Boolean getShrinkWhitespacesInSql() {
+ return shrinkWhitespacesInSql;
+ }
+
+ public void setShrinkWhitespacesInSql(Boolean shrinkWhitespacesInSql) {
+ this.shrinkWhitespacesInSql = shrinkWhitespacesInSql;
+ }
+
+ public Boolean getNullableOnForEach() {
+ return nullableOnForEach;
+ }
+
+ public void setNullableOnForEach(Boolean nullableOnForEach) {
+ this.nullableOnForEach = nullableOnForEach;
+ }
+
+ public Boolean getArgNameBasedConstructorAutoMapping() {
+ return argNameBasedConstructorAutoMapping;
+ }
+
+ public void setArgNameBasedConstructorAutoMapping(Boolean argNameBasedConstructorAutoMapping) {
+ this.argNameBasedConstructorAutoMapping = argNameBasedConstructorAutoMapping;
+ }
+
+ public String getLogPrefix() {
+ return logPrefix;
+ }
+
+ public void setLogPrefix(String logPrefix) {
+ this.logPrefix = logPrefix;
+ }
+
+ public Class extends Log> getLogImpl() {
+ return logImpl;
+ }
+
+ public void setLogImpl(Class extends Log> logImpl) {
+ this.logImpl = logImpl;
+ }
+
+ public Class extends VFS> getVfsImpl() {
+ return vfsImpl;
+ }
+
+ public void setVfsImpl(Class extends VFS> vfsImpl) {
+ this.vfsImpl = vfsImpl;
+ }
+
+ public Class> getDefaultSqlProviderType() {
+ return defaultSqlProviderType;
+ }
+
+ public void setDefaultSqlProviderType(Class> defaultSqlProviderType) {
+ this.defaultSqlProviderType = defaultSqlProviderType;
+ }
+
+ public LocalCacheScope getLocalCacheScope() {
+ return localCacheScope;
+ }
+
+ public void setLocalCacheScope(LocalCacheScope localCacheScope) {
+ this.localCacheScope = localCacheScope;
+ }
+
+ public JdbcType getJdbcTypeForNull() {
+ return jdbcTypeForNull;
+ }
+
+ public void setJdbcTypeForNull(JdbcType jdbcTypeForNull) {
+ this.jdbcTypeForNull = jdbcTypeForNull;
+ }
+
+ public Set getLazyLoadTriggerMethods() {
+ return lazyLoadTriggerMethods;
+ }
+
+ public void setLazyLoadTriggerMethods(Set lazyLoadTriggerMethods) {
+ this.lazyLoadTriggerMethods = lazyLoadTriggerMethods;
+ }
+
+ public Integer getDefaultStatementTimeout() {
+ return defaultStatementTimeout;
+ }
+
+ public void setDefaultStatementTimeout(Integer defaultStatementTimeout) {
+ this.defaultStatementTimeout = defaultStatementTimeout;
+ }
+
+ public Integer getDefaultFetchSize() {
+ return defaultFetchSize;
+ }
+
+ public void setDefaultFetchSize(Integer defaultFetchSize) {
+ this.defaultFetchSize = defaultFetchSize;
+ }
+
+ public ResultSetType getDefaultResultSetType() {
+ return defaultResultSetType;
+ }
+
+ public void setDefaultResultSetType(ResultSetType defaultResultSetType) {
+ this.defaultResultSetType = defaultResultSetType;
+ }
+
+ public ExecutorType getDefaultExecutorType() {
+ return defaultExecutorType;
+ }
+
+ public void setDefaultExecutorType(ExecutorType defaultExecutorType) {
+ this.defaultExecutorType = defaultExecutorType;
+ }
+
+ public AutoMappingBehavior getAutoMappingBehavior() {
+ return autoMappingBehavior;
+ }
+
+ public void setAutoMappingBehavior(AutoMappingBehavior autoMappingBehavior) {
+ this.autoMappingBehavior = autoMappingBehavior;
+ }
+
+ public AutoMappingUnknownColumnBehavior getAutoMappingUnknownColumnBehavior() {
+ return autoMappingUnknownColumnBehavior;
+ }
+
+ public void setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior) {
+ this.autoMappingUnknownColumnBehavior = autoMappingUnknownColumnBehavior;
+ }
+
+ public Properties getVariables() {
+ return variables;
+ }
+
+ public void setVariables(Properties variables) {
+ this.variables = variables;
+ }
+
+ public Boolean getLazyLoadingEnabled() {
+ return lazyLoadingEnabled;
+ }
+
+ public void setLazyLoadingEnabled(Boolean lazyLoadingEnabled) {
+ this.lazyLoadingEnabled = lazyLoadingEnabled;
+ }
+
+ public Class> getConfigurationFactory() {
+ return configurationFactory;
+ }
+
+ public void setConfigurationFactory(Class> configurationFactory) {
+ this.configurationFactory = configurationFactory;
+ }
+
+ public Class extends TypeHandler> getDefaultEnumTypeHandler() {
+ return defaultEnumTypeHandler;
+ }
+
+ public void setDefaultEnumTypeHandler(Class extends TypeHandler> defaultEnumTypeHandler) {
+ this.defaultEnumTypeHandler = defaultEnumTypeHandler;
+ }
+
+ void applyTo(Configuration target) {
+ PropertyMapper mapper = PropertyMapper.get();
+ mapper.from(getSafeRowBoundsEnabled()).to(target::setSafeRowBoundsEnabled);
+ mapper.from(getSafeResultHandlerEnabled()).to(target::setSafeResultHandlerEnabled);
+ mapper.from(getMapUnderscoreToCamelCase()).to(target::setMapUnderscoreToCamelCase);
+ mapper.from(getAggressiveLazyLoading()).to(target::setAggressiveLazyLoading);
+ mapper.from(getMultipleResultSetsEnabled()).to(target::setMultipleResultSetsEnabled);
+ mapper.from(getUseGeneratedKeys()).to(target::setUseGeneratedKeys);
+ mapper.from(getUseColumnLabel()).to(target::setUseColumnLabel);
+ mapper.from(getCacheEnabled()).to(target::setCacheEnabled);
+ mapper.from(getCallSettersOnNulls()).to(target::setCallSettersOnNulls);
+ mapper.from(getUseActualParamName()).to(target::setUseActualParamName);
+ mapper.from(getReturnInstanceForEmptyRow()).to(target::setReturnInstanceForEmptyRow);
+ mapper.from(getShrinkWhitespacesInSql()).to(target::setShrinkWhitespacesInSql);
+ mapper.from(getNullableOnForEach()).to(target::setNullableOnForEach);
+ mapper.from(getArgNameBasedConstructorAutoMapping()).to(target::setArgNameBasedConstructorAutoMapping);
+ mapper.from(getLazyLoadingEnabled()).to(target::setLazyLoadingEnabled);
+ mapper.from(getLogPrefix()).to(target::setLogPrefix);
+ mapper.from(getLazyLoadTriggerMethods()).to(target::setLazyLoadTriggerMethods);
+ mapper.from(getDefaultStatementTimeout()).to(target::setDefaultStatementTimeout);
+ mapper.from(getDefaultFetchSize()).to(target::setDefaultFetchSize);
+ mapper.from(getLocalCacheScope()).to(target::setLocalCacheScope);
+ mapper.from(getJdbcTypeForNull()).to(target::setJdbcTypeForNull);
+ mapper.from(getDefaultResultSetType()).to(target::setDefaultResultSetType);
+ mapper.from(getDefaultExecutorType()).to(target::setDefaultExecutorType);
+ mapper.from(getAutoMappingBehavior()).to(target::setAutoMappingBehavior);
+ mapper.from(getAutoMappingUnknownColumnBehavior()).to(target::setAutoMappingUnknownColumnBehavior);
+ mapper.from(getVariables()).to(target::setVariables);
+ mapper.from(getLogImpl()).to(target::setLogImpl);
+ mapper.from(getVfsImpl()).to(target::setVfsImpl);
+ mapper.from(getDefaultSqlProviderType()).to(target::setDefaultSqlProviderType);
+ mapper.from(getConfigurationFactory()).to(target::setConfigurationFactory);
+ mapper.from(getDefaultEnumTypeHandler()).to(target::setDefaultEnumTypeHandler);
+ }
+
+ }
+
+ /**
+ * {@link com.mybatisflex.core.FlexGlobalConfig} 配置。
+ *
+ * @author 王帅
+ * @since 2023-06-21
+ */
+ public static class GlobalConfig {
+
+ /**
+ * 启动是否打印 banner 和 版本号。
+ */
+ private boolean printBanner = true;
+
+
+ /**
+ * 全局的 ID 生成策略配置,当 @Id 未配置 或者 配置 KeyType 为 None 时
+ * 使用当前全局配置。
+ */
+ @NestedConfigurationProperty
+ private FlexGlobalConfig.KeyConfig keyConfig;
+
+ /**
+ * 逻辑删除数据存在标记值。
+ */
+ private Object normalValueOfLogicDelete = FlexConsts.LOGIC_DELETE_NORMAL;
+
+ /**
+ * 逻辑删除数据删除标记值。
+ */
+ private Object deletedValueOfLogicDelete = FlexConsts.LOGIC_DELETE_DELETED;
+
+
+ /**
+ * 默认的分页查询时的每页数据量。
+ */
+ private int defaultPageSize = 10;
+
+
+ /**
+ * 默认的 Relation 注解查询深度。
+ */
+ private int defaultRelationQueryDepth = 2;
+
+ /**
+ * 默认的逻辑删除字段。
+ */
+ private String logicDeleteColumn;
+
+ /**
+ * 默认的多租户字段。
+ */
+ private String tenantColumn;
+
+ /**
+ * 默认的乐观锁字段。
+ */
+ private String versionColumn;
+
+ /**
+ * 全局忽略 @Table 中配置的 schema
+ */
+ private boolean ignoreSchema = false;
+
+ public boolean isPrintBanner() {
+ return printBanner;
+ }
+
+ public void setPrintBanner(boolean printBanner) {
+ this.printBanner = printBanner;
+ }
+
+ public FlexGlobalConfig.KeyConfig getKeyConfig() {
+ return keyConfig;
+ }
+
+ public void setKeyConfig(FlexGlobalConfig.KeyConfig keyConfig) {
+ this.keyConfig = keyConfig;
+ }
+
+ public Object getNormalValueOfLogicDelete() {
+ return normalValueOfLogicDelete;
+ }
+
+ public void setNormalValueOfLogicDelete(Object normalValueOfLogicDelete) {
+ this.normalValueOfLogicDelete = normalValueOfLogicDelete;
+ }
+
+ public Object getDeletedValueOfLogicDelete() {
+ return deletedValueOfLogicDelete;
+ }
+
+ public void setDeletedValueOfLogicDelete(Object deletedValueOfLogicDelete) {
+ this.deletedValueOfLogicDelete = deletedValueOfLogicDelete;
+ }
+
+ public int getDefaultPageSize() {
+ return defaultPageSize;
+ }
+
+ public void setDefaultPageSize(int defaultPageSize) {
+ this.defaultPageSize = defaultPageSize;
+ }
+
+ public int getDefaultRelationQueryDepth() {
+ return defaultRelationQueryDepth;
+ }
+
+ public void setDefaultRelationQueryDepth(int defaultRelationQueryDepth) {
+ this.defaultRelationQueryDepth = defaultRelationQueryDepth;
+ }
+
+ public String getLogicDeleteColumn() {
+ return logicDeleteColumn;
+ }
+
+ public void setLogicDeleteColumn(String logicDeleteColumn) {
+ this.logicDeleteColumn = logicDeleteColumn;
+ }
+
+ public String getTenantColumn() {
+ return tenantColumn;
+ }
+
+ public void setTenantColumn(String tenantColumn) {
+ this.tenantColumn = tenantColumn;
+ }
+
+ public String getVersionColumn() {
+ return versionColumn;
+ }
+
+ public void setVersionColumn(String versionColumn) {
+ this.versionColumn = versionColumn;
+ }
+
+ public boolean isIgnoreSchema() {
+ return ignoreSchema;
+ }
+
+ public void setIgnoreSchema(boolean ignoreSchema) {
+ this.ignoreSchema = ignoreSchema;
+ }
+
+ void applyTo(FlexGlobalConfig target) {
+ PropertyMapper mapper = PropertyMapper.get();
+ mapper.from(isPrintBanner()).to(target::setPrintBanner);
+ mapper.from(getKeyConfig()).to(target::setKeyConfig);
+ mapper.from(getNormalValueOfLogicDelete()).to(target::setNormalValueOfLogicDelete);
+ mapper.from(getDeletedValueOfLogicDelete()).to(target::setDeletedValueOfLogicDelete);
+ mapper.from(getDefaultPageSize()).to(target::setDefaultPageSize);
+ mapper.from(getDefaultRelationQueryDepth()).to(target::setDefaultRelationQueryDepth);
+ mapper.from(getLogicDeleteColumn()).to(target::setLogicDeleteColumn);
+ mapper.from(getVersionColumn()).to(target::setVersionColumn);
+ mapper.from(getTenantColumn()).to(target::setTenantColumn);
+ mapper.from(isIgnoreSchema()).to(target::setIgnoreSchema);
+ }
+
+ }
+
+ /**
+ * MyBatis Flex Admin 配置。
+ *
+ * @author 王帅
+ * @since 2023-07-02
+ */
+ public static class AdminConfig {
+
+ /**
+ * 启用服务。
+ */
+ private boolean enable;
+
+ /**
+ * 连接端点。
+ */
+ private String endpoint;
+
+ /**
+ * 秘密密钥。
+ */
+ private String secretKey;
+
+ public boolean isEnable() {
+ return enable;
+ }
+
+ public void setEnable(boolean enable) {
+ this.enable = enable;
+ }
+
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ }
+
+ /**
+ * Seata 配置
+ *
+ * @author life
+ */
+ public static class SeataConfig {
+
+ /**
+ * 是否开启
+ */
+ private boolean enable = false;
+
+ /**
+ * 事务模式支持,只支持XA或者AT
+ */
+ private SeataMode seataMode = SeataMode.AT;
+
+ public boolean isEnable() {
+ return enable;
+ }
+
+ public void setEnable(boolean enable) {
+ this.enable = enable;
+ }
+
+ public SeataMode getSeataMode() {
+ return seataMode;
+ }
+
+ public void setSeataMode(SeataMode seataMode) {
+ this.seataMode = seataMode;
+ }
+
+ }
+
+ /**
+ * @author life
+ */
+ public enum SeataMode {
+
+ XA,
+
+ AT
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/mybatisflex/spring/boot/MybatisLanguageDriverAutoConfiguration.java b/src/main/java/com/mybatisflex/spring/boot/MybatisLanguageDriverAutoConfiguration.java
new file mode 100644
index 0000000..f91ad97
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/MybatisLanguageDriverAutoConfiguration.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2015-2022 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 com.mybatisflex.spring.boot;
+
+import org.apache.ibatis.scripting.LanguageDriver;
+import org.mybatis.scripting.freemarker.FreeMarkerLanguageDriver;
+import org.mybatis.scripting.freemarker.FreeMarkerLanguageDriverConfig;
+import org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriver;
+import org.mybatis.scripting.thymeleaf.ThymeleafLanguageDriverConfig;
+import org.mybatis.scripting.velocity.VelocityLanguageDriver;
+import org.mybatis.scripting.velocity.VelocityLanguageDriverConfig;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 脚本语言驱动的自动配置,平常一般项目用不到,只为了同步 MyBatis 自带的 MybatisLanguageDriverAutoConfiguration。
+ * 参考:...
+ * @author Kazuki Shimizu
+ * @author michael
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(LanguageDriver.class)
+public class MybatisLanguageDriverAutoConfiguration {
+
+ private static final String CONFIGURATION_PROPERTY_PREFIX = "mybatis-flex.scripting-language-driver";
+
+ /**
+ * Configuration class for mybatis-freemarker 1.1.x or under.
+ */
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(FreeMarkerLanguageDriver.class)
+ @ConditionalOnMissingClass("org.mybatis.scripting.freemarker.FreeMarkerLanguageDriverConfig")
+ public static class LegacyFreeMarkerConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ FreeMarkerLanguageDriver freeMarkerLanguageDriver() {
+ return new FreeMarkerLanguageDriver();
+ }
+
+ }
+
+ /**
+ * Configuration class for mybatis-freemarker 1.2.x or above.
+ */
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass({FreeMarkerLanguageDriver.class, FreeMarkerLanguageDriverConfig.class})
+ public static class FreeMarkerConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ FreeMarkerLanguageDriver freeMarkerLanguageDriver(FreeMarkerLanguageDriverConfig config) {
+ return new FreeMarkerLanguageDriver(config);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".freemarker")
+ public FreeMarkerLanguageDriverConfig freeMarkerLanguageDriverConfig() {
+ return FreeMarkerLanguageDriverConfig.newInstance();
+ }
+
+ }
+
+ /**
+ * Configuration class for mybatis-velocity 2.0 or under.
+ */
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(org.mybatis.scripting.velocity.Driver.class)
+ @ConditionalOnMissingClass("org.mybatis.scripting.velocity.VelocityLanguageDriverConfig")
+ @SuppressWarnings("deprecation")
+ public static class LegacyVelocityConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ org.mybatis.scripting.velocity.Driver velocityLanguageDriver() {
+ return new org.mybatis.scripting.velocity.Driver();
+ }
+
+ }
+
+ /**
+ * Configuration class for mybatis-velocity 2.1.x or above.
+ */
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass({VelocityLanguageDriver.class, VelocityLanguageDriverConfig.class})
+ public static class VelocityConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ VelocityLanguageDriver velocityLanguageDriver(VelocityLanguageDriverConfig config) {
+ return new VelocityLanguageDriver(config);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".velocity")
+ public VelocityLanguageDriverConfig velocityLanguageDriverConfig() {
+ return VelocityLanguageDriverConfig.newInstance();
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(ThymeleafLanguageDriver.class)
+ public static class ThymeleafConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ ThymeleafLanguageDriver thymeleafLanguageDriver(ThymeleafLanguageDriverConfig config) {
+ return new ThymeleafLanguageDriver(config);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".thymeleaf")
+ public ThymeleafLanguageDriverConfig thymeleafLanguageDriverConfig() {
+ return ThymeleafLanguageDriverConfig.newInstance();
+ }
+
+ // This class provides to avoid the https://github.com/spring-projects/spring-boot/issues/21626 as workaround.
+ @SuppressWarnings("unused")
+ private static class MetadataThymeleafLanguageDriverConfig extends ThymeleafLanguageDriverConfig {
+
+ @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".thymeleaf.dialect")
+ @Override
+ public DialectConfig getDialect() {
+ return super.getDialect();
+ }
+
+ @ConfigurationProperties(CONFIGURATION_PROPERTY_PREFIX + ".thymeleaf.template-file")
+ @Override
+ public TemplateFileConfig getTemplateFile() {
+ return super.getTemplateFile();
+ }
+
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/mybatisflex/spring/boot/SpringBootVFS.java b/src/main/java/com/mybatisflex/spring/boot/SpringBootVFS.java
new file mode 100644
index 0000000..9e09d40
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/SpringBootVFS.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2015-2023 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 com.mybatisflex.spring.boot;
+
+import org.apache.ibatis.io.VFS;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
+import java.text.Normalizer;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * MyBatis 的 VFS 支持。
+ * 参考:...
+ * @author Hans Westerbeek
+ * @author Eddú Meléndez
+ * @author Kazuki Shimizu
+ * @author Michael
+ */
+public class SpringBootVFS extends VFS {
+
+ private static Charset urlDecodingCharset;
+
+ static {
+ setUrlDecodingCharset(Charset.defaultCharset());
+ }
+
+ private final ResourcePatternResolver resourceResolver;
+
+ public SpringBootVFS() {
+ this.resourceResolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader());
+ }
+
+ /**
+ * Set the charset for decoding an encoded URL string.
+ *
+ * Default is system default charset.
+ *
+ *
+ * @param charset the charset for decoding an encoded URL string
+ * @since 2.3.0
+ */
+ public static void setUrlDecodingCharset(Charset charset) {
+ urlDecodingCharset = charset;
+ }
+
+ private static String preserveSubpackageName(final String baseUrlString, final Resource resource,
+ final String rootPath) {
+ try {
+ return rootPath + (rootPath.endsWith("/") ? "" : "/") + Normalizer
+ .normalize(URLDecoder.decode(resource.getURL().toString(), urlDecodingCharset.name()), Normalizer.Form.NFC)
+ .substring(baseUrlString.length());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public boolean isValid() {
+ return true;
+ }
+
+ @Override
+ protected List list(URL url, String path) throws IOException {
+ String urlString = URLDecoder.decode(url.toString(), urlDecodingCharset.name());
+ String baseUrlString = urlString.endsWith("/") ? urlString : urlString.concat("/");
+ Resource[] resources = resourceResolver.getResources(baseUrlString + "**/*.class");
+ return Stream.of(resources).map(resource -> preserveSubpackageName(baseUrlString, resource, path))
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/src/main/java/com/mybatisflex/spring/boot/SqlSessionFactoryBeanCustomizer.java b/src/main/java/com/mybatisflex/spring/boot/SqlSessionFactoryBeanCustomizer.java
new file mode 100644
index 0000000..fd99be1
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/SqlSessionFactoryBeanCustomizer.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015-2022 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 com.mybatisflex.spring.boot;
+
+import org.mybatis.spring.SqlSessionFactoryBean;
+
+/**
+ * 参考:...
+ *
+ * 为 FlexSqlSessionFactoryBean 做自定义的配置支持。
+ *
+ * @see com.mybatisflex.spring.FlexSqlSessionFactoryBean
+ */
+@FunctionalInterface
+public interface SqlSessionFactoryBeanCustomizer {
+
+ /**
+ * 自定义 {@link SqlSessionFactoryBean}。
+ *
+ * @param factoryBean FlexSqlSessionFactoryBean
+ */
+ void customize(SqlSessionFactoryBean factoryBean);
+
+}
diff --git a/src/main/java/com/mybatisflex/spring/boot/package-info.java b/src/main/java/com/mybatisflex/spring/boot/package-info.java
new file mode 100644
index 0000000..8097571
--- /dev/null
+++ b/src/main/java/com/mybatisflex/spring/boot/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * 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
+ *