一起聊聊SpringBoot的starter机制

前言 我们都知道,spring的功能非常强大,但也有些弊端。比如:我们需要手动去配置大量的参数,没有默认值,需要我们管理大量的jar包和它们的依赖。
为了提升spring项目的开发效率,简化一些配置,spring官方引入了springboot。
当然,引入springboot还有其他原因,在这里就不过多描述了。
本文重点跟大家一起聊聊springboot的starter机制,因为它太重要了。
基于 spring boot + mybatis plus + vue & element 实现的后台管理系统 + 用户小程序,支持 rbac 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/yunaiv/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/ 1 为什么要用starter? 在springboot还没有出来之前,我们使用spring开发项目。如果程序需要连接数据库,我们一般会使用hibernate或mybatis等orm框架,这里我以mybatis为例,具体的操作步骤如下:
到maven仓库去找需要引入的mybatis jar包,选取合适的版本。 到maven仓库去找mybatis-spring整合的jar包,选取合适的版本。 在spring的applicationcontext.xml文件中配置datasource和mybatis相关信息。 当然有些朋友可能会指正,不是还需要引入数据库驱动包吗?
确实需要引入,但数据库驱动有很多,比如:mysql、oracle、sqlserver,这不属于mybatis的范畴,使用者可以根据项目的实际情况单独引入。
如果程序只是需要连接数据库这一个功能还好,按上面的步骤做基本可以满足需求。但是,连接数据库可能只是庞大的项目体系中一个环节,实际项目中往往更复杂,需要引入更多的功能,比如:连接redis、连接mongodb、使用rocketmq、使用excel功能等等。
引入这些功能的话,需要再把上面的步骤再重复一次,工作量无形当中增加了不少,而且有很多重复的工作。
另外,还是有个问题,每次到要到maven中找合适的版本,如果哪次找的mybatis.jar包 和 mybatis-spring.jar包版本不兼容,程序不是会出现问题?
springboot为了解决以上两个问题引入了starter机制。
基于 spring cloud alibaba + gateway + nacos + rocketmq + vue & element 实现的后台管理系统 + 用户小程序,支持 rbac 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/yunaiv/yudao-cloud 视频教程:https://doc.iocoder.cn/video/ 2 starter有哪些要素? 我们首先一起看看mybatis-spring-boot-starter.jar是如何定义的。
可以看到它的meta-inf目录下只包含了:
pom.protperties  配置maven所需的项目version、groupid和artifactid。 pom.xml  配置所依赖的jar包。 manifest.mf 这个文件描述了该jar文件的很多信息。 spring.provides 配置所依赖的artifactid,给ide使用的,没有其他的作用。 注意一下,没有一行代码。
我们重点看一下pom.xml,因为这个jar包里面除了这个没有啥重要的信息
  4.0.0      org.mybatis.spring.boot    mybatis-spring-boot    1.3.1    mybatis-spring-boot-starter  mybatis-spring-boot-starter            org.springframework.boot      spring-boot-starter              org.springframework.boot      spring-boot-starter-jdbc              org.mybatis.spring.boot      mybatis-spring-boot-autoconfigure              org.mybatis      mybatis              org.mybatis      mybatis-spring       从上面可以看出,pom.xml文件中会引入一些jar包,其中除了引入spring-boot-starter,之外重点看一下:mybatis-spring-boot-autoconfigure。
我们找到mybatis-spring-boot-autoconfigure.jar文件,打开这个文件。
里面包含如下文件:
pom.properties  配置maven所需的项目version、groupid和artifactid pom.xml  配置所依赖的jar包 additional-spring-configuration-metadata.json  手动添加ide提示功能 manifest.mf 这个文件描述了该jar文件的很多信息 spring.factories spi会读取的文件 spring-configuration-metadata.json 系统自动生成的ide提示功能 configurationcustomizer 自定义configuration回调接口 mybatisautoconfiguration mybatis配置类 mybatisproperties mybatis属性类 springbootvfs 扫描嵌套的jar包中的类 spring-configuration-metadata.json和additional-spring-configuration-metadata.json的功能差不多,我们在applicationcontext.properties文件中输入spring时,会自动出现下面的配置信息可供选择,就是这个功能了。
来自灵魂的一问:这两个文件有什么区别?
答:如果pom.xml中引入了spring-boot-configuration-processor包,则会自动生成spring-configuration-metadata.json。
如果需要手动修改里面的元数据,则可以在additional-spring-configuration-metadata.json中编辑,最终两个文件中的元数据会合并到一起。
mybatisproperties类是属性实体类:
@configurationproperties(prefix = mybatisproperties.mybatis_prefix)public class mybatisproperties {  public static final string mybatis_prefix = mybatis;  private string configlocation;  private string[] mapperlocations;  private string typealiasespackage;  private string typehandlerspackage;  private boolean checkconfiglocation = false;  private executortype executortype;  private properties configurationproperties;  @nestedconfigurationproperty  private configuration configuration;  public string getconfiglocation() {    return this.configlocation;  }  public void setconfiglocation(string configlocation) {    this.configlocation = configlocation;  }  @deprecated  public string getconfig() {    return this.configlocation;  }  @deprecated  public void setconfig(string config) {    this.configlocation = config;  }  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;  }  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;  }  public properties getconfigurationproperties() {    return configurationproperties;  }  public void setconfigurationproperties(properties configurationproperties) {    this.configurationproperties = configurationproperties;  }  public configuration getconfiguration() {    return configuration;  }  public void setconfiguration(configuration configuration) {    this.configuration = configuration;  }  public resource[] resolvemapperlocations() {    resourcepatternresolver resourceresolver = new pathmatchingresourcepatternresolver();    list resources = new arraylist();    if (this.mapperlocations != null) {      for (string mapperlocation : this.mapperlocations) {        try {          resource[] mappers = resourceresolver.getresources(mapperlocation);          resources.addall(arrays.aslist(mappers));        } catch (ioexception e) {          // ignore        }      }    }    return resources.toarray(new resource[resources.size()]);  }} 可以看到mybatis初始化所需要的很多属性都在这里,相当于一个javabean。
下面重点看一下mybatisautoconfiguration的代码:
@org.springframework.context.annotation.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 configurationcustomizers;  public mybatisautoconfiguration(mybatisproperties properties,                                  objectprovider interceptorsprovider,                                  resourceloader resourceloader,                                  objectprovider databaseidprovider,                                  objectprovider configurationcustomizersprovider) {    this.properties = properties;    this.interceptors = interceptorsprovider.getifavailable();    this.resourceloader = resourceloader;    this.databaseidprovider = databaseidprovider.getifavailable();    this.configurationcustomizers = 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()));    }    configuration configuration = this.properties.getconfiguration();    if (configuration == null && !stringutils.hastext(this.properties.getconfiglocation())) {      configuration = new configuration();    }    if (configuration != null && !collectionutils.isempty(this.configurationcustomizers)) {      for (configurationcustomizer customizer : this.configurationcustomizers) {        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();    if (executortype != null) {      return new sqlsessiontemplate(sqlsessionfactory, executortype);    } else {      return new sqlsessiontemplate(sqlsessionfactory);    }  }  public static class autoconfiguredmapperscannerregistrar      implements beanfactoryaware, importbeandefinitionregistrar, resourceloaderaware {    private beanfactory beanfactory;    private resourceloader resourceloader;    @override    public void registerbeandefinitions(annotationmetadata importingclassmetadata, beandefinitionregistry registry) {      classpathmapperscanner scanner = new classpathmapperscanner(registry);      try {        if (this.resourceloader != null) {          scanner.setresourceloader(this.resourceloader);        }        list packages = autoconfigurationpackages.get(this.beanfactory);        if (logger.isdebugenabled()) {          for (string pkg : packages) {            logger.debug(using auto-configuration base package '{}', pkg);          }        }        scanner.setannotationclass(mapper.class);        scanner.registerfilters();        scanner.doscan(stringutils.tostringarray(packages));      } catch (illegalstateexception ex) {        logger.debug(could not determine auto-configuration package, automatic mapper scanning disabled., ex);      }    }    @override    public void setbeanfactory(beanfactory beanfactory) throws beansexception {      this.beanfactory = beanfactory;    }    @override    public void setresourceloader(resourceloader resourceloader) {      this.resourceloader = resourceloader;    }  }  @org.springframework.context.annotation.configuration  @import({ autoconfiguredmapperscannerregistrar.class })  @conditionalonmissingbean(mapperfactorybean.class)  public static class mapperscannerregistrarnotfoundconfiguration {      @postconstruct    public void afterpropertiesset() {      logger.debug(no {} found., mapperfactorybean.class.getname());    }  }} 这个类就是一个configuration(配置类),它里面定义很多bean,其中最重要的就是sqlsessionfactory的bean实例,该实例是mybatis的核心功能,用它创建sqlsession,对数据库进行crud操作。
除此之外,mybatisautoconfiguration类还包含了:
@conditionalonclass 配置了只有包含sqlsessionfactory.class和sqlsessionfactorybean.class,该配置类才生效。 @conditionalonbean 配置了只有包含datasource实例时,该配置类才生效。 @enableconfigurationproperties 该注解会自动填充mybatisproperties实例中的属性。 autoconfigureafter 配置了该配置类在datasourceautoconfiguration类之后自动配置。 这些注解都是一些辅助功能,决定configuration是否生效,当然这些注解不是必须的。
接下来,重点看看spring.factories文件有啥内容:
org.springframework.boot.autoconfigure.enableautoconfiguration=org.mybatis.spring.boot.autoconfigure.mybatisautoconfiguration 里面只有一行配置,即key为enableautoconfiguration,value为mybatisautoconfiguration。
好了,介绍了这么多东西,现在我们来总结一下,
starter几个要素如下图所示:
那么,编写starter需要哪些步骤?
1.需要定义一个名称为xxx-spring-boot-starter的空项目,里面不包含任何代码,可以有pom.xml和pom.properties文件。 2.pom.xml文件中包含了名称为xxx-spring-boot-autoconfigure的项目。 3.xxx-spring-boot-autoconfigure项目中包含了名称为xxxautoconfiguration的类,该类可以定义一些bean实例。当然,configuration类上可以打一些如:conditionalonclass、conditionalonbean、enableconfigurationproperties等注解。 4.需要在spring.factories文件中增加key为enableautoconfiguration,value为xxxautoconfiguration。 我们试着按照这四步,自己编写一个starter看看能否成功,验证一下总结的内容是否正确。
3 如何定义自己的starter? 3.1 先创建一个空项目 该项目名称为id-generate-starter,注意为了方便我把项目重命名了,原本应该是叫id-generate-spring-boot-starter的,如下图所示:
pom.xml文件定义如下:
    4.0.0    1.3.1    com.sue    id-generate-spring-boot-starter    id-generate-spring-boot-starter                        com.sue            id-generate-spring-boot-autoconfigure            1.3.1             我们看到,它只引入了id-generate-spring-boot-autoconfigure。当然如果有需要这里还可以引入多个autoconfigure或者多个其他jar包或者。
3.2 创建id-generate-autoconfigure 同样为了方便我把项目重命名了,原本是叫id-generate-spring-boot-autoconfigure,如下图所示:
该项目当中包含:pom.xml、spring.factories、idgenerateautoconfiguration、idgenerateservice 和 idproperties 这5个关键文件,下面我们逐一看看。
先从pom.xml
            org.springframework.boot        spring-boot-starter-parent        2.0.4.release        4.0.0    1.3.1    com.sue    id-generate-spring-boot-autoconfigure    id-generate-spring-boot-autoconfigure                        org.springframework.boot            spring-boot-starter                            org.springframework.boot            spring-boot-autoconfigure                            org.springframework.boot            spring-boot-configuration-processor            true                                                    org.apache.maven.plugins                maven-compiler-plugin                                    1.8                    1.8                                         我们可以看到,这个文件比较简单就引入了:
spring-boot-starter:springboot的相关jar包。 spring-boot-autoconfigure:springboot自动配置相关jar包。 spring-boot-configuration-processor:springboot生成ide提示功能相关jar包。 重点看看spring.factories文件:
org.springframework.boot.autoconfigure.enableautoconfiguration=com.sue.idgenerateautoconfiguration 它里面只包含一行配置,其中key是enableautoconfiguration,value是idgenerateautoconfiguration。
再重点看一下idgenerateautoconfiguration
@conditionalonclass(idproperties.class)@enableconfigurationproperties(idproperties.class)@configurationpublic class idgenerateautoconfiguration {    @autowired    private idproperties properties;    @bean    public idgenerateservice idgenerateservice() {        return new idgenerateservice(properties.getworkid());    }} 该类是一个使用了@configuration注解标记为了配置类,生效的条件是@conditionalonclass注解中检测到包含idproperties.class。并且使用@enableconfigurationproperties注解会自动注入idproperties的实例。
此外,最关键的点是该类里面创建了idgenerateservice的bean实例,这是自动配置的精髓。
再看看idgenerateservice
public class idgenerateservice {    private long workid;    public idgenerateservice(long workid) {        this.workid = workid;    }    public long generate() {        return new random().nextint(100) + this.workid;    }} 我们可以看到它是一个普通的类,甚至都没有使用@service注解,里面有个generate方法,根据workid的值和随机数动态生成id。
最后看看idproperties
@configurationproperties(prefix = idproperties.prefix)public class idproperties {    public static final string prefix = sue;    private long workid;    public long getworkid() {        return workid;    }    public void setworkid(long workid) {        this.workid = workid;    }} 它是一个配置实体类,里面包含了相关的配置文件。使用@configurationproperties注解,会自动把application.properties文件中以sue开通的,参数名称跟idproperties中一样的参数值,自动注入到idproperties对象中。
3.3 创建id-generate-test 这个项目主要用于测试。
该项目里面包含:pom.xml、application.properties、application 和 testrunner 文件。
先看看pom.xml文件
    4.0.0    1.3.1    com.sue    spring-boot-id-generate-test    spring-boot-id-generate-test                        com.sue            id-generate-spring-boot-starter            1.3.1             由于只测试刚刚定义的id生成功能,所以只引入的id-generate-spring-boot-starter jar包。
application.properties配置资源文件
sue.workid=123 只有一行配置,因为我们的idproperties中目前只需要这一个参数。
application是测试程序启动类
@springbootapplicationpublic class application {    public static void main(string[] args) {        springapplication.run(application.class, args);    }} 很简单,就是一个普通的springboot启动类
testrunner是我们的测试类
@componentpublic class testrunner implements applicationrunner {    @autowired    private idgenerateservice idgenerateservice;    public void run(applicationarguments args) throws exception {        long sysno = idgenerateservice.generate();        system.out.println(sysno);    }} 它实现了applicationrunner接口,所以在springboot启动的时候会调用该类的run方法。
好了,所有自定义starter的代码和测试代码都已经就绪。接下,运行一下application类的main方法。
运行结果:
176 完美,验证成功了。
接下来,我们分析一下starter的底层实现原理。
4 starter的底层原理是什么? 通过上面编写自己的starter的例子,相信大家对starter的认识更进一步了,现在跟大家一起看看starter的底层是如何实现的。
id-generate-starter.jar其实是一个空项目,依赖于id-generate-autoconfiguration.jar。
id-generate-starter.jar是一个入口,我们给他取一个更优雅的名字:门面模式,其他业务系统想引入相应的功能,必须要通过这个门面。
我们重点分析一下 id-generate-autoconfiguration.jar
该jar包核心内容是:idgenerateconfiguration,这个配置类中创建了idgenerateservice对象,idgenerateservice是我们所需要自动配置的具体功能。
接下来一个最重要的问题:idgenerateconfiguration为什么会自动加载的呢?
还记得我们定义的spring.factories文件不?
org.springframework.boot.autoconfigure.enableautoconfiguration=com.sue.idgenerateautoconfiguration 它里面只包含一行配置,其中key是enableautoconfiguration,value是idgenerateautoconfiguration。
要搞明白这个过程,要从application类的@springbootapplication注解开始:
@target(elementtype.type)@retention(retentionpolicy.runtime)@documented@inherited@springbootconfiguration@enableautoconfiguration@componentscan(excludefilters = {    @filter(type = filtertype.custom, classes = typeexcludefilter.class),    @filter(type = filtertype.custom, classes = autoconfigurationexcludefilter.class) })public @interface springbootapplication {  @aliasfor(annotation = enableautoconfiguration.class)  class[] exclude() default {};  @aliasfor(annotation = enableautoconfiguration.class)  string[] excludename() default {};  @aliasfor(annotation = componentscan.class, attribute = basepackages)  string[] scanbasepackages() default {};    @aliasfor(annotation = componentscan.class, attribute = basepackageclasses)  class[] scanbasepackageclasses() default {};} 从上面可以看出该注解里面包含了@enableautoconfiguration注解。
@target(elementtype.type)@retention(retentionpolicy.runtime)@documented@inherited@autoconfigurationpackage@import(autoconfigurationimportselector.class)public @interface enableautoconfiguration {  string enabled_override_property = spring.boot.enableautoconfiguration;  class[] exclude() default {};  string[] excludename() default {};} @enableautoconfiguration注解会引入autoconfigurationimportselector类。
该类的selectimports方法一个关键方法:
@override  public string[] selectimports(annotationmetadata annotationmetadata) {    //配置有没有配置spring.boot.enableautoconfiguration开关,默认为true    //如果为false,则不执行自动配置的功能,直接返回    if (!isenabled(annotationmetadata)) {      return no_imports;    }    //找spring-autoconfigure-metadata.properties中的元素    autoconfigurationmetadata autoconfigurationmetadata = autoconfigurationmetadataloader        .loadmetadata(this.beanclassloader);    //获取enableautoconfiguration注解中的属性     annotationattributes attributes = getattributes(annotationmetadata);    //获取工程下所有配置key为enableautoconfiguration的值,即idgenerateconfiguration等类。    list configurations = getcandidateconfigurations(annotationmetadata,        attributes);    //删除重复的值        configurations = removeduplicates(configurations);    //获取需要排除的规则列表    set exclusions = getexclusions(annotationmetadata, attributes);    //检查    checkexcludedclasses(configurations, exclusions);    //删除需要排除的值    configurations.removeall(exclusions);    //根据配置文件中配置的开关,过滤一部分不满足条件的值    configurations = filter(configurations, autoconfigurationmetadata);    fireautoconfigurationimportevents(configurations, exclusions);    return stringutils.tostringarray(configurations);  } 这里就是starter能够自动配置的秘密。
此外,有些朋友看其他人定义的springboot starter可能会有疑惑。
先看看druid-spring-boot-starter
alibaba定义的druid-spring-boot-starter只有xxx-spring-boot-starter.jar文件,而没有xxx-spring-boot-autoconfigure.jar文件。
再看看spring-boot-starter-jdbc:
更神奇的是这个文件中连pom.xml都没有,一脸懵逼。。。。。。。
是不是我讲错了?
答:其实没有。
springboot的原则是约定优于配置。
从spring-boot-starter-jdbc内部空实现来看,它的约定是要把xxx-spring-boot-starter.jar和xxx-spring-boot-autoconfigure.jar区分开的。个人认为,alibaba定义得并不好,没有遵照springboot的约定,虽然功能不受影响。(这个地方欢迎一起探讨一下)
而springboot自己定义的spring-boot-starter-jdbc为什么连pom.xml文件也没有呢?
它不需要依赖xxx-spring-boot-autoconfigure.jar文件吗?
因为springboot把所有的自动配置的类都统一放到spring-boot-autoconfigure.jar下面了:
spring.factories文件内容如下:
springboot这样集中管理自动配置,而不需要从各个子包中遍历,我个人认为是为了查找效率。
我们最后再看看spring-cloud-starter-openfegin
明显看到,它是遵循了我们说的原则的。
除此之外,还有一个原则一顺便提一下。
springboot和springcloud系列定义jar包的名称是:
spring-boot-starter-xxx.jar spring-cloud-starter-xxx.jar 而我们自己的项目定义的jar应该是:


应用智能家居领域中的低功耗蓝牙模块
恒峰—配网行波型故障预警与定位装置:保障电力系统安全稳定运行的重要利器
区块链技术走向十字路口,下一步该如何走?
一种高压直流母线电压的采样电路(一)
欧盟高层表示将在欧洲大陆建设电池供应链,规划发展电池产能
一起聊聊SpringBoot的starter机制
15A电源模块可以有多小?
成功案例|KEZMO牛只RFID自动称重管理
贴片绕线电感规格相同就能互相替代吗
市场监管总局依法对阿里巴巴集团实施“二选一”等涉嫌垄断行为立案调查
研华新推出WISE-5000物联网边缘工业控制器
新款iPhone 重大改变!这一特性彻底告别:压力触感没了
IC行业明年复苏在即,电子产品销售Q3将环比增10%
递归指的是在函数的定义中使用函数自身的方法
低压降肖特基,VF低到0.3V
纵观智能家居主要功能以及发展趋势
NVIDIA刷榜 双核没人要 Steam玩家最爱PC是这样
2020重磅开局!ITES深圳工业展,5月见
GTC23 | NVIDIA 携手谷歌云提供强大的全新生成式 AI 平台,基于新款 L4 GPU 和 Vertex AI 构建
Marvell宣布推出全球首款TD-HSPA+通信芯片