如何写一个自己的Spring-Boot-Starter

背景

我们知道可以使用SpringBoot快速开发基于Spring框架的项目,由于围绕SpringBoot存在很多开箱即用的Starter依赖,是的我们在开发业务代码时候能够非常方便的、不需要过多关注框架的配置,而只需要关注业务即可。

例如我我想在SpringBoot项目中集成Redis,那么我只需要加入spring-data-redis-starter的依赖,并简单配置一下连接信息以及Jedis连接池就可以。这为我们省去了之前很多的配置操作。甚至有些功能的开启只需要在启动类或配置类上增加一个注释即可完成。

原理

我们指定使用一个公用Starter的时候,只需要将相应的依赖添加到Maven的pom文件中即可,免去了自己需要引用很多的依赖包,并且SpringBoot会自动进行类的自动配置。

那么SpringBoot是如何知道要实例化哪些类并进行配置的?

  1. SpringBoot在启动时,会去依赖的Starter包中寻找 resouces/META-INF/spring.factories 文件,然后根据文件中配置的Jar包去扫描项目所依赖的Jar包,这类似于Java的SPI机制。
  2. 根据 spring.factories 配置加载 AutoConfigure 类。
  3. 根据@Conditional 注释的条件,进行自动配置并将Bean注入Spring Context 上下文当中。

我们也可以使用 @ImportAutoConfiguration({MyServiceAutoConfigration.class})指定自动配置哪些类。

用过SPI机制的同学可能会清楚一个概念,当一个框架需要动态的扩展能力、给使用者充分的扩展能力,那么可能会使用到SPI机制。

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

官方说明

Spring Boot checks for the presence of a META-INF/spring.factories file within your published jar.

The file should list your configuration classes under the EnableAutoConfiguration key, as shown in the following example:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

从上面的说明可以看出,只要我们在自己的Starter的META-INF/spring.factories中配置好AutoConfiguration,SpringBoot就可以检测并且加载。AutoConfiguration需要用到@Configuration 标识,代表它是一个配置类,并且结合 @Conditional 及相关注解灵活读取配置信息。

@Conditional

我们可以控制一个类满足某一条件才能进行实例化。在Spring中就有@Conditional注释和Condition接口,这个接口有一个matches方法,使用者可以重写这个方法。

public class TestCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return true;
    }
}

我们就可以把@Conditional(value = TestCondition.class)标注在类上面,表示该类下面的所有@Bean都会启用配置。在spring-boot-autoconfigure中,SpringBoot官方实现了一系列Condition,用来方便平时开发,如下:

注解 说明
@ConditionalOnBean 在BeanFactory已经存在指定Bean
@ConditionalOnMissingBean 在BeanFactory不存在指定Bean
@ConditionalOnClass 在classpath下已存在指定class
@ConditionalOnCloudPlatform 指定云平台已生效
@ConditionalOnExpression 指定SPEL表达式为true
@ConditionalOnJava 指定Java版本(前或后)
@ConditionalOnJndi 指定JNDI存在
@ConditionalOnNotWebApplication 非web应用
@ConditionalOnWebApplication web环境
@ConditionalOnProperty 指定的property有指定的值
@ConditionalOnResource 在classpath下存在指定的资源
@ConditionalOnSingleCandidate BeanFactory中该类型Bean只有一个或@Primary的只有一个


实现步骤如何写一个自己的Spring-Boot-Starter

第一步:命名与分模块

接下来我们开始编写我们自己的,SpringBoot官方推荐自定义starterxxx-spring-boot-starter命名,并且分两个模块,Autoconfigure模块Starter模块,主要配置在Autoconfigure模块Starter模块其实是一个空项目模块。

首先在父项目的pom文件加入SpringBoot依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>${spring-boot.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

Starter模块

Starter模块是个空模块,只有一个pom文件,目的就是提供必要的依赖项。

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>me.mingshan</groupId>
    <artifactId>demo-spring-boot-autoconfigure</artifactId>
    <version>${project.version}</version>
  </dependency>
</dependencies>

第二步:编写AutoConfigure模块

Autoconfigure模块加入依赖:

<!-- Compile dependencies -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
</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-starter-aop</artifactId>
</dependency>

其中 spring-boot-configuration-processor 主要是用来生成元数据文件(META-INF/spring-configuration-metadata.json)。如果存在该文件,那么SpringBoot会优先读取元数据文件,提供启动速度。

自定义Service

例如我们想实现一个基于AOP切面实现的程序耗时日志打印Starter,本次我们可以选择使用@ConditionalOnProperty,即配置文件中有 aspect.log.enable = true,才加载我们的配置类。

1.定义AspectLog注释

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AspectLog {
}

2.定义配置文件对应类

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("aspect-log")
public class AspectLogProperties {
    private boolean enable;
    public boolean isEnable() {
        return enable;
    }
    public void setEnable(boolean enable) {
        this.enable = enable;
    }
}

3.定义自动配置类

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.PriorityOrdered;

@Aspect
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
@Configuration
@ConditionalOnProperty(prefix = "aspect-log", name = "enable",
                     havingValue = "true", matchIfMissing = true)
public class AspectLogAutoConfiguration implements PriorityOrdered {

    protected Logger logger = LoggerFactory.getLogger(getClass());

@Around("@annotation(com.xxx.aspect.log.AspectLog) ")
    public Object isOpen(ProceedingJoinPoint thisJoinPoint) 
                                        throws Throwable 
{
        //执行方法名称 
        String taskName = thisJoinPoint.getSignature()
            .toString().substring(
                thisJoinPoint.getSignature()
                    .toString().indexOf(" "), 
                    thisJoinPoint.getSignature().toString().indexOf("("));
        taskName = taskName.trim();
        long time = System.currentTimeMillis();
        Object result = thisJoinPoint.proceed();
        logger.info("method:{} run :{} ms", taskName, 
                            (System.currentTimeMillis() - time));
        return result;
    }
    @Override
    public int getOrder() {
        //保证事务等切面先执行
        return Integer.MAX_VALUE;
    }
}

第三步:配置spring.factories

META-INF/spring.factories是spring的工程机制,在这个文件中定义的类都会自动加载,多个配置使用逗号分割,换行用

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.shanyuan.autoconfiguration.aspectlog.AspectLogAutoConfiguration

第四步:构建成starter

mvn install ,就可以进行测试验证了,自己的 Spring Boot Starter 开发完成了。

DEMO

https://github.com/j5land/aspect-log-spring-boot

总结

我们描述Starter的实现原理,简要说明了@Conditional的作用,以及如何开发一个Starter的步骤。

最后通过spring.factories 配置加载,根据@Conditional 注条件,进行自动配置并将Bean 注入Spring Context进行实践,编写了基于注解的面向切面接口耗时统计的`Starter`,可以参见DEMO部分,欢迎star ^_^

References

  • Creating Your Own Starter (https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter)
  • MyBatis Stater源码阅读 (https://github.com/mybatis/spring-boot-starter)


原文始发于微信公众号(程序猿阿南):如何写一个自己的Spring-Boot-Starter

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/22259.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!