前言 我们都知道,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+通信芯片