SpringBoot核心特性——手写一个自己的starter

前言

用SpringBoot开发会发现集成功能是很方便的,比如需要Web相关功能,就只需要引入spring-boot-starter-web、集成单元测试则引入spring-boot-starter-test...这样一个个starter极大简化了开发成本,增强了开发效率。本文则来讲解一下如何封装一个自己的starter。

创建starter

maven依赖

新建一个thinking-starter工程,pom.xml依赖如下:

4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.14
geek.springboot.starter
thinking-starter
jar
8
8
UTF-8
org.projectlombok
lombok
1.16.18
org.springframework.boot
spring-boot-autoconfigure

配置映射

创建一个ReadingConfig,映射application.yml或application.properties中的相关配置:

package geek.springboot.starter.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 读取application.properties中reading相关的配置
*/
@Data
@ConfigurationProperties(prefix = "reading")
public class ReadingConfig {
// 类型
private String type;
}

在resources目录下新建application.properties,默认配置type为txt,代码如下:

reading.type=txt

默认服务

新建一个Reading接口:

package geek.springboot.starter.service;
public interface Reading {
void reading();
}

新建一个Reading接口实现类:

package geek.springboot.starter.service.impl;
import geek.springboot.starter.config.ReadingConfig;
import geek.springboot.starter.service.Reading;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@AllArgsConstructor
public class ReadingService implements Reading {
// reading相关配置类
private ReadingConfig readingConfig;
@Override
public void reading() {
log.info("start reading... type is {}", this.readingConfig.getType());
}
}

自动装配

新建一个ReadingConfiguration,这个类是自动装配的核心:

package geek.springboot.starter.configuration;
import geek.springboot.starter.config.ReadingConfig;
import geek.springboot.starter.service.Reading;
import geek.springboot.starter.service.impl.ReadingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(ReadingConfig.class)
@ConditionalOnProperty(name = "reading.enable", havingValue = "true") // 当存在reading.enable属性,且其值为true时,才初始化该Bean
public class ReadingConfiguration {
@Autowired
private ReadingConfig readingConfig;
// 若当前IOC容器中没有Reading接口实现时,提供一个默认的Reading实现
@Bean
@ConditionalOnMissingBean(Reading.class)
public Reading readingService() {
return new ReadingService(this.readingConfig);
}
}

如果对@EnableConfigurationProperties不熟悉,可以看这里《SpringBoot核心特性——万字拆解外部配置》

如果对@ConditionalOnProperty不熟悉,可以看这里《SpringBoot核心特性——你所要知道的自动装配》

自动装配注册

新建META-INF/spring.factories,提醒SpringBoot在启动时,别忘了这还有个自动装配的Configuration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=geek.springboot.starter.configuration.ReadingConfiguration

maven打包

到这里其实简易版starter就差不多了,整体思路就是只要application.yml或application.properties配置了reading.enable=true,那么我就会往Spring提供一个Reading实现,业务方可以在业务逻辑中注入这个Reading实现去做一些事情。做到这里,剩下来的就是进行Maven打包,让别的项目可以依赖该starter。

IDEA点击Maven菜单,然后点击Lifecycle下的install,进行打包。

image.png

引用starter

切换到业务工程,pom.xml依赖添加我们的starter

geek.springboot.starter
thinking-starter
1.0-SNAPSHOT

新建一个ReadingHandler,代码如下:

package geek.springboot.application.service;
import geek.springboot.starter.service.Reading;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@Slf4j
@Service
public class ReadingHandler {
/**
* 因为若application.yml中没有reading相关配置,starter中自动装配不会生效,默认的Reading实现不会被初始化,会导致在SpringBoot启动时抛异常。
* 所以这里required设置为false,表示ReadingHandler初始化时,不强制要求一定要注入Reading实现
*/
@Autowired(required = false)
private Reading reading;
@PostConstruct
public void init() {
// 若当前Reading实现不为空,调用其reading()方法,否则打印reading is null...
if (this.reading != null) {
this.reading.reading();
} else {
log.error("reading is null...");
}
}
}

启动SpringApplication,输出如下:

image.png

接下来往application.yml中添加reading配置,让自动装配生效

reading:
enable: true

重启SpringApplication,可以看到自动装配生效了,控制台输出如下:

image.png

重写starter默认配置

之前创建starter时,reading.type默认配置为reading.type=txt,我们想重写默认配置也是很简单的,只需要在当前项目application.yml中稍作添加

reading:
enable: true
type: json # 重写starter默认配置

重启SpringApplication,输出如下,可以看到默认配置已经成功重写

2023-07-30 15:12:01.014  INFO 96828 --- [main] g.s.starter.service.impl.ReadingService  : start reading... type is json

重写starter默认实现

如果觉得starter默认的Reading实现不够好,那么我们也可以自定义Reading实现。因为starter构造Reading实现那里加上了@ConditionalOnMissingBean(Reading.class),所以我们只需要在我们当前工程自行实现Reading接口,并将其注册到SpringIOC中,则starter中默认Reading实现将不会被初始化。

新建一个MyReadingService,代码如下:

package geek.springboot.application.service;
import geek.springboot.starter.config.ReadingConfig;
import geek.springboot.starter.service.Reading;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class MyReadingService implements Reading {
@Autowired
private ReadingConfig readingConfig;
@Override
public void reading() {
log.info("my reading service start reading... type is {}", this.readingConfig.getType());
}
}

重启SpringApplication,控制台输出如下,可以看到ReadingHandler中注入的不是默认的ReadingService了,而是我们创建的MyReadingService

2023-07-30 15:27:57.631  INFO 92232 --- [main] g.s.a.service.MyReadingService           : my reading service start reading... type is json

实现一个自己的@EnableXXX

在application.yml配置reading.enable=true这样的方式让自动装配生效,其实不够优雅。那么我们也可以像别的starter那样提供@EnableXXX(@EnableScheduling、@EnableAsync、@EnableCaching...)注解,然后在SpringApplication启动类加上@EnableXXX,让我们的自动装配生效。

回到starter,创建一个EnableReading注解,代码如下:

package geek.springboot.starter.annotation;
import geek.springboot.starter.configuration.ReadingSelector;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// @Import作用是往SpringIOC中导入一个类,这里即导入ReadingSelector
@Import(ReadingSelector.class)
public @interface EnableReading {
}

创建ReadingSelector,用以取代之前的ReadingConfiguration

package geek.springboot.starter.configuration;
import geek.springboot.starter.config.ReadingConfig;
import geek.springboot.starter.service.Reading;
import geek.springboot.starter.service.impl.ReadingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(ReadingConfig.class)
public class ReadingSelector {
@Autowired
private ReadingConfig readingConfig;
// 当SpringIOC容器中不存在Reading实现时,才往Spring中初始化ReadingService作为Reading接口的实现
@Bean
@ConditionalOnMissingBean(Reading.class)
public Reading readingService() {
return new ReadingService(this.readingConfig);
}
}

重新将starter打包到本地Maven仓库,并记得在业务工程中刷新一下maven依赖。

接下来验证我们的@EnableReading注解

回到业务工程,为了印证@EnableReading注解是否可行,稍作一下调整

application.yml删除reading.enable: true,让ReadingConfiguration自动装配无效

reading:
type: json # 重写starter默认配置

ReadingHandler稍作调整,表示ReadingHandler初始化时,Spring一定要注入一个Reading实现

@Autowired // 之前是@Autowired(required = false)
private Reading reading;

以及记得把MyReadingService中的@Service注解删除,以防我们的自定义Reading实现被Spring加载

最后给SpringApplication启动类加上@EnableReading

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

启动SpringApplication,可以看到通过标记@EnableReading来让自动装配生效也是成功的

2023-07-30 15:58:24.308  INFO 87024 --- [main] g.s.starter.service.impl.ReadingService  : start reading... type is json

结尾

本文章源自《Learn SpringBoot》专栏,感兴趣的话还请关注点赞收藏.

上一篇文章:《# SpringBoot核心特性——你所要知道的自动装配》