创建Springboot应用
命名规范
Spring官方建议命名规则为:
官方的Starter命名为:spring-boot-starter-XXXXXX
非官方的Starter命名为:XXXXXX-spring-boot-starter
项目结构
Spring官方建议一个Starter应包含两个模块,其中一个用于AutoConfiguration,另一个用于实现业务。为了方便项目搭建,也可以直接使用一个模块。
POM依赖
SpringbootStarter与普通的Springboot项目不同,其依赖于其他的Springboot项目使用,需要结合自动装配特性。以下依赖为必须项:
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId></groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
MAVEN打包配置修改
SpringbootStarter不需要启动类!如果使用了启动类的话,将会导致调用者的SpringIOC容器无法管理该Starter。
启动类修改
@EnableAutoConfiguration
@ComponentScan({""})
public class MySpringBootStarterApplication {
}
因为删除了启动类和
@SpringbootApplication
注解,需要手动增加@EnableAutoConfiguration
和@ComponentScan
。其中的ComponentScan还有一个作用,用于指定Spring需要扫描的包。
打开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 {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "nameGenerator"
)
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
该注解是一个复合注解,其中默认ComponentScan
的范围为当前包。也就是说当该Starter被父级应用所依赖时,按本项目举例,该starter的包名为:“”;父级项目的包名为:“”,两者包名不一致,会导致Starter的包不能被扫描到,所以需要添加@ComponentScan({""})
用于指定当前包也需要被扫描。
单元测试类修改
由于单独的Starter并不是一个完整的应用,大多数时候都是作为一个实际应用的一部分存在,所以需要创建能够独立运行的Test。
首先需要保证引入以下依赖:
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
spring-boot-starter-test为官方提供的测试包,包含Junit的集成
在单元测试类中,添加以下注解:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
XXXXAutoConfiguration.class,
YYYYAutoConfiguration.class,
ZZZZAutoConfiguration.class
})
@TestPropertySource("classpath:")
RunWith 标识为Spring提供的JUnit运行环境
SpringBootTest(classes={…}) 不同于完整的Springboot项目,单独的starter没有所以需要指定环境需要加载的Configuration文件, 此处的classes的值是数组,根据测试的覆盖范围需要把涉及到的Configuration文件写入
TestPropertySource 指示测试时读取resource/作为配置文件,因为作为一个Starter,运行时读取依赖它的应用的配置文件,所以测试中需要指定一个配置文件作为数据来源
其中SpringBootTest所指定的AutoConfiguration类为自定义的自动装配类,在下文进行说明;剩下的@Test、@Before这些使用和平常一样,不再赘述。
开始编写代码
创建一个Properties类,读取配置
与普通的Springboot项目创建方式一致,直接贴代码:
/**
* @author Nicemorning
* @date Create in 22:36 2020/7/12 0012
*/
@Data
@ConfigurationProperties("")
public class ChuanglanExpressProperties {
/**
* 快递信息查询接口APP ID
*/
public String appId;
/**
* 快递信息查询接口APP KEY
*/
public String appKey;
/**
* 接口地址
*/
public String url = "https://api./open/kdwl/kdcx";
}
创建一个业务接口
/**
* 快递物流信息查询接口
*
* @author Nicemorning
* @date Create in 23:17 2020/7/12 0012
*/
public interface IChuanglanExpress {
/**
* 通过快递单号查询快递物流信息,自动识别快递公司。顺丰除外
*
* @param tradeNo 快递单号
* @return 快递物流信息
*/
default ExpressResponse queryExpressInfo(String tradeNo) {
return null;
}
/**
* 通过快递单号和指定快递公司查询快递物流信息。顺丰除外
*
* @param tradeNo 快递单号
* @param company 指定快递公司,使用快递公司字母简称。
* 如:圆通:yuantong;高铁速递:gtsd;中通快递:zhongtong;申通快递:shentong;百世快递(原汇通):huitong;韵达快递:yunda;顺丰速运:shunfeng
* @return 快递物流信息
*/
default ExpressResponse queryExpressInfo(String tradeNo, String company) {
return null;
}
/**
* 通过快递单号查询顺丰快递物流信息,该接口只用于顺丰快递
*
* @param tradeNo 快递单号
* @param senderPhone 寄件人手机号后四位
* @param receiverPhone 收件人手机号后四位
* @return 快递物流信息
*/
default ExpressResponse querySfExpressInfo(String tradeNo, String senderPhone, String receiverPhone) {
return null;
}
}
创建一个业务接口实现类
百度上很多文章在这里并没有添加@Component
注解,这个地方可以不加,如果不加的话写法不用那么复杂。但是在IDEA中注入时会飘红,看着很不爽。加上@Component
注解后可以解决飘红问题,但是由于加上该注解则意味着这个类的实例化过程将交给Spring进行管理,需要进行不同的处理方式。
我在这里使用了伪单例模式编写,普通的单例模式是在getInstance()
时判断是否已有实例对象,没有的话就去创建。这里是使用initInstance()
的方式来实现实例的创建,该方法在下文的AutoConfiguration
类中调用,利用自动装配原理保证该方法当且仅当项目启动时调用一次,实现单例。
ExpressProvider
类是真正实现业务的类,由于其中需要注入properties
,所以在初始化时交给自动装配去执行。
/**
* 快递物流信息查询接口
*
* @author Nicemorning
* @date Create in 23:16 2020/7/12 0012
*/
@Component
public class ChuanglanExpress implements IChuanglanExpress {
private final ExpressProvider provider;
private volatile static ChuanglanExpress instance;
public static void initInstance(ExpressProvider provider) {
if (instance == null) {
synchronized (ChuanglanExpress.class) {
if (instance == null) {
instance = new ChuanglanExpress(provider);
}
}
}
}
public static ChuanglanExpress getInstance() {
return instance;
}
private ChuanglanExpress(ExpressProvider provider) {
this.provider = provider;
}
/**
* 通过快递单号查询快递物流信息,自动识别快递公司。顺丰除外
*
* @param tradeNo 快递单号
* @return 快递物流信息
*/
@Override
public ExpressResponse queryExpressInfo(String tradeNo) {
return provider.queryExpressInfo(tradeNo);
}
/**
* 通过快递单号和指定快递公司查询快递物流信息。顺丰除外
*
* @param tradeNo 快递单号
* @param company 指定快递公司,使用快递公司字母简称。
* 如:圆通:yuantong;高铁速递:gtsd;中通快递:zhongtong;申通快递:shentong;百世快递(原汇通):huitong;韵达快递:yunda;顺丰速运:shunfeng
* @return 快递物流信息
*/
@Override
public ExpressResponse queryExpressInfo(String tradeNo, String company) {
return provider.queryExpressInfo(company, tradeNo);
}
/**
* 通过快递单号查询顺丰快递物流信息,该接口只用于顺丰快递
*
* @param tradeNo 快递单号
* @param senderPhone 寄件人手机号后四位
* @param receiverPhone 收件人手机号后四位
* @return 快递物流信息
*/
@Override
public ExpressResponse querySfExpressInfo(String tradeNo, String senderPhone, String receiverPhone) {
return provider.querySfExpressInfo(tradeNo, senderPhone, receiverPhone);
}
}
Provider(该类只是在我的项目中这样使用了而已,并不代表必须这么做)
注入properties
@Slf4j
public class ExpressProvider {
private ChuanglanExpressProperties properties;
private ExpressProvider(ChuanglanExpressProperties properties) {
this.properties = properties;
}
public ExpressProvider init(ChuanglanExpressProperties properties) {
this.properties = properties;
return new ExpressProvider(properties);
}
/**
* 通过快递接口发送查询物流信息,自动识别快递公司
*
* @param tradeNo 快递单号
* @return 返回响应实体类
*/
@SuppressWarnings("UnusedReturnValue")
public ExpressResponse queryExpressInfo(String tradeNo) {
.....
}
.....
}
这些类写完后,会发现几个问题,下面一个个来解决。
Providerv 中的Properties怎么注入?
由于Provider中并没有添加@Component
注解,该类并不会被Spring管理,也就不会进行依赖注入。其实可以添加该注解,然后在启动类上增加@Import
注解的方式来注入,但是这种方式无法做到参数的初始化。我们使用另一种方式实现:自动装配。
实现AutoConfiguration
/**
* @author Nicemorning
* @date Create in 22:40 2020/7/12 0012
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "", name = "app-id")
@EnableConfigurationProperties({ChuanglanExpressProperties.class})
@Import({ExpressProvider.class})
public class ChuanglanExpressAutoConfiguration {
private final ExpressProvider expressProvider;
public ChuanglanExpressAutoConfiguration(ExpressProvider expressProvider) {
this.expressProvider = expressProvider;
}
@Bean
@ConditionalOnMissingBean(ChuanglanExpress.class)
IChuanglanExpress createProvider(ChuanglanExpressProperties chuanglanExpressProperties) {
ChuanglanExpress.initInstance(expressProvider.init(chuanglanExpressProperties));
return ChuanglanExpress.getInstance();
}
}
需要注意的是,示例代码中的createProvider()
方法名称在整个项目中的所有AutoConfiguration
类下必须唯一,因为Spring会根据这个方法的名称来命名注入的对象,如果方法名相同则会抛出异常,报错createProvider
已经存在。
Configuration 用于表示该类为配置类
ConditionalOnProperty 表示当指定的配置项存在时才执行这个配置类,其中还有一个属性是
matchIfMissing
,该属性默认值为false
,意为当缺少所指定的-id
时当前配置类不会被执行EnableConfigurationProperties 表示当前配置类需要读取的配置项,即上文所说的Properties类
Import({}) 将刚才创建的Provider注入进来,这样就可以将Provider按实际需要给Provider初始化配置信息
Bean 声明该方法将创建一个Bean,交给Spring管理。创建的Bean类型为该方法的返回类型
ConditionalOnMissingBean() 表示如果IOC容器中不存在
ChuanglanExpress
的实例时执行,如果已存在将不会执行。这样也就保证了伪单例的方式能够实现真正的单例
Bean方法中,为什么参数可以直接指定ChuanglanExpressProperties chuanglanExpressProperties
?这个参数是如何获得的,如何传入的?
打开EnableConfigurationProperties
的源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EnableConfigurationPropertiesRegistrar.class})
public @interface EnableConfigurationProperties {
String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
Class<?>[] value() default {};
}
可以发现这里注入了EnableConfigurationPropertiesRegistrar
,再打开这个类可以看到
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
EnableConfigurationPropertiesRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerInfrastructureBeans(registry);
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
this.getTypes(metadata).forEach(beanRegistrar::register);
}
private Set<Class<?>> getTypes(AnnotationMetadata metadata) {
return (Set)metadata.getAnnotations().stream(EnableConfigurationProperties.class).flatMap((annotation) -> {
return Arrays.stream(annotation.getClassArray("value"));
}).filter((type) -> {
return Void.TYPE != type;
}).collect(Collectors.toSet());
}
static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
ConfigurationPropertiesBindingPostProcessor.register(registry);
BoundConfigurationProperties.register(registry);
ConfigurationPropertiesBeanDefinitionValidator.register(registry);
ConfigurationBeanFactoryMetadata.register(registry);
}
}
该类的实现中已经将所需要的配置注册到Bean管理容器中。所以在方法上可以直接使用ChuanglanExpressProperties chuanglanExpressProperties
这个参数,并且将由Spring直接传参。
到现在为止,该项目已经可以启动。但是启动时会发现,AutoConfiguration并没有被执行。这是因为在项目启动时,并没有执行自定义的AutoConfiguration。这里有两种方式可以实现,一种是利用Import,这种方式不利于管理,我们可以使用第二种方式:使用进行管理。
编写
在Resources目录下新建META-INF目录。在该目录下新建文件
目录结构为:
resources:
|–META-INF:
|----
编写如下内容,将自定义的AutoConfiguration全部添加进去,多个AutoConfiguration需要逗号分隔,如果使用换行的话需要使用\将换行符进行转义
=\
,\
,\
写入配置
在或中填写需要的配置,建议使用yml,因为properties中文会出现乱码的问题,而且处理起来比较麻烦。
chuanglan:
wanshu:
express:
app-id: xxxxxxxxxxxxxxxxxx
app-key: yyyyyyyyyyyyyyyyyyyyyy
url: https:///open/kdwl/kdcx
然后编写单元测试类即可开始测试。
打包并发布到仓库给其他项目使用
如何发布到私服或*仓库这里就不说了,只说一下打包需要注意的事项。
maven中不要引入spring-boot-maven-plugin,如果有的话需要删除掉。
然后使用mvn install进行打包,发布的命令可以自行百度。
优化starter为纯工具JDK
自定义的starter往往是作为工具类的集合给其他项目使用,上文的过程虽然可以实现这些功能,但是每次使用时仍需要注入对应的业务类来实现:
@Autowired
private ChuanglanExpress express;
public void queryExpress(){
express.queryExpressInfo("xxxxxxxxxxxxxxxxxxxxxx");
}
作为工具类我们更倾向于直接使用类调用静态方法的方式去执行,下问将介绍如何将其封装为工具类并暴露接口。
其实上面的所有业务实现为什么使用伪单例,就是为了实现工具类做铺垫。
添加工具类
public class ExpressUtil {
private static final ChuanglanExpress CHUANGLAN_EXPRESS;
static {
CHUANGLAN_EXPRESS = ChuanglanExpress.getInstance();
}
/**
* 通过快递单号查询快递物流信息,自动识别快递公司。顺丰除外
*
* @param tradeNo 快递单号
* @return 快递物流信息
*/
public static ExpressResponse queryExpressInfo(String tradeNo) {
return ExpressUtil.CHUANGLAN_EXPRESS.queryExpressInfo(tradeNo);
}
............
}
使用静态代码块保证该类在第一次初始化时,就能过初始化ChuanglanExpress
的实例。其中ChuanglanExpress
实例已经通过自动装配在项目启动时就已经initInstance()
了,所以在此只需要简单的getInstance()
即可。
为什么不使用@PostConstruct
初始化ChuanglanExpress
的实例?
如果使用了@PostConstruct
的话,在单元测试时是可以执行通过的,但是如果被引用到其他项目时则不会生效,@PostConstruct
方法并不会被执行。具体原因暂时不太清楚,应该是和Spring容器的周期有关,之后找到原因了再补上。
至此就完成了工具类的实现,使用时只需要简单的一句话就可以搞定
ExpressUtil.queryExpressInfo("xxxxxxxxxxxxxxxx")