从SpringBoot源码到自己封装一个Starter

这篇博客主要讲述一下springboot怎么给我们简化了大量的配置,然后跟着源码自己封装一个Starter,首先我们需要从两个地方来说,第一就是springboot的起步依赖,第二就是springboot自动装配;

起步依赖

我们在创建一个springboot工程时需要引入spring-boot-starter-web这个依赖;

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

这个依赖我们点进去可以看到其实这个起步依赖集成了常用的web依赖,例如spring-web,spring-webmvc

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-json</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.0.16.Final</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>5.1.6.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>5.1.6.RELEASE</version>
  <scope>compile</scope>
</dependency>

Spring Boot的起步依赖说白了就是对常用的依赖进行再一次封装,方便我们引入,简化了 pom.xml 配置,但是更重要的是将依赖的管理交给了 Spring Boot,我们无需关注不同的依赖的不同版本是否存在冲突的问题,Spring Boot 都帮我们考虑好了,我们拿来用即可!

在使用 Spring Boot 的起步依赖之前,我们需要在pom.xml中添加配置:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

即让pom.xml继承 Spring Boot 的pom.xml,而 Spring Boot 的pom.xml里面定义了常用的框架的依赖以及相应的版本号,我们无需担心版本冲突问题;

自动装配

首先我们知道springboot启动需要一个启动引导类,这个类除了是应用的入口之外,还发挥着配置的 Spring Boot 的重要作用。

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

可以看到@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 {};
    
    //........
}

这里的@SpringBootConfiguration@ComponentScan注解,前者其实就是@Configuration注解,就是起到声明这个类为配置类的作用,而后者起到开启自动扫描组件的作用。

我们重点分析一下@EnableAutoConfiguration这个注解,这个注解的作用就是开启Spring Boot 的自动装配功能,我们点进行看下:

@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 {};
}

我们重点分析一下@Import({AutoConfigurationImportSelector.class})这个注解,我们知道@Import的作用是将组件添加到 Spring 容器中,而在这里即是将AutoConfigurationImportSelector这个组件添加到 Spring 容器中。也就是将AutoConfigurationImportSelector声明成一个Bean;

我们重点分析一下@Import注解中的AutoConfigurationImportSelector类;

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
            AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        }
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        configurations = removeDuplicates(configurations);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }


protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

getAutoConfigurationEntry方法中扫描ClassPath下的所有jar包的spring.factories文件,将spring.factories文件keyEnableAutoConfiguration的所有值取出,然后这些值其实是类的全限定名,也就是自动配置类的全限定名,然后 Spring Boot 通过这些全限定名进行类加载(反射),将这些自动配置类添加到 Spring 容器中。

我们找到一个名为spring-boot-autoconfigure-2.1.4.RELEASE.jar的 jar 包,打开它的spring.factories文件,发现这个文件有keyEnableAutoConfiguration的键值对

image

也就是这个jar包有自动配置类,可以发现这些自动配置配都是以xxxAutoConfiguration的命名规则来取名的,这些自动配置类包含我了们常用的框架的自动配置类,比如aopmongoredisweb等等,基本能满足我们日常开发的需求。例如我们程序中需要用到aop,直接引入相应的依赖即可!

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

我们取一个较为简单的配置类进行分析,看看是怎么发挥它的配置作用的;我们以HttpEncodingAutoConfiguration为例;部分代码如下:

//声明这个类为配置类
@Configuration 
//开启ConfigurationProperties功能,同时将配置文件和HttpProperties.class绑定起来
@EnableConfigurationProperties({HttpProperties.class})
//只有在web应用下自动配置类才生效
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
//只有存在CharacterEncodingFilter.class情况下 自动配置类才生效
@ConditionalOnClass({CharacterEncodingFilter.class})
//判断配置文件是否存在某个配置spring.http.encoding,如果存在其值为enabled才生效,如果不存在这个配置类也生效。
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    private final Encoding properties;

    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }

    //将字符编码过滤器组件添加到 Spring 容器中
    @Bean
    //仅在该注解规定的类不存在于 spring容器中时,使用该注解的config或者bean声明才会被实例化到容器中
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }
    
    @Bean
public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
    return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
}

Configuration:这个注解声明了这个类为配置类(和我们平时写的配置类一样,同样是在类上加这个注解)。

EnableConfigurationProperties:开启ConfigurationProperties功能,也就是将配置文件和HttpProperties.class这个类绑定起来,将配置文件的相应的值和HttpProperties.class的变量关联起来,可以点击HttpProperties.class进去看看,

@ConfigurationProperties(
    prefix = "spring.http"
)

public static final Charset DEFAULT_CHARSET;
private Charset charset;
private Boolean force;
private Boolean forceRequest;
private Boolean forceResponse;
private Map<Locale, Charset> mapping;

通过ConfigurationProperties指定前缀,将配置文件application.properties前缀为spring.http的值和HttpProperties.class的变量关联起来,通过类的变量可以发现,我们可以设置的属性是charsetforceforceRequestforceResponsemapping。另外ConfigurationProperties注解将HttpProperties类注入到Spring容器成为一个bean对象,因为一般来说,像springboot默认的包扫描路径为xxxxxxApplication.java所在包以及其所有子包,但是一些第三方的jar中的bean很明显不能被扫描到,此时该注解就派上了用场,当然,你可能会说,我使用@ComponentScan不就行了,这两个注解的区别是:@ComponentScan前提是你要的bean已经存在bean容器中了,而@EnableConfigurationProperties是要让容器自动去发现你要类并注册成为bean。也就是我们除了使用 Spring Boot 默认提供的配置信息之外,我们还可以通过配置文件指定配置信息。

  • ConditionalOnWebApplication:这个注解的作用是自动配置类在 Web 应用中才生效。
  • ConditionalOnClass:只有在存在CharacterEncodingFilter这个类的情况下自动配置类才会生效。
  • ConditionalOnProperty:判断配置文件是否存在某个配置 spring.http.encoding ,如果存在其值为 enabled 才生效,如果不存在这个配置类也生效。
  • @ConditionalOnMissingBean:仅在该注解规定的类不存在于 spring容器中时,使用该注解的config或者bean声明才会被实例化到容器中

可以发现后面几个注解都是ConditionalXXXX的命名规则,这些注解是 Spring 制定的条件注解,只有在符合条件的情况下自动配置类才会生效。

接下来的characterEncodingFilter方法,创建一个CharacterEncodingFilter的对象,也就是字符编码过滤器,同时设置相关属性,然后将对象返回,通过@Bean注解,将返回的对象添加到 Spring 容器中。这样字符编码过滤器组件配置好了,而平时的话,我们需要在 web.xml 进行如下配置:

 <filter>
       <filter-name>springUtf8Encoding</filter-name>
       <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
       <init-param>
           <param-name>encoding</param-name>
           <param-value>utf-8</param-value>
       </init-param>
       <init-param>
           <param-name>forceEncoding</param-name>
           <param-value>true</param-value>
       </init-param> 
    </filter>
    <filter-mapping>
       <filter-name>springUtf8Encoding</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

到这里原理我们已经分析完了,下面我们动手自己封装一个类似上面的spring-boot-starter-aop

封装一个Starter

1,SpringBoot Starter开发规范

  • 1、命名使用spring-boot-starter-xxx,其中xxx是我们具体的包名称,如果集成Spring Cloud则使用spring-cloud-starter-xxx
  • 2、通常需要准备两个jar文件,其中一个不包含任何代码,只用于负责引入相关以来的jar文件,另外一个则包含核心的代码

nacos与Spring Cloud集成的starter如下图:

更多Starter制作规范,我们可以查看官网文档

2,Starter开发步骤

我们创建一个名字为okay-spring-boot-starter的工程,并引入相关依赖:

    <dependencies>
        <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>
    <dependencyManagement>
        <!-- 我们是基于Springboot的应用 -->
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

因为我们需要用到Springboot提供的相关注解,并且使用springboot提供的自动配置功能,我们不得不引入spring-boot-autoconfigurespring-boot-dependencies两个依赖。

3,创建自动配置类

一般来说,我们可能想在springboot启动的时候就预先注入自己的一些bean,此时,我们要新建自己的自动配置类,一般采用xxxxAutoConfiguration。这里就类似于上面的HttpEncodingAutoConfiguration,下面我们模仿HttpEncodingAutoConfiguration新建一个OkayStarterAutoConfiguration配置类;

@Configuration
@EnableConfigurationProperties(OkayProperties.class)
@ConditionalOnClass(Okay.class)
@ConditionalOnWebApplication
public class OkayStarterAutoConfiguration {

    
    @Bean
    @ConditionalOnMissingBean
    /**
     * 当存在okay.config.enable=true的配置时,这个Okay bean才生效
     */
    @ConditionalOnProperty(prefix = "okay.config", name = "enable", havingValue = "true")
    public Okay defaultStudent(OkayProperties okayProperties) {
        Okay okay = new Okay();
        okay.setPlatform(okayProperties.getPlatform());
        okay.setChannel(okayProperties.getChannel());
        okay.setEnable(okayProperties.getEnable());
        return okay;
    }
}

这里每个注解的含义上面已经解释过了,这里就不做过多的解释;

新建一个OkayProperties,声明该starter的使用者可以配置哪些配置项。

@ConfigurationProperties(prefix = "okay.config")
public class OkayProperties {

    private String platform;

    private String channel;

    private Boolean enable;

    public String getPlatform() {
        return platform;
    }

    public void setPlatform(String platform) {
        this.platform = platform;
    }

    public String getChannel() {
        return channel;
    }

    public void setChannel(String channel) {
        this.channel = channel;
    }

    public Boolean getEnable() {
        return enable;
    }

    public void setEnable(Boolean enable) {
        this.enable = enable;
    }

    @Override
    public String toString() {
        return "OkayProperties{" +
                "platform='" + platform + '\'' +
                ", channel='" + channel + '\'' +
                ", enable=" + enable +
                '}';
    }
}

resources目录下新建一个META-INF目录并且创建一个spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  cn.haoxiaoyong.okay.starter.config.OkayStarterAutoConfiguration

到这里是不是和上面我们讲解的源码基本一致!

使用我们自己的Starter

新创建一个springboot工程,引入我们自己maven依赖:

    <dependency>
        <groupId>cn.haoxiaoyong.okay</groupId>
        <artifactId>okay-spring-boot-starter</artifactId>
        <version>0.0.2-SNAPSHO</version>
    </dependency>

并在配置文件appliaction.yml中配置

image

你看多智能还会自动提示!

okay:
  config:
    platform: pdd
    channel: ws
    enable: true
@RestController
@Slf4j
public class OkController {

    @Autowired
    Okay okay;

    @RequestMapping("okay")
    public String testOkay() {
        log.info(okay.getChannel() + "  " + okay.getPlatform() + "  " + okay.getEnable());

        return okay.getChannel() + "  " + okay.getPlatform() + "  " + okay.getEnable();
    }
}

浏览器输入:localhost:8082/okay,控制台打印:

image

这个例子只是展示一下逻辑效果,这篇使用自定义Starter 并制作一个简单的图床

示例地址:https://github.com/haoxiaoyong1014/springboot-examples/tree/master/okay-spring-boot-starter

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

推荐阅读更多精彩内容