Spring|Spring Boot 自定义starter

一、简介 SpringBoot 用起来方便,它默认集成了 Java 的主流框架。这也是 SpringBoot 的一大特色,使用方便,需要什么框架或者技术,只需要引入对应的 starter 即可。目前官方已经集成的各大技术的启动器,可以查看 文档。
即使官方集成了很多主流框架,但SpringBoot官方也不能囊括我们所有的使用场景,往往我们需要自定义starter,来简化我们对SpringBoot的使用。
二、命名规范 在制作自己的starter之前,先来谈谈starter的命名规则,命名规则分为两种,一种是官方的命名规则,另一种就是我们自己制作的starter命名规则。
官方命名规则

  • 前缀:spring-boot-starter-
  • 模式:spring-boot-starter-模块名
  • 举例:spring-boot-starter-webspring-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-starterhello-spring-boot-starter-autoconfigurer
hello-spring-boot-starter-autoconfigurer pom.xml
4.0.0org.springframework.boot spring-boot-starter-parent 2.1.7.RELEASE com.example hello-spring-boot-starter-autoconfigurer 0.0.1-SNAPSHOT autoconfigurerUTF-8UTF-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} ${java.version} ${project.build.sourceEncoding}

项目结构:
Spring|Spring Boot 自定义starter
文章图片
项目结构 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.0org.springframework.boot spring-boot-starter-parent 2.1.7.RELEASE com.example hello-spring-boot-starter 0.0.1-SNAPSHOT starter1.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就会看到以下信息:

Spring|Spring Boot 自定义starter
文章图片
hello测试结果
到此,一个简单的starter就介绍完毕了。
四、进阶版 在翻看SpringBoot自动注入相关源码时会发现, 在 SpringBoot 中,我们经常可以看到很多以 Condition开头的注解,例如:ConditionalOnBeanConditionalOnMissingBeanConditionalOnClassConditionalOnMissingClassConditionalOnJavaConditionalOnPropertyConditionalOnResource等等,如果看它们的源码的话,可以发现它们都使用了@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对象,当且仅当所有Classmatches方法都返回true时,才会装载BeanSpring中。
使用范例:
@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[] 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 = https://www.it610.com/article/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(); } }

测试结果: Spring|Spring Boot 自定义starter
文章图片
设置系统字符集参数 Spring|Spring Boot 自定义starter
文章图片
测试结果 (注:以上示例本人摘抄于网络,并未对其进行检验,但大致思路没问题,仅供参考)
五、针对@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
【Spring|Spring Boot 自定义starter】SpringBoot根据条件自动装配Bean(基于Condition接口和Conditional注解)

    推荐阅读