Springboot自定义Starter、配置读取和工具类构建

创建Springboot应用

命名规范

Spring官方建议命名规则为:

官方的Starter命名为:spring-boot-starter-XXXXXX
非官方的Starter命名为:XXXXXX-spring-boot-starter

项目结构

Spring官方建议一个Starter应包含两个模块,其中一个用于AutoConfiguration,另一个用于实现业务。为了方便项目搭建,也可以直接使用一个模块。

POM依赖

SpringbootStarter与普通的Springboot项目不同,其依赖于其他的Springboot项目使用,需要结合自动装配特性。以下依赖为必须项:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>ch.qos.logback</groupId>
                    <artifactId>logback-classic</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

MAVEN打包配置修改

SpringbootStarter不需要启动类!如果使用了启动类的话,将会导致调用者的SpringIOC容器无法管理该Starter。

启动类修改

@EnableAutoConfiguration
@ComponentScan({"cn.nicemorning.myspringbootstarter"})
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的包名为:“cn.nicemorning.myspringbootstarter”;父级项目的包名为:“com.douyait.agm”,两者包名不一致,会导致Starter的包不能被扫描到,所以需要添加@ComponentScan({"cn.nicemorning.myspringbootstarter"})用于指定当前包也需要被扫描。

单元测试类修改

由于单独的Starter并不是一个完整的应用,大多数时候都是作为一个实际应用的一部分存在,所以需要创建能够独立运行的Test。

首先需要保证引入以下依赖:

<dependency>
        <groupId>org.springframework.boot</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:application.yml")

RunWith 标识为Spring提供的JUnit运行环境

SpringBootTest(classes={…}) 不同于完整的Springboot项目,单独的starter没有Application.class所以需要指定环境需要加载的Configuration文件, 此处的classes的值是数组,根据测试的覆盖范围需要把涉及到的Configuration文件写入

TestPropertySource 指示测试时读取resource/test.properties作为配置文件,因为作为一个Starter,运行时读取依赖它的应用的配置文件,所以测试中需要指定一个配置文件作为数据来源

其中SpringBootTest所指定的AutoConfiguration类为自定义的自动装配类,在下文进行说明;剩下的@Test、@Before这些使用和平常一样,不再赘述。

开始编写代码

创建一个Properties类,读取配置

与普通的Springboot项目创建方式一致,直接贴代码:

/**
 * @author Nicemorning
 * @date Create in 22:36 2020/7/12 0012
 */
@Data
@ConfigurationProperties("chuanglan.wanshu.express")
public class ChuanglanExpressProperties {
    /**
     * 快递信息查询接口APP ID
     */
    public String appId;
    /**
     * 快递信息查询接口APP KEY
     */
    public String appKey;
    /**
     * 接口地址
     */
    public String url = "https://api.253.com/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 = "chuanglan.wanshu.express", 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,意为当缺少所指定的chuanglan.wanshu.express.app-id时当前配置类不会被执行

EnableConfigurationProperties 表示当前配置类需要读取的配置项,即上文所说的Properties类

Import({ExpressProvider.class}) 将刚才创建的Provider注入进来,这样就可以将Provider按实际需要给Provider初始化配置信息

Bean 声明该方法将创建一个Bean,交给Spring管理。创建的Bean类型为该方法的返回类型

ConditionalOnMissingBean(ChuanglanExpress.class) 表示如果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,这种方式不利于管理,我们可以使用第二种方式:使用spring.factories进行管理。

编写spring.factories

在Resources目录下新建META-INF目录。在该目录下新建spring.factories文件

目录结构为:

resources:

|–META-INF:

|----spring.factories

编写如下内容,将自定义的AutoConfiguration全部添加进去,多个AutoConfiguration需要逗号分隔,如果使用换行的话需要使用\将换行符进行转义

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.douyait.com.chuanglanspringbootstarter.sms.config.notify.NotifySmsAutoConfiguration,\
com.douyait.com.chuanglanspringbootstarter.sms.config.verify.VerifySmsAutoConfiguration,\
com.douyait.com.chuanglanspringbootstarter.express.config.ChuanglanExpressAutoConfiguration

写入配置

在application.yml或application.properties中填写需要的配置,建议使用yml,因为properties中文会出现乱码的问题,而且处理起来比较麻烦。

chuanglan:
  wanshu:
    express:
      app-id: xxxxxxxxxxxxxxxxxx
      app-key: yyyyyyyyyyyyyyyyyyyyyy
      url: https://xxxxx.xxxxxx.com/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")
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值