一、简介
SpringBoot
用起来方便,它默认集成了 Java
的主流框架。这也是 SpringBoot
的一大特色,使用方便,需要什么框架或者技术,只需要引入对应的 starter
即可。目前官方已经集成的各大技术的启动器,可以查看 文档。
即使官方集成了很多主流框架,但SpringBoot
官方也不能囊括我们所有的使用场景,往往我们需要自定义starter
,来简化我们对SpringBoot
的使用。
二、命名规范
在制作自己的starter之前,先来谈谈starter
的命名规则,命名规则分为两种,一种是官方的命名规则,另一种就是我们自己制作的starter
命名规则。
官方命名规则
- 前缀:
spring-boot-starter-
- 模式:
spring-boot-starter-模块名
- 举例:
spring-boot-starter-web
、spring-boot-starter-jdbc
自定义命名规则
- 后缀:
-spring-boot-starter
- 模式:
模块-spring-boot-starter
- 举例:
hello-spring-boot-starter
三、创建自己的starter
一个完整的SpringBoot Starter
可能包含以下组件:
-
autoconfigurer
模块:包含自动配置的代码 -
starter
模块:提供对autoconfigurer
模块的依赖,以及一些其它的依赖
(PS:如果你不需要区分这两个概念的话,也可以将自动配置代码模块与依赖管理模块合并成一个模块)
简而言之,starter
应该提供使用该库所需的一切
1、创建两个工程
我们需要先创建两个工程 hello-spring-boot-starter
和 hello-spring-boot-starter-autoconfigurer
hello-spring-boot-starter-autoconfigurer
pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.7.RELEASE
com.example
hello-spring-boot-starter-autoconfigurer
0.0.1-SNAPSHOT
autoconfigurer
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-configuration-processor
true
org.apache.maven.plugins
maven-compiler-plugin
${java.version}
${project.build.sourceEncoding}
项目结构:
HelloProperties.java
package com.example.autoconfigurer;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* hello 配置属性
*
* @author lz
* @date 2019/8/23
*/
@ConfigurationProperties(prefix = HelloProperties.HELLO_PREFIX)
public class HelloProperties {
public static final String HELLO_PREFIX = "project.hello";
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
HelloService.java
package com.example.autoconfigurer;
/**
* Hello 服务
*
* @author lz
* @date 2019/8/23
*/
public class HelloService {
HelloProperties helloProperties;
HelloProperties getHelloProperties() {
return helloProperties;
}
void setHelloProperties(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}
public String sayHello(String name) {
return helloProperties.getPrefix() + " " + name+" " + helloProperties.getSuffix();
}
}
HelloAutoConfiguration.java
package com.example.autoconfigurer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* Hello 服务 配置类
*
* @author lz
* @date 2019/6/4
*/
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
@Order(0)
public class HelloAutoConfiguration {
@Bean
public HelloService helloService(HelloProperties helloProperties) {
HelloService helloService = new HelloService();
helloService.setHelloProperties(helloProperties);
return helloService;
}
}
META-INF\spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfigurer.HelloAutoConfiguration
hello-spring-boot-starter
pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.7.RELEASE
com.example
hello-spring-boot-starter
0.0.1-SNAPSHOT
starter
1.8
org.springframework.boot
spring-boot-starter
com.example
hello-spring-boot-starter-autoconfigurer
0.0.1-SNAPSHOT
starter
项目只做依赖的引入,不需要写任何代码,对两个项目进行install
编译安装;
2、使用
创建一个demo
程序进行引用自定义starter
项目:
pom.xml引入hello-spring-boot-starter
依赖:
com.example
hello-spring-boot-starter
0.0.1-SNAPSHOT
编写一个HelloService
测试控制器HelloTestController.java
:
package com.example.demo;
import com.example.autoconfigurer.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* hello 测试接口
*
* @author lz
* @date 2019/8/23
*/
@RestController
public class HelloTestController {
@Autowired
private HelloService helloService;
@GetMapping
public String testHello(String name) {
return helloService.sayHello(name);
}
}
在application.yml
配置文件中加入配置:
project:
hello:
prefix: hi
suffix: what's up man ?
在加入配置时会有相应的属性提醒,这就是以下依赖的作用:
org.springframework.boot
spring-boot-configuration-processor
true
在项目编译时加入依赖,就会编译出一个spring-configuration-metadata.json
的文件,springboot
配置时的提示就是来自于这个文件。
启动项目,访问测试接口:http://localhost:8080/?name=zhangsan
就会看到以下信息:
到此,一个简单的starter
就介绍完毕了。
四、进阶版
在翻看SpringBoot
自动注入相关源码时会发现, 在 SpringBoot
中,我们经常可以看到很多以 Condition
开头的注解,例如:ConditionalOnBean
、ConditionalOnMissingBean
、ConditionalOnClass
、ConditionalOnMissingClass
、ConditionalOnJava
、ConditionalOnProperty
、ConditionalOnResource
等等,如果看它们的源码的话,可以发现它们都使用了@Conditional
注解,并且指定了一个或者多个XxxCondition.class
,再看XxxCondition
源码,发现它们都实现了 Condition
接口。
其实Condition
接口和Conditional
注解是SpringBoot
提供的实现按条件自动装配 Bean
的工具。
1、如何使用 Condition
接口和 Conditional
注解
-
Condition
接口源码如下,自定义条件时实现该接口
/**
* 实现 Condition 的 matches 方法,在此方法中进行逻辑判断
* 方法返回值:
* true:装载此类到 Spring 容器中
* false:不装载此类到 Spring 容器中
*/
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
-
Conditional
注解源码如下:
使用方式:
在配置类(带有@SpringBootConfiguration
或者@Configuration
的类)上加此注解或者在有@Bean
的方法上加此注解,并指定实现了Condition
接口的Class对象,注意:如果指定多个Class
对象,当且仅当所有Class
的matches
方法都返回true
时,才会装载Bean
到Spring
中。
使用范例:
@Conditional(GBKCondition.class)、@Conditional({GBKCondition.class, UTF8Condition.class})
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* value:Class 对象数组,当配置多个 Class 对象时,当且仅当所有条件都返回 true 时,相关的 Bean 才可以被装载到 Spring 容器中
*/
Class extends Condition>[] value();
}
示例:
以系统字符集判断系统加载GBK
还是UTF-8
的类
面向接口编程思想:
编码转换接口EncodingConvert.java
:
public interface EncodingConvert {
}
UTF8
编码UTF8EncodingConvert.java
public class UTF8EncodingConvert implements EncodingConvert {
}
GBK
编码GBKEncodingConvert.java
public class GBKEncodingConvert implements EncodingConvert {
}
GBK
加载条件,实现 Condition
接口,获取程序运行时参数,判断是否是加载该 Bean
GBKCondition.java
public class GBKCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String encoding = System.getProperty("file.encoding");
return null != encoding ? ("GBK".equals(encoding.toUpperCase())) : false;
}
}
UTF-8
加载条件UTF8Condition.java
public class UTF8Condition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String encoding = System.getProperty("file.encoding");
return null != encoding ? ("UTF-8".equals(encoding.toUpperCase())) : false;
}
}
编码配置类EncodingConvertConfiguration.java
@Configuration
public class EncodingConvertConfiguration {
@Bean
@Conditional(GBKCondition.class)
public EncodingConvert gbkEncoding() {
return new GBKEncodingConvert();
}
@Bean
@Conditional(value = UTF8Condition.class)
public EncodingConvert utf8Encoding() {
return new UTF8EncodingConvert();
}
}
启动类App.java
@SpringBootApplication
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
System.out.println(context.getBeansOfType(EncodingConvert.class));
context.close();
}
}
测试结果:
(注:以上示例本人摘抄于网络,并未对其进行检验,但大致思路没问题,仅供参考)
五、针对@ConditionalOnClass
在实际开发过程中,往往有很多特殊情况需要我们去探索,就拿上面的示例进行讲解,如果各种字符集的实现都有第三方来做,那么在制作一个通用的starter
时,就会有class
不在classpath
下的情况,那么就会用到@ConditionalOnClass
的注解来判断是否在classpath
下存在这个相应的类,从而进行注入spring
。
但本人在制作starter时,最初是把@ConditionalOnClass
注解加入到方法上,这样就可以一个XXXAutoConfiguration
类注入很多实现该接口的服务,但实际往往与理想相悖。通过测试发现@ConditionalOnClass
在类上面是可以实现classpath
下类是否存在的检测的,如果不存在,则不注入,如果存在,则进行相关的注入操作,但为什么@ConditionalOnClass
可以标记在方法上,而又不起作用,暂时还不清楚。
通过对Spring Boot
org.springframework.boot.autoconfigure
包中源码的阅读,得知 SpringBoot
其实也是只是把@ConditionalOnClass
注解用于类上,而并没有用于方法。那么上面的问题又该如何解决呢?
继续通过阅读发现org.springframework.boot.autoconfigure.websocket.servlet
包下的WebSocketServletAutoConfiguration
源码:
@Configuration
@ConditionalOnClass({ Servlet.class, ServerContainer.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@AutoConfigureBefore(ServletWebServerFactoryAutoConfiguration.class)
public class WebSocketServletAutoConfiguration {
@Configuration
@ConditionalOnClass({ Tomcat.class, WsSci.class })
static class TomcatWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
public TomcatWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
return new TomcatWebSocketServletWebServerCustomizer();
}
}
@Configuration
@ConditionalOnClass(WebSocketServerContainerInitializer.class)
static class JettyWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
public JettyWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
return new JettyWebSocketServletWebServerCustomizer();
}
}
@Configuration
@ConditionalOnClass(io.undertow.websockets.jsr.Bootstrap.class)
static class UndertowWebSocketConfiguration {
@Bean
@ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer")
public UndertowWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() {
return new UndertowWebSocketServletWebServerCustomizer();
}
}
}
可以看出,如果一个配置类中需要用到多个@ConditionalOnClass
注解,那么最好的解决办法就是像这样写一些静态内部类,然后再把公共类进行自动注入,这样,当加载公共类时,就会去加载这些静态的内部类,然后就会根据@ConditionalOnClass
的条件,是否进行自动注入了。
下面是org.springframework.boot.test.autoconfigure.json
包下JsonTestersAutoConfiguration
的部分源码:
@Configuration
@ConditionalOnClass(ObjectMapper.class)
static class JacksonJsonTestersConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnBean(ObjectMapper.class)
public FactoryBean> jacksonTesterFactoryBean(ObjectMapper mapper) {
return new JsonTesterFactoryBean<>(JacksonTester.class, mapper);
}
}
@Configuration
@ConditionalOnClass(Gson.class)
static class GsonJsonTestersConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnBean(Gson.class)
public FactoryBean> gsonTesterFactoryBean(Gson gson) {
return new JsonTesterFactoryBean<>(GsonTester.class, gson);
}
}
@Configuration
@ConditionalOnClass(Jsonb.class)
static class JsonbJsonTesterConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnBean(Jsonb.class)
public FactoryBean> jsonbTesterFactoryBean(Jsonb jsonb) {
return new JsonTesterFactoryBean<>(JsonbTester.class, jsonb);
}
}
可以看出FactoryBean
有三种不同的实现,而这三种实现不全是Spring
官网来维护的,那么就很明显能达到我们想要的结果。
下面是本人写的一个相关功能的部分关键源码:
@Configuration
@EnableConfigurationProperties(ResourceProperties.class)
@Slf4j
public class ResourceServiceAutoConfiguration {
@Configuration
@ConditionalOnClass(OSSClient.class)
@AutoConfigureAfter(OSSClient.class)
static class OSSResourceServiceAutoConfiguration {
@Order(2)
@Conditional(OssConditional.class)
@Bean
@ConditionalOnMissingBean
public IResourceService IResourceServiceFactory(OSSClient ossClient) {
log.info("OssResourceServiceImpl 初始化...");
return new OssResourceServiceImpl(ossClient);
}
}
@Configuration
@ConditionalOnClass(HdfsService.class)
@AutoConfigureAfter(HdfsService.class)
static class HdfsResourceServiceAutoConfiguration {
@Order(2)
@Conditional(HdfsConditional.class)
@Bean
@ConditionalOnMissingBean
public IResourceService IResourceServiceFactory(HdfsService hdfsService) {
log.info("HdfsResourceServiceImpl 初始化...");
return new HdfsResourceServiceImpl(hdfsService);
}
}
}
这样,当你应用oss
模块时就注入oss
相关的服务,当你引用hdfs
时,就注入hdfs
相关的服务。
参考:
第五篇 : SpringBoot 自定义starter
SpringBoot根据条件自动装配Bean(基于Condition接口和Conditional注解)