1、为什么用starter

我们知道,springboot框架是为了简化spring框架开发推出的,那么,在之前的spring框架开发中,如果我们需要连接数据库,引入mybatis框架,需要怎么操作呢?我们需要去maven仓库找mybatis的jar包,还要找mybatis-spring的jar包,然后在applicationContext.xml文件中配置dataSource和mybatis相关信息,还有数据库驱动包等,可见,整个步骤相当麻烦。另外,我们还要考虑版本兼容的问题。因此,springboot为了简化开发,解决上述问题,引入了starter机制。

2、什么是starter

我们还以mybatis-springboot-starter为例,看看它都有哪些东西。找到对应的jar包,展开发现,内容很少,如下

springboot启动卡在mybatisplus springboot mybatis starter_mybatis


pom.properties:配置了maven所需的version、groupId和artifactId信息

springboot启动卡在mybatisplus springboot mybatis starter_java_02


pom.xml:配置了所需要的jar包,我们重点关注下mybatis-spring-boot-autoconfigure这个包

springboot启动卡在mybatisplus springboot mybatis starter_mybatis_03


MANIFEST.MF:配置了jar包的信息,这个我们不过多关注

spring.provides:主要给ide使用,也不过多关注

我们可以看到,这个starter很简单,一行实际代码都没有,那么它是如何工作的呢,我们看下上面说到的mybatis-spring-boot-autoconfigure这个包,看到autoconfigure有没有眼前一亮,这个自动装配之前在SpringBoot自动装配原理有过讲解,接下来,我们看下这个包。

springboot启动卡在mybatisplus springboot mybatis starter_mybatis_04


pom.properties、pom.xml和MANIFEST.MF这三个不再赘述

additional-spring-configuration-metadata.json和spring-configuration-metadata.json这两个文件配置了一些properties属性信息,都属于ide配置提示,例如我们引入这个starter后,在配置文件输入mybatis,ide自动弹出一些属性配置

springboot启动卡在mybatisplus springboot mybatis starter_ide_05


那么这两个文件有什么区别呢?如果pom.xml中引入了spring-boot-configuration-processor这个包,则会自动生成spring-configuration-metadata.json文件,如果需要手动修改里面的元数据,则可以在additional-spring-configuration-metadata.json中编辑,最终两个文件中的元数据会合并到一起。

spring.factories:这个文件的内容是key-value形式,key是EnableAutoConfiguration,value就是要自动装配的内容(即对应的XXXAutoConfiguration.java文件的类路径)。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

ConfigurationCustomizer.java:一个自定义回调接口
SpringBootVFS.java:用于扫面嵌套的jar包
MybatisProperties.java:一个JavaBean,配置了mybatis初始化需要的一些属性,接下看相当重要的MybatisAutoConfiguration.java文件的内容

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.mybatis.spring.boot.autoconfigure;

import java.util.Iterator;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.ClassPathMapperScanner;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisAutoConfiguration {
    private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
    private final MybatisProperties properties;
    private final Interceptor[] interceptors;
    private final ResourceLoader resourceLoader;
    private final DatabaseIdProvider databaseIdProvider;
    private final List<ConfigurationCustomizer> configurationCustomizers;

    public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
        this.properties = properties;
        this.interceptors = (Interceptor[])interceptorsProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = (DatabaseIdProvider)databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = (List)configurationCustomizersProvider.getIfAvailable();
    }

    @PostConstruct
    public 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 SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }

        org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
        if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
            configuration = new org.apache.ibatis.session.Configuration();
        }

        if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
            Iterator var4 = this.configurationCustomizers.iterator();

            while(var4.hasNext()) {
                ConfigurationCustomizer customizer = (ConfigurationCustomizer)var4.next();
                customizer.customize(configuration);
            }
        }

        factory.setConfiguration(configuration);
        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 (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }

        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }

        return factory.getObject();
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
    }

    @Configuration
    @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
    @ConditionalOnMissingBean({MapperFactoryBean.class})
    public static class MapperScannerRegistrarNotFoundConfiguration {
        public MapperScannerRegistrarNotFoundConfiguration() {
        }

        @PostConstruct
        public void afterPropertiesSet() {
            MybatisAutoConfiguration.logger.debug("No {} found.", MapperFactoryBean.class.getName());
        }
    }

    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
        private BeanFactory beanFactory;
        private ResourceLoader resourceLoader;

        public AutoConfiguredMapperScannerRegistrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
            ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

            try {
                if (this.resourceLoader != null) {
                    scanner.setResourceLoader(this.resourceLoader);
                }

                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
                if (MybatisAutoConfiguration.logger.isDebugEnabled()) {
                    Iterator var5 = packages.iterator();

                    while(var5.hasNext()) {
                        String pkg = (String)var5.next();
                        MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
                    }
                }

                scanner.setAnnotationClass(Mapper.class);
                scanner.registerFilters();
                scanner.doScan(StringUtils.toStringArray(packages));
            } catch (IllegalStateException var7) {
                MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", var7);
            }

        }

        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            this.beanFactory = beanFactory;
        }

        public void setResourceLoader(ResourceLoader resourceLoader) {
            this.resourceLoader = resourceLoader;
        }
    }
}

这个文件由@Configuration标明这是一个配置类,配置了很多bean,例如SqlSessionFactorySqlSessionTemplate,最重要的是SqlSessionFactory,它用来创建SqlSession,用来操作数据库,@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})@ConditionalOnBean({DataSource.class})分别注明只有SqlSessionFactory类和SqlSessionFactoryBean类、DataSource实例时,该配置生效。@EnableConfigurationProperties({MybatisProperties.class})自动配置MybatisProperties.java中的属性,@AutoConfigureAfter({DataSourceAutoConfiguration.class})注明这个配置类在DataSourceAutoConfiguration之后配置。
通过上面的例子,我们可以发现,编写一个starter主要步骤如下:

  • 定义一个xxx-spring-boot-starter空项目,只包含pom.properties和pom.xml,pom.xml文件中包含xxx-spring-boot-autoconfigure包
  • 定义一个xxx-spring-boot-autoconfigure项目,包含xxxAutoConfiguration类,该类定义一些bean、spring.factories文件增加k-v键值对、XXXProperties类,定义配置值
    接下来,尝试自定义一个starter。

3、自定义starter

首先,建一个空项目randompwd-spring-boot-starter,pom文件如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.zy</groupId>
    <artifactId>randompwd-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>randompwd-spring-boot-starter</name>
    <description>Demo for Custom Starter</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.example.zy</groupId>
            <artifactId>randompwd-spring-boot-autoconfigure</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

创建randompwd-spring-boot-autoconfigure项目,pom文件如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.zy</groupId>
    <artifactId>randompwd-spring-boot-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>randompwd-spring-boot-autoconfigure</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

在resource下创建META-INF文件夹,创建spring.factories文件,配置k-v键值对

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.zy.RandomPwdAutoConfiguration

新建RandomPwdAutoConfiguration.java文件,(可以先新建该文件再配置k-v值,防止出错,配置成功可以从k-v值点到该文件),代码如下

package com.example.zy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zy
 * @version 1.0.0
 * @ClassName RandomPwdAutoConfiguration.java
 * @Description TODO
 * @createTime 2022/12/14
 */
//标记配置类
@Configuration
//检测到含有RandomPwdProperties类生效
@ConditionalOnClass(RandomPwdProperties.class)
//自动加载配置类
@EnableConfigurationProperties(RandomPwdProperties.class)
public class RandomPwdAutoConfiguration {
    @Autowired
    private RandomPwdProperties randomPwdProperties;
    @Bean
    public RandomPwdService randomPwdService(){
        return new RandomPwdService(randomPwdProperties.getLength());
    }
}

对应的RandomPwdProperties代码如下,它会把配置文件中以randompwd开头的配置读取

package com.example.zy;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author zy
 * @version 1.0.0
 * @ClassName RandomPwdProperties.java
 * @Description TODO
 * @createTime 2022/12/14
 */
@ConfigurationProperties(prefix="randompwd")
public class RandomPwdProperties {
    private int length;


    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }
}

RandomPwdService代码如下

package com.example.zy;

import java.util.Random;

/**
 * @author zy
 * @version 1.0.0
 * @ClassName RandomPwdService.java
 * @Description TODO
 * @createTime 2022/12/14
 */
public class RandomPwdService {
    private int length;

    public RandomPwdService(int length) {
        this.length = length;
    }

    public  String randomPwd(){
        char charr[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
        char charq[] = "1234567890".toCharArray();
        StringBuilder sb = new StringBuilder();
        Random r = new Random();
        for (int x = 0; x < length - 6; ++x) {

            sb.append(charr[r.nextInt(charr.length)]);
        }
        for (int x = 0; x < 6; ++x) {
            sb.append(charq[r.nextInt(charq.length)]);
        }
        return sb.toString();
    }
}

将这两个打包,然后我们新建一个测试类,或者在其他项目上简单测试,引入我们的starter

<dependency>
            <groupId>com.example.zy</groupId>
            <artifactId>randompwd-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

配置文件敲入random,可以看到,提示了配置

springboot启动卡在mybatisplus springboot mybatis starter_java_06


然后写一个测试接口

@Autowired
    private RandomPwdService randomPwdService;
    @RequestMapping("/randomPwd")
    public String randomPwd(){
        return randomPwdService.randomPwd();
    }

启动项目,测试接口,发现正常使用,至此,自定义Starter成功。

springboot启动卡在mybatisplus springboot mybatis starter_ide_07