实现一个Spring boot stater

1 自动配置

Spring boot的一大特性就是“自动配置”。在传统的Spring应用开发中,开发者往往需要写很多的XML配置项,包括数据源的配置,组件Bean的配置,数据库事务的配置等等,但如果使用Spring Boot的话,往往不需要做这些配置,只需要添加对应的依赖库即可,即所谓的“开箱即用”,这可以让应用开发者把更多的精力放在业务逻辑上,而不是一大堆的乱七八糟的配置上,对于这点,我想只要有传统SSM项目开发经验以及Spring Boot开发经验的朋友应该能体会到。

那Spring Boot自动配置的原理是什么呢?它又是如何实现的呢?

Spring Boot的自动配置关键是spring-boot-autoconfigure依赖,如果仔细观察,会发现Spring boot starter包含了该依赖,如下图所示(我这里用的2.0.0版本):

pring-boot-autoconfigure包含了很多自动配置项,例如JPA的自动配置,Kafka的自动配置等,如下图所示(仅截了部分):

进一步查看源码,会发现每一个包下都有一个XXXAutoConfiguration(XXX表示就是组件名称,例如JPA,Kafka等),下面是KafkaAutoConfiguration的部分源码:

@Configuration
@ConditionalOnClass(KafkaTemplate.class)
@EnableConfigurationProperties(KafkaProperties.class)
@Import(KafkaAnnotationDrivenConfiguration.class)
public class KafkaAutoConfiguration {

    private final KafkaProperties properties;

    private final RecordMessageConverter messageConverter;
    
    .........
}
  1. @Configuration注解表示这是一个配置类,用过Spring Boot的朋友都应该知道是什么东西,不多做解释了。
  2. @ConditionalOnClass(KafkaTemplate.class),该注解表示当KafkaTemplate.class被加载到JVM中,才会对配置类进行初始化,简单理解就是把他当做if语句来看。
  3. @EnableConfigurationProperties(KafkaProperties.class),加载属性配置类的注解,有这个注解,KafkaProperties才会作用于应用上下文中。
  4. @Import(KafkaAnnotationDrivenConfiguration.class),Spring 的基础注解,不多说了。

如果你用IDEA来查看该源码,且你的项目中没有包含Spring Kafka相关的依赖项,应该会看到KafkaTemplate被标红了,即IDE找不到该类,所以最终KafkaAutoConfiguration不会被初始化,项目中也不会存在KafkaTemplate这个Bean。而如果此时加入Spring Kafka的依赖项,那么KafkaAutoConfiguration就会被初始化,最终应用中会存在KafkaTemplate这个Bean,用户可以直接依赖注入到需要用到的地方而不需要做什么配置,因为KafkaAutoConfiguration这个类里都帮我们做了一些默认的配置。

如果项目确实对KafkaTemplate有什么特殊的配置,仍然可以选择自己手动配置,一般有两种方法:

  1. 自己创建一个KafkaTemplate的Bean,这个应该不难。在配置类中用@Bean注解即可。
  2. 在配置文件application.properties配置一些KafkaTemplate的配置项,这些配置项会覆盖默认配置。

那Spring Boot是如何发现这些东西的呢?也就是说Spring boot是怎么知道这是一个自动配置类的呢?主要有两个,一是@EnableAutoConfiguration注解,而是spring.factories配置文件,现在看看spring.factories配置文件,该文件在类路径中META-INF文件夹下,如下图所示:

文件里有什么配置呢?打开看看就知道了,文件里的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置就是我们今天讨论的关键,可以发现,该键对应着很多值,每个值之间用逗号分隔(\是换行),搜索一下可以发现,存在KafkaAutoConfiguration这个配置项,完整的是org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration。

接下来来看看@EnableAutoConfiguration注解,该注解在@SpringBootApplication中有用到,所以只要加入了@SpringBootApplication注解,也就加入了@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 {};

}

关键是@Import(AutoConfigurationImportSelector.class)注解,该注解导入了AutoConfigurationImportSelector这个类,该类是自动配置的核心,从名字上看可以看出这应该是一个选择器。其部分代码如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
         //加载spring.factories配置文件的配置信息
        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;
    }

现在可以把上面说到的串起来了。

  1. 在应用的配置类上加入@EnableAutoConfiguration注解,该注解会导入AutoConfigurationImportSelector类。
  2. 启动应用的时候,Spring Boot会获取spring.factories里的配置信息。(AutoConfigurationImportSelector。getCandidateConfigurations()方法)
  3. 获取到信息之后,Spring Boot就会尝试去触发XXXAutoConfiguration,是否能触发还取决于具体AutoConfiguration类,例如在KafkaAutoConfiguration中,如果缺少KafkaTemplate类的存在,那么就不会触发KafkaAutoConfiguration的执行,如果触发成功,就会执行KafkaAutoConfiguration里的代码,这时候会发生什么就取决于具体的代码逻辑了。

简单概括就是以上三个流程。当然,其中的细节还有很多,例如Spring Boot是如何去读取spring.factories的配置信息的等等,所以我以上说的都只是大致原理,真正的实现其实异常复杂!!!

这里还要一说的是spring.factories文件并不仅仅包含了自动配置相关的配置信息,还包含其他一些信息,所以不要误以为spring.factories是专门为自动配置服务的。

2 动手实现一个Starter

Spring Boot系列有很多的starter,例如spring-boot-starter-web,spring-boot-starter-data-jpa等等,我们知道加入spring-boot-starter-web依赖,不需要任何配置,就可以直接构建Web应用了,这就是自动配置的威力。实际上,Spring作为一个扩展性比较强的框架,还允许用户自己编写符合需求的starter。

一个完整的starter组件至少需要包含两个部分:

  • 提供自动配置功能的自动配置模块。
  • 提供依赖关系管理功能的组件模块,即封装了组件所有功能,开箱即用。

具体的来说,就是需要一个XXXAutoConfiguration自动配置类以及一个封装好的功能模块,例如JPATemplate等等,用的时候直接依赖注入即可。

这时候可能有读者要问了,在spring-boot-starter-web项目里好像没发现什么XXXAutoConfiguration啊,其实是有的,和Web相关的自动配置类被Spring写到spring-boot-autoconfigure项目里了,即ServletWebServerFactoryAutoConfiguration。

spring-boot-autoconfigure里其实包含了很多XXXAutoConfiguration,例如KafkaAutoConfiguration,HibernateJpaAutoConfiguration等等。基本都是Spring家族的项目,但对于我们自制的starter就需要在项目中写XXAutoConfiguration类了,毕竟Spring boot不能提前预知所有用户的需求不是?好了,不多说了,直接动手实现吧!

首先构建一个Maven项目(其他的构建框架也可以,挑一个熟悉就行),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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>top.yeonon</groupId>
    <artifactId>car-server-starter</artifactId>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>

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

</project>

其实只加入了spring-boot-autoconfigure依赖,因为比较简单嘛,不需要太多东西。

然后创建一个Properties类(其实没有也没什么关系,最好还是有,提供一些灵活性):

@ConfigurationProperties(prefix = "yeonon.car")
public class CarProperties {

    private static final String DEFAULT_NAME = "BWM";
    private static final String DEFAULT_COLOR = "white";
    private static final Integer DEFAULT_AGE = 1;


    private String name = DEFAULT_NAME;

    private String color = DEFAULT_COLOR;

    private Integer age = DEFAULT_AGE;

    //getter 和 setter必须要有,否则属性注入会有问题
}

之后创建我们的功能模块:

public class CarService {

    private Car car;

    public Car getCar() {
        return car;
    }

    public void setCar(Car car) {
        this.car = car;
    }
}

非常简单,就是获取Car对象而已。Car类就不贴出来了,就是一个POJO而已。下面就是核心类了,即CarAutoConfiguration自动配置类,如下所示:

@Configuration
@ConditionalOnClass(CarService.class)
@EnableConfigurationProperties(value = CarProperties.class)
public class CarAutoConfiguration {

    @Autowired
    private CarProperties carProperties;

    @Bean
    @ConditionalOnMissingBean(CarService.class)
    @ConditionalOnProperty(value = "yeonon.car.server.configuration.auto.enabled", matchIfMissing = true)
    public CarService carService() {
        CarService carService = new CarService();
        Car car = new Car();
        car.setName(carProperties.getName());
        car.setColor(carProperties.getColor());
        car.setAge(carProperties.getAge());
        carService.setCar(car);
        return carService;
    }
}

  1. @Configuration注解是要有的,否则无法生效。
  2. @ConditionalOnClass(CarService.class),原则上可有可无,但作为一个健壮的starter,还是有的好,作为一个“防御措施”。
  3. @EnableConfigurationProperties(value = CarProperties.class),要使用属性系统,就需要把属性类加入到应用上下文中。
  4. @Bean就是定义一个Bean了,代码逻辑没什么可说的,无法就是创建对象,然后构建对象并返回而已。关键在于@ConditionalOnMissingBean注解和@ConditionalOnProperty注解。@ConditionalOnMissingBean注解的意思就是如果应用中不存在CarService的Bean,那么就执行下面的方法构建一个Bean,已经存在的话,就不会调用下面的方法了,这意味着用户可以自己创建Bean来覆盖系统默认配置的Bean。@ConditionalOnProperty就是当配置存在的时候,才会执行Bean的构建。

打完收工!别着急!别忘了要配置spring.factories,spring-boot-autoconfigure里的spring.factories我们是没法动的,所以就只能在自己的项目中动刀子了。

在resource文件夹(其实就是类路径classpath)下创建一个META-INF文件夹,为什么要创建这玩意?问Spring去吧!然后创建spring.factories文件(不要打错一个字!)。在里面写入如下内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
top.yeonon.stater.test.CarAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration这个键名是不是很熟悉,没错,就是spring-boot-autoconfigure中的spring.factories里的键名,在这里我们只需要把我们自己的CarAutoConfiguration全限定类名加入就行了。

这才算完事,然后测试一下?先打包,打包过程我就不说了,然后新建一个项目,把刚刚打包好的car-starter作为依赖加入进去,如下所示:

<dependency>
    <groupId>top.yeonon</groupId>
    <artifactId>car-server-starter</artifactId>
    <version>1.0</version>
</dependency>

为了方便测试,也加入spring-boot-starter-web吧,然后写一个Controller,如下所示:

@RestController
@RequestMapping("/hello")
public class HelloController {

    @Autowired
    private CarService carService;

    @GetMapping("car")
    public Car getMyCar() {
        return carService.getCar();
    }
}

运行一下,访问该路径,大致可以得到如下输出:

这里的值还是我们之前的默认值(返回去看看CarProperties类),Spring Boot的属性系统也是相当厉害,现在来试试修改配置项,如下所示:

## 注意前缀是yeonon.car,也是在CarProperties类里配置好的
yeonon.car.name=MSLD
yeonon.car.age=2
yeonon.car.color=black

然后重启项目,再次访问,得到的结果应该是这样:

是不是很神奇?

3 小结

Spring Boot的自动配置非常强大,免去了很多配置文件,用得好的话会觉得既方便又灵活,用不好的话可能会发生一些配置冲突的问题(不过其实都是能解决的)。自己编写stater也是可行的,不过要确定确实需要自定义的starter,否则最好还是不要给自己挖坑哈,例如我们的Car stater这个例子,实际上如果仅仅是为了实现这个功能,完全不需要专门写一个starter。

最后,Spring水很深,道阻且长!

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

推荐阅读更多精彩内容