Springboot starter详解

springboot starter封装

什么是springboot starter机制?

能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要在maven中引入starter依赖,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置。

starter让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。SpringBoot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。
所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

为什么需要自定义starter?

在我们的日常开发工作中,经常会有一些独立于业务之外的配置模块,我们经常将其放到一个特定的包下,然后如果另一个工程需要复用这块功能的时候,需要将代码硬拷贝到另一个工程,重新集成一遍,麻烦至极。 如果我们将这些可独立于业务代码之外的功配置模块封装成一个个starter,复用的时候只需要将其在pom中引用依赖即可,SpringBoot为我们完成自动装配,简直不要太爽。

什么时候需要自定义starter?

在我们的日常开发工作中,可能会需要开发一个通用模块,以供其它工程复用。SpringBoot就为我们提供这样的功能机制,我们可以把我们的通用模块封装成一个个starter,这样其它工程复用的时候只需要在pom中引用依赖即可,由SpringBoot为我们完成自动装配。

举一些常见场景:

通用模块-短信发送模块
基于AOP技术实现日志切面
分布式雪花ID,Long-->string,解决精度问题 jackson2/fastjson
微服务项目的数据库连接池配置
微服务项目的每个模块都要访问redis数据库,每个模块都要配置redisTemplate

自动加载核心注解说明

image-20220803154634046.png

自定义starter的开发流程

这里以封装 liteflow-spring-boot-starter 为例

(1)创建Starter项目

命名规范

SpringBoot官方命名方式
格式:spring-boot-starter-{模块名}
举例:spring-boot-starter-web

自定义命名方式
格式:{模块名}-spring-boot-starter
举例:mystarter-spring-boot-starter

引入必要的依赖

该依赖作用是在使用IDEA编写配置文件有代码提示, 作用在编译阶段

<!--表示两个项目之间依赖不传递;不设置optional或者optional是false,表示传递依赖-->
<!--例如:project1依赖a.jar(optional=true),project2依赖project1,则project2不依赖a.jar-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional> <!-- 避免传递依赖 -->
</dependency>       

完整pom.xml

<?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.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.unionman</groupId>
    <artifactId>liteflow-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>liteflow-spring-boot-starter</name>
    <description>liteflow-spring-boot-starter</description>
    <properties>
        <java.version>1.8</java.version>
        <swagger.fox.version>3.0.0</swagger.fox.version>
        <knife4j.version>2.0.8</knife4j.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--非必需,该依赖作用是在使用IDEA编写配置文件有代码提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-test</artifactId>-->
<!--            <scope>test</scope>-->
<!--        </dependency>-->

        <dependency>
            <groupId>com.yomahub</groupId>
            <artifactId>liteflow-spring-boot-starter</artifactId>
            <version>2.8.3</version>
        </dependency>
        <dependency>
            <groupId>com.yomahub</groupId>
            <artifactId>liteflow-script-groovy</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>com.baomidou</groupId>-->
<!--            <artifactId>mybatis-plus-boot-starter-test</artifactId>-->
<!--            <version>3.5.1</version>-->
<!--        </dependency>-->


        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${swagger.fox.version}</version>
        </dependency>

        <!--
            // swagger2.0版本后,swagger ui访问地址 ip:port/swagger-ui.html
        // swagger3.0版本后,swagger ui访问地址 ip:port/swagger-ui/index.html (当前使用)
        // 兼容swagger3.0, 增强型swagger knife4j ui界面: ip:port/doc.html (当前使用)

        -->
        <!-- knife4j提供的微服务starter -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>
        <!-- knife4j  ui -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-micro-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy</artifactId>
        </dependency>
    </dependencies>


    <!-- starter 我们没有main入口,需要去除pom文件中maven打包插件spring-boot-maven-plugin,starter无需打成jar包 -->
    <build>
        <plugins>
        </plugins>
    </build>

</project>
(2)编写相关属性类
package com.unionman.liteflow.config;


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


@ConfigurationProperties(prefix = "app.service")
@Data
public class LiteServiceConfig {

    private String serviceUid;
}

@ConfigurationProperties注解基本用法

以定义一个配置信息类,和配置文件进行映射

  • 前缀定义了哪些外部属性将绑定到类的字段上
  • 根据 Spring Boot 宽松的绑定规则,类的属性名称必须与外部属性的名称匹配
  • 我们可以简单地用一个值初始化一个字段来定义一个默认值
  • 类本身可以是包私有的
  • 类的字段必须有公共 setter 方法

注意:需要在配置类上加上 @EnableConfigurationProperties(LiteServiceConfig.class)

(3)编写Starter项目的业务功能
(4)编写自动配置类 xxxAutoConfiguration

命名规范: XXXAutoConfiguration,如 LiteFlowAutoConfiguration

package com.unionman.liteflow.config;


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration // 表示该类是配置类
@ComponentScan("com.unionman.liteflow") // 由于这里没有启动类,注入组件不能通过@SpringBootApplication,而需通过注解扫描
//@MapperScan("com.unionman.liteflow.mapper") // 不使用@Mapper注入mapper接口,则使用包扫描
@EnableConfigurationProperties(LiteServiceConfig.class) // 激活自动配置(指定文件中的配置)
public class LiteFlowAutoConfiguration {

//
//    @Bean(initMethod = "init", destroyMethod = "beforeDestory")
//    public InitLiteFlowBean initLiteFlowBean(){
//        return new InitLiteFlowBean();
//    }


}

@Configuration: 定义一个配置类

@EnableConfigurationProperties:作用是使@ConfigurationProperties注解生效。

如 @EnableConfigurationProperties(LiteServiceConfig.class) 让上面的LiteServiceConfig中的

@ConfigurationProperties生效

(5)编写spring.factories文件加载自动配置类

为什么需要spring.factories?

Spring Boot会默认扫描跟启动类平级的包,如果我们的Starter跟启动类不在同一个主包下,需要通过配置spring.factories文件来配置生效,SpringBoot默认加载各个jar包下classpath路径的spring.factories文件,配置的key为

org.springframework.boot.autoconfigure.EnableAutoConfiguration

image-20220803120012943.png

在 resources 目录下新建 META-INF文件夹,再创建 spring.factories 文件,加入以下内容

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.liteflowstarter.config.LiteFlowAutoConfiguration,\

注意:其中填的是配置类的全限定名,多个之间逗号分割,还可以 \ 进行转义即相当于去掉后面换行和空格符号

(6)maven去掉打包插件

starter 我们没有main入口,需要去除pom文件中maven打包插件spring-boot-maven-plugin,starter无需打成jar包

 <build>
        <plugins>
            <plugin>
<!--                <groupId>org.springframework.boot</groupId>-->
<!--                <artifactId>spring-boot-maven-plugin</artifactId>-->
<!--                <version>2.3.1.RELEASE</version>-->
            </plugin>
        </plugins>
    </build>
(7)在其他项目中引用
<dependency>
    <groupId>com.unionman</groupId>
    <artifactId>liteflow-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

配置文件application.yml 添加starter所需的配置

liteflow:
  ruleSource: liteflow/*.el.xml

app:
  service:
    # umliteflow-spring-boot-starter 识别 不同服务的唯一标识
    serviceUid: demo
(8)在starter中使用@Component等注解无法注入问题

由于这里没有启动类,注入组件不能通过@SpringBootApplication,而需通过包路径注解扫描 @ComponentScan指定starter最外层包 ,如 @ComponentScan("com.unionman.liteflow"),即扫描 该包目录下的@Component,@Repository,@Service,@Controller

@Configuration 使用详解

Full全模式和Lite轻量级模式

@Configuration参数proxyBeanMethods:

Full 全模式(默认):@Configuration(proxyBeanMethods = true)

同一配置类下,当直接调用@Bean修饰的方法注入的对象,则调用该方法会被代理,从ioc容器中取bean实列,所以实例是一样的。即单实例对象在该模式下SpringBoot每次启动都会判断检查容器中是否存在该组件

Lite 轻量级模式:@Configuration(proxyBeanMethods = false)

同一配置类下,当直接调用@Bean修饰的方法注入的对象,则调用该方法不会被代理,相当于直接调用一个普通方法,会有构造方法,但是没有bean的生命周期,返回的是不同的实例

注:proxyBeanMethods 是为了让使用@Bean注解的方法被代理。而不是@Bean的单例多例的设置参数。

测试例子这里不展示,可以下载我的代码查看

@Configuration(proxyBeanMethods = false)
public class AppConfig {
    
    //放一份myBean到ioc容器
    @Bean
    public Mybean myBean() {
        return new Mybean();
    }
 
    //放一份yourBean到ioc容器
    @Bean
    public YourBean yourBean() {
        System.out.println("==========");
        //注意:@Configuration(proxyBeanMethods = false):myBean()方法不代理,直接调用
        //注意:@Configuration(proxyBeanMethods = true):myBean()方法代理,从ioc容器拿
        return new YourBean(myBean());
    }
}
什么时候用Full全模式,什么时候用Lite轻量级模式?

当在你的同一个Configuration配置类中,注入到容器中的bean实例之间有依赖关系时,建议使用Full全模式

当在你的同一个Configuration配置类中,注入到容器中的bean实例之间没有依赖关系时,建议使用Lite轻量级模式,以提高springboot的启动速度和性能

@ComponentScan 使用详解

原文链接:https://blog.csdn.net/u012326462/article/details/82765485

作用就是根据定义的扫描路径,把符合扫描规则的类装配spring容器中。

如果@ComponentScan配置了包,则扫描@ComponentScan配置的包

如果@ComponentScan没有配置包,则扫描其注解的类的包(如 @SpringbootApplication内部就有@ComponentScan,则扫描启动类所在的包,这就是为什么只要在该包下基本都能被扫到的原因)


image-20220803144331854.png
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.annotation.AliasFor;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

    String resourcePattern() default "**/*.class";

    boolean useDefaultFilters() default true;

    ComponentScan.Filter[] includeFilters() default {};

    ComponentScan.Filter[] excludeFilters() default {};

    boolean lazyInit() default false;

    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Filter {
        FilterType type() default FilterType.ANNOTATION;

        @AliasFor("classes")
        Class<?>[] value() default {};

        @AliasFor("value")
        Class<?>[] classes() default {};

        String[] pattern() default {};
    }
}

basePackages与value: 用于指定包的路径,进行扫描

basePackageClasses: 用于指定某个类所在的包的路径进行扫描

nameGenerator: bean的名称的生成器

useDefaultFilters: 是否开启对@Component,@Repository,@Service,@Controller的类进行检测,默认true

includeFilters: 包含的过滤条件

        FilterType.ANNOTATION:按照注解过滤

        FilterType.ASSIGNABLE_TYPE:按照给定的类型

        FilterType.ASPECTJ:使用ASPECTJ表达式

        FilterType.REGEX:正则

        FilterType.CUSTOM:自定义规则

excludeFilters: 排除的过滤条件,用法和includeFilters一样

DEMO项目目录


image-20220803142954185.png

应用默认的过滤器,扫描service包:

@Configuration
@ComponentScan(value = "com.xhx.spring.service",
        useDefaultFilters = true
)
public class MyConfig {
}

HelloController所在的包的类也被扫描了进去

@Configuration
@ComponentScan(value = "com.xhx.spring.service",
        useDefaultFilters = true,
        basePackageClasses = HelloController.class
)
public class MyConfig {
}

把默认的过滤器关掉,扫描带Controller注解的。

@Configuration
@ComponentScan(value = "com.xhx.spring",
        useDefaultFilters = false,
        includeFilters = {
            @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
        }
)
public class MyConfig {
}

按照类的类型扫描,虽然HelloController没有加注解,但是被注入到了spring容器中

@Configuration
@ComponentScan(value = "com.xhx.spring",
        useDefaultFilters = false,
        includeFilters = {
            @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,classes = {HelloController.class})
        }
)
public class MyConfig {
}

自定义扫描过滤器

package com.xhx.spring.componentscan.config;
 
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
 
import java.io.IOException;
 
/**
 * xuhaixing
 * 2018/9/18 23:07
 **/
public class MyTypeFilter implements TypeFilter {
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        String className = metadataReader.getClassMetadata().getClassName();
        // 满足类名包含Controller的类 会被扫描
        if(className.contains("Controller")){
            return true;
        }
        return false;
    }
}

注意: 要设置useDefaultFilters = false(系统默认为true,需要手动设置) includeFilters包含过滤规则才会生效。

@Configuration
@ComponentScan(value = "com.xhx.spring",
        useDefaultFilters = false,
        includeFilters = {
            @ComponentScan.Filter(type = FilterType.CUSTOM,classes = {MyTypeFilter.class})
        }
)
public class MyConfig {
}

输出spring容器中的bean的测试类:只过滤输出了名字中含有hello的类。

package com.xhx.spring.componentscan;
 
import com.xhx.spring.componentscan.config.MyConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
 
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class ComponentScanApplicationTests {
 
    @Test
    public void testLoads() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        List<String> hello = Arrays.stream(context.getBeanDefinitionNames()).collect(Collectors.toList());
        hello.stream().filter(name->name.contains("hello")).peek(System.out::println).count();
    }
 
}

[demo]SpringBoot自定义多数据源starter组件

https://blog.csdn.net/weixin_45840947/article/details/123892165#t0

【有空看看】mybatis-spring-boot-starter

mybatis自动配置原理

https://jishuin.proginn.com/p/763bfbd5ad27

参考资料

  1. Springboot官方文档
  2. 一文搞懂🔥SpringBoot自动配置原理
  3. SpringBoot starter的理解与使用
  4. 还在curd吗?封装属于自己的Spring-Boot-Starter
  5. SpringBoot自定义多数据源starter组件
  6. MyBaits自动配置原理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容