001/**
002 * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package com.mybatisflex.spring.boot;
017
018import com.mybatisflex.core.mybatis.FlexConfiguration;
019import com.mybatisflex.spring.FlexSqlSessionFactoryBean;
020import org.apache.ibatis.annotations.Mapper;
021import org.apache.ibatis.mapping.DatabaseIdProvider;
022import org.apache.ibatis.plugin.Interceptor;
023import org.apache.ibatis.scripting.LanguageDriver;
024import org.apache.ibatis.session.ExecutorType;
025import org.apache.ibatis.session.SqlSessionFactory;
026import org.apache.ibatis.type.TypeHandler;
027import org.mybatis.spring.SqlSessionFactoryBean;
028import org.mybatis.spring.SqlSessionTemplate;
029import org.mybatis.spring.mapper.MapperFactoryBean;
030import org.mybatis.spring.mapper.MapperScannerConfigurer;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.springframework.beans.BeanWrapper;
034import org.springframework.beans.BeanWrapperImpl;
035import org.springframework.beans.factory.*;
036import org.springframework.beans.factory.config.BeanDefinition;
037import org.springframework.beans.factory.support.BeanDefinitionBuilder;
038import org.springframework.beans.factory.support.BeanDefinitionRegistry;
039import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
040import org.springframework.boot.autoconfigure.AutoConfigureAfter;
041import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
042import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
043import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
044import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
045import org.springframework.boot.context.properties.EnableConfigurationProperties;
046import org.springframework.context.EnvironmentAware;
047import org.springframework.context.annotation.Bean;
048import org.springframework.context.annotation.Configuration;
049import org.springframework.context.annotation.Import;
050import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
051import org.springframework.core.env.Environment;
052import org.springframework.core.io.Resource;
053import org.springframework.core.io.ResourceLoader;
054import org.springframework.core.type.AnnotationMetadata;
055import org.springframework.util.Assert;
056import org.springframework.util.CollectionUtils;
057import org.springframework.util.ObjectUtils;
058import org.springframework.util.StringUtils;
059
060import javax.sql.DataSource;
061import java.beans.PropertyDescriptor;
062import java.util.List;
063import java.util.Optional;
064import java.util.Set;
065import java.util.stream.Collectors;
066import java.util.stream.Stream;
067
068
069/**
070 * Mybatis-Flex 的核心配置
071 * <p>
072 * <p>
073 * 参考 {@link <a href="https://github.com/mybatis/spring-boot-starter/blob/master/mybatis-spring-boot-autoconfigure/src/main/java/org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.java">
074 * https://github.com/mybatis/spring-boot-starter/blob/master/mybatis-spring-boot-autoconfigure/src/main/java/org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.java</a>}
075 * <p>
076 * 为 Mybatis-Flex 开启自动配置功能,主要修改一下几个方面:
077 * <p>
078 * 1、替换配置为 mybatis-flex 的配置前缀
079 * 2、修改 SqlSessionFactory 为 FlexSqlSessionFactoryBean
080 * 3、修改 Configuration 为 FlexConfiguration
081 */
082@Configuration(proxyBeanMethods = false)
083@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
084@ConditionalOnSingleCandidate(DataSource.class)
085@EnableConfigurationProperties(MybatisFlexProperties.class)
086@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
087public class MybatisFlexAutoConfiguration implements InitializingBean {
088
089    protected static final Logger logger = LoggerFactory.getLogger(MybatisFlexAutoConfiguration.class);
090
091    protected final MybatisFlexProperties properties;
092
093    protected final Interceptor[] interceptors;
094
095    protected final TypeHandler[] typeHandlers;
096
097    protected final LanguageDriver[] languageDrivers;
098
099    protected final ResourceLoader resourceLoader;
100
101    protected final DatabaseIdProvider databaseIdProvider;
102
103    protected final List<ConfigurationCustomizer> configurationCustomizers;
104
105    protected final List<SqlSessionFactoryBeanCustomizer> sqlSessionFactoryBeanCustomizers;
106
107    public MybatisFlexAutoConfiguration(MybatisFlexProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
108                                        ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
109                                        ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
110                                        ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider,
111                                        ObjectProvider<List<SqlSessionFactoryBeanCustomizer>> sqlSessionFactoryBeanCustomizers) {
112        this.properties = properties;
113        this.interceptors = interceptorsProvider.getIfAvailable();
114        this.typeHandlers = typeHandlersProvider.getIfAvailable();
115        this.languageDrivers = languageDriversProvider.getIfAvailable();
116        this.resourceLoader = resourceLoader;
117        this.databaseIdProvider = databaseIdProvider.getIfAvailable();
118        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
119        this.sqlSessionFactoryBeanCustomizers = sqlSessionFactoryBeanCustomizers.getIfAvailable();
120    }
121
122    @Override
123    public void afterPropertiesSet() {
124        checkConfigFileExists();
125    }
126
127    private void checkConfigFileExists() {
128        if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
129            Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
130            Assert.state(resource.exists(),
131                    "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
132        }
133    }
134
135    @Bean
136    @ConditionalOnMissingBean
137    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
138//    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
139        SqlSessionFactoryBean factory = new FlexSqlSessionFactoryBean();
140        factory.setDataSource(dataSource);
141        if (properties.getConfiguration() == null || properties.getConfiguration().getVfsImpl() == null) {
142            factory.setVfs(SpringBootVFS.class);
143        }
144        if (StringUtils.hasText(this.properties.getConfigLocation())) {
145            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
146        }
147        applyConfiguration(factory);
148        if (this.properties.getConfigurationProperties() != null) {
149            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
150        }
151        if (!ObjectUtils.isEmpty(this.interceptors)) {
152            factory.setPlugins(this.interceptors);
153        }
154        if (this.databaseIdProvider != null) {
155            factory.setDatabaseIdProvider(this.databaseIdProvider);
156        }
157        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
158            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
159        }
160        if (this.properties.getTypeAliasesSuperType() != null) {
161            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
162        }
163        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
164            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
165        }
166        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
167            factory.setTypeHandlers(this.typeHandlers);
168        }
169        Resource[] mapperLocations = this.properties.resolveMapperLocations();
170        if (!ObjectUtils.isEmpty(mapperLocations)) {
171            factory.setMapperLocations(mapperLocations);
172        }
173        Set<String> factoryPropertyNames = Stream
174                .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
175                .collect(Collectors.toSet());
176        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
177        if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
178            // Need to mybatis-spring 2.0.2+
179            factory.setScriptingLanguageDrivers(this.languageDrivers);
180            if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
181                defaultLanguageDriver = this.languageDrivers[0].getClass();
182            }
183        }
184        if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
185            // Need to mybatis-spring 2.0.2+
186            factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
187        }
188        applySqlSessionFactoryBeanCustomizers(factory);
189        return factory.getObject();
190    }
191
192    protected void applyConfiguration(SqlSessionFactoryBean factory) {
193        MybatisFlexProperties.CoreConfiguration coreConfiguration = this.properties.getConfiguration();
194//    Configuration configuration = null;
195        FlexConfiguration configuration = null;
196        if (coreConfiguration != null || !StringUtils.hasText(this.properties.getConfigLocation())) {
197            configuration = new FlexConfiguration();
198        }
199        if (configuration != null && coreConfiguration != null) {
200            coreConfiguration.applyTo(configuration);
201        }
202        if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
203            for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
204                customizer.customize(configuration);
205            }
206        }
207        factory.setConfiguration(configuration);
208    }
209
210    protected void applySqlSessionFactoryBeanCustomizers(SqlSessionFactoryBean factory) {
211        if (!CollectionUtils.isEmpty(this.sqlSessionFactoryBeanCustomizers)) {
212            for (SqlSessionFactoryBeanCustomizer customizer : this.sqlSessionFactoryBeanCustomizers) {
213                customizer.customize(factory);
214            }
215        }
216    }
217
218    @Bean
219    @ConditionalOnMissingBean
220    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
221        ExecutorType executorType = this.properties.getExecutorType();
222        if (executorType != null) {
223            return new SqlSessionTemplate(sqlSessionFactory, executorType);
224        } else {
225            return new SqlSessionTemplate(sqlSessionFactory);
226        }
227    }
228
229    /**
230     * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
231     * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
232     * similar to using Spring Data JPA repositories.
233     */
234    public static class AutoConfiguredMapperScannerRegistrar
235            implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
236
237        private BeanFactory beanFactory;
238        private Environment environment;
239
240        @Override
241        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
242
243            if (!AutoConfigurationPackages.has(this.beanFactory)) {
244                logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
245                return;
246            }
247
248            logger.debug("Searching for mappers annotated with @Mapper");
249
250            List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
251            if (logger.isDebugEnabled()) {
252                packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
253            }
254
255            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
256            builder.addPropertyValue("processPropertyPlaceHolders", true);
257            builder.addPropertyValue("annotationClass", Mapper.class);
258            builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
259            BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
260            Set<String> propertyNames = Stream.of(beanWrapper.getPropertyDescriptors()).map(PropertyDescriptor::getName)
261                    .collect(Collectors.toSet());
262            if (propertyNames.contains("lazyInitialization")) {
263                // Need to mybatis-spring 2.0.2+
264                builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
265            }
266            if (propertyNames.contains("defaultScope")) {
267                // Need to mybatis-spring 2.0.6+
268                builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
269            }
270
271            // for spring-native
272            boolean injectSqlSession = environment.getProperty("mybatis.inject-sql-session-on-mapper-scan", Boolean.class,
273                    Boolean.TRUE);
274            if (injectSqlSession && this.beanFactory instanceof ListableBeanFactory) {
275                ListableBeanFactory listableBeanFactory = (ListableBeanFactory) this.beanFactory;
276                Optional<String> sqlSessionTemplateBeanName = Optional
277                        .ofNullable(getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory));
278                Optional<String> sqlSessionFactoryBeanName = Optional
279                        .ofNullable(getBeanNameForType(SqlSessionFactory.class, listableBeanFactory));
280                if (sqlSessionTemplateBeanName.isPresent() || !sqlSessionFactoryBeanName.isPresent()) {
281                    builder.addPropertyValue("sqlSessionTemplateBeanName",
282                            sqlSessionTemplateBeanName.orElse("sqlSessionTemplate"));
283                } else {
284                    builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryBeanName.get());
285                }
286            }
287            builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
288
289            registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
290        }
291
292        @Override
293        public void setBeanFactory(BeanFactory beanFactory) {
294            this.beanFactory = beanFactory;
295        }
296
297        @Override
298        public void setEnvironment(Environment environment) {
299            this.environment = environment;
300        }
301
302        private String getBeanNameForType(Class<?> type, ListableBeanFactory factory) {
303            String[] beanNames = factory.getBeanNamesForType(type);
304            return beanNames.length > 0 ? beanNames[0] : null;
305        }
306
307    }
308
309    /**
310     * If mapper registering configuration or mapper scanning configuration not present, this configuration allow to scan
311     * mappers based on the same component-scanning path as Spring Boot itself.
312     */
313    @org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
314    @Import(AutoConfiguredMapperScannerRegistrar.class)
315    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
316    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
317
318        @Override
319        public void afterPropertiesSet() {
320            logger.debug(
321                    "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
322        }
323
324    }
325
326}