提供spring boot扩展包,包含自动装配、starter、一些工具类等。

1. 关于

1.1. 简介

spring-boot-extension是一个拓展Spring Boot的库,内置一些SpringBoot未包含的Starter包
同时也补充的各种工具类,用于简化开发、提升开发效率

2. 安装

2.1. 依赖相关

Maven
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.livk-cloud</groupId>
            <artifactId>spring-extension-dependencies</artifactId>
            <version>${version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>
Gradle-Groovy
implementation platform('io.github.livk-cloud:spring-extension-dependencies:$version')
Groovy-Kotlin
implementation(platform("io.github.livk-cloud:spring-extension-dependencies:${version}"))

最小BOM依赖

Maven
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.livk-cloud</groupId>
            <artifactId>spring-extension-bom</artifactId>
            <version>${version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>
Gradle-Groovy
implementation platform('io.github.livk-cloud:spring-extension-bom:$version')
Groovy-Kotlin
implementation(platform("io.github.livk-cloud:spring-extension-bom:${version}"))

2.2. 支持的Java版本

version

支持情况

Jdk21以下

Jdk21-Jdk23

3. 使用指导

3.1. spring boot装配文件自动生成

根据代码定义生成spring boot的自动装配文件和spring.factories、aot.factories

Maven
<dependency>
    <groupId>io.github.livk-cloud</groupId>
    <artifactId>spring-auto-service</artifactId>
    <version>${version}</version>
    <scope>provided</scope>
</dependency>
Gradle-Groovy
compileOnly 'io.github.livk-cloud:spring-auto-service:${version}'
annotationProcessor 'io.github.livk-cloud:spring-auto-service:${version}'
Groovy-Kotlin
compileOnly("io.github.livk-cloud:spring-auto-service:${version}")
annotationProcessor("io.github.livk-cloud:spring-auto-service:${version}")

3.1.1. @SpringAutoService使用示例

@Component
@SpringAutoService
public class SpringContextHolder implements BeanFactoryAware, ApplicationContextAware, DisposableBean {

}

生成文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.livk.commons.spring.context.SpringContextHolder
@AutoConfiguration
@ConditionalOnClass(WebClient.class)
@SpringAutoService(com.livk.commons.http.annotation.EnableWebClient.class)
public class WebClientConfiguration {

}

生成文件 META-INF/spring/com.livk.commons.http.annotation.EnableWebClient.imports

com.livk.commons.http.WebClientConfiguration

3.1.2. @SpringFactories 使用示例

@SpringFactories支持生成aot.factories原理基本同下,只需指定属性aot=true

指定接口为spring.factories的Key

@SpringFactories(org.springframework.boot.env.EnvironmentPostProcessor)
public class TraceEnvironmentPostProcessor implements EnvironmentPostProcessor {

}

生成文件 META-INF/spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=\
    com.livk.commons.spring.TraceEnvironmentPostProcessor

当前类如果仅仅只有一个接口,可以不指定,自动生成

@SpringFactories
public class TraceEnvironmentPostProcessor implements EnvironmentPostProcessor {

}

生成文件 META-INF/spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=\
    com.livk.commons.spring.TraceEnvironmentPostProcessor

3.2. spring通用工具拓展

提供一些通用、工具类方便开发

Maven
<dependency>
    <groupId>io.github.livk-cloud</groupId>
    <artifactId>spring-extension-commons</artifactId>
    <version>${version}</version>
</dependency>
Gradle-Groovy
implementation 'io.github.livk-cloud:spring-extension-commons:${version}'
Groovy-Kotlin
implementation("io.github.livk-cloud:spring-extension-commons:${version}")

3.2.1. aop

AnnotationAbstractPointcutAdvisor

使用注解处理AOP的通用切点及表达式

使用示例

public class LockInterceptor extends AnnotationAbstractPointcutAdvisor<OnLock>{
    @Override
    protected Object invoke(MethodInvocation invocation, OnLock onLock){
        //AOP处理等同于org.aspectj.lang.annotation.Around
    }

    @Override
    public Pointcut getPointcut(){
        //实现切入点
    }
}

将注解作为泛型,自动获取到注解信息

AnnotationAbstractPointcutTypeAdvisor

定制化拓展

使用示例

public class LockInterceptor extends AnnotationAbstractPointcutTypeAdvisor<OnLock>{
    @Override
    protected Object invoke(MethodInvocation invocation, OnLock onLock){
        //
    }

    @Override
    protected AnnotationPointcutType pointcutType() {
        //默认实现
        return AnnotationAutoPointcut.auto();
    }
}
AnnotationPointcutType

提供四种可选方案

  1. TYPE基于类级别的拦截等价于(@Around(@within(Annotation)))

  2. METHOD基于方法级别的拦截等价于(@Around(@annotation(Annotation)))

  3. TYPE_OR_METHOD基于类或方法级别的拦截等价于(@Around(@annotation(Annotation)||@within(Annotation)))

  4. AUTO根据Annotation Target推断(如果仅有TYPE、则为TYPE级别。如果仅有METHOD、则为METHOD级别。如果同时都有则为TYPE_OR_METHOD级别。以上情况都无法出现则抛出异常)

3.2.2. expression

AbstractExpressionResolver

ExpressionResolver抽象实现
将Map或者Method等信息转成Context

使用示例:

public class MyExpressionResolver extends AbstractExpressionResolver {
    @Override
    public <T> T evaluate(String value, Context context, Class<T> returnType) {
        //TODO:解析表达式
    }

    //重写此方法用于调整Context的解析
    @Override
    protected ContextFactory getContextFactory() {
        return super.getContextFactory();
    }
}
CacheExpressionResolver

用于对表达式解析进行缓存构建
同时添加spring-environment的支持

使用示例:

public class MyExpressionResolver extends CacheExpressionResolver<Expression> {
  //将表达式转成不同组件的表达式类
    @Override
    protected Expression compile(String value) throws Throwable {
        return null;
    }

  //根据组件的表达式进行计算
    @Override
    protected <T> T calculate(Expression expression, Context context, Class<T> returnType) throws Throwable {
        return null;
    }
}
ConverterExpressionResolver

用于适配不同的解析工具
将Context转成相对于的上下文环境

public class MyExpressionResolver extends ConverterExpressionResolver<EvaluationContext, Expression> {
  //将上下文转成组件的上下文
    @Override
    protected EvaluationContext transform(Context context) {
        return null;
    }
}
内置ExpressionResolver
  1. SpringExpressionResolver → 根据SpringEL表达式进行解析

  2. AviatorExpressionResolver → 根据Aviator表达式进行解析(需要重新引入jar)

  3. FreeMarkerExpressionResolver → 根据FreeMarker表达式进行解析(需要重新引入jar)

  4. JexlExpressionResolver → 根据Apache Commons Jexl3表达式进行解析(需要重新引入jar)

  5. MvelExpressionResolver → 根据Mvel 2表达式进行解析(需要重新引入jar)

3.2.3. http

EnableHttpClient

EnableHttpClient会根据value值自动导入对应的http客户端

使用示例:

@Slf4j
@EnableHttpClient({
    HttpClientType.REST_TEMPLATE,
    HttpClientType.WEB_CLIENT,
    HttpClientType.REST_CLIENT
})
public class App {

    @Bean
    public ApplicationRunner applicationRunner(WebClient webClient,
                                             RestTemplate restTemplate,
                                             RestClient restClient) {
        return args -> {
            log.info("restTemplate:{}", restTemplate);
            log.info("webClient:{}", webClient);
            log.info("restClient:{}", restClient);
        };
    }

}

同时提供了@EnableRestClient、@EnableRestTemplate和@EnableWebClient作为快捷方式

3.2.4. jackson

JacksonSupport

Jackson便捷开发工具,通过包装ObjectMapper实现各种JSON转换

使用示例:

public static void main(String[] args){
  JacksonSupport json = new JacksonSupport(new JsonMapper());
}
@NumberJsonFormat

Number类型数据Jackson序列化处理注解

3.2.5. util

AnnotationMetadataResolver

根据包名查找含有某些注解的类

使用示例:

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main{
    public static void main(String[] args){
      AnnotationMetadataResolver resolver = new AnnotationMetadataResolver();
      resolver.find(MyAnnotation.class,"com.livk.resolver");
    }
}
BeanLambda

根据lambda解析Field和Method

使用示例:

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main{
    public static void main(String[] args){
      String methodName = BeanLambda.methodName(Maker::getNo);
      Method method = BeanLambda.method(Maker::getNo);
      String fieldName = BeanLambda.fieldName(Maker::getNo);
      Field field = BeanLambda.field(Maker::getNo);
    }
}
GenericWrapper

进行类包装

使用示例:

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main{
    public static void main(String[] args){
      GenericWrapper<?> wrapper = GenericWrapper.of("123");
      wrapper.unwrap()
    }
}
MultiValueMapSplitter

字符串分割成MultiValueMap,例如String str = "root=1,2,3&root=4&a=b&a=c"

使用示例:

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main{
    public static void main(String[] args){
      String str = "root=1,2,3&root=4&a=b&a=c";
            Map<String, List<String>> map = Map.of("root", List.of("1", "2", "3", "4"), "a", List.of("b", "c"));
            MultiValueMap<String, String> multiValueMap = MultiValueMapSplitter.of("&", "=").split(str, ",");
            assertEquals(CollectionUtils.toMultiValueMap(map), multiValueMap);
    }
}

3.2.6. web

HttpParameters

http请求Parameter参数,类似于org.springframework.http.HttpHeaders

使用示例:

public class Main{
    public static void main(String[] args){
        MockHttpServletRequest request = new MockHttpServletRequest();
            request.addParameter("username", "livk", "root", "admin");
            request.addParameter("password", "123456");
            MultiValueMap<String, String> params = WebUtils.params(request);
            HttpParameters parameters = new HttpParameters(params);
    }
}
RequestWrapper

HttpServletRequest包装类,用于修改body、添加header、添加param

使用示例:

public class Main{
    public static void main(String[] args){
        MockHttpServletRequest request = new MockHttpServletRequest();
            RequestWrapper wrapper = new RequestWrapper(request);
        wrapper.body(JsonMapperUtils.writeValueAsBytes(Map.of("root", "root")));
        wrapper.addHeader("Content-Type", "application/json");
        wrapper.addParameter("username", "livk");
        wrapper.addParameter("username", new String[]{"root", "admin"});
    }
}
ResponseWrapper

HttpServletResponse包装类,用于修改body

使用示例:

public class Main{
    public static void main(String[] args){
        MockHttpServletResponse response = new MockHttpServletResponse();
        ResponseWrapper wrapper = new ResponseWrapper(response);
        wrapper.replaceBody(JsonMapperUtils.writeValueAsBytes(Map.of("root", "root")));
    }
}

3.3. spring 组件拓展

兼容Spring的基础包
提供一些第三方包与spring整合的拓展,包括一些自定义拓展

Maven
<dependency>
    <groupId>io.github.livk-cloud</groupId>
    <artifactId>spring-extension-context</artifactId>
    <version>${version}</version>
</dependency>
Gradle-Groovy
implementation 'io.github.livk-cloud:spring-extension-context:${version}'
Groovy-Kotlin
implementation("io.github.livk-cloud:spring-extension-context:${version}")

兼容SpringBoot的拓展包
使用spring boot的自动装配特性,自定义配置文件来覆盖官方的配置

Maven
<dependency>
    <groupId>io.github.livk-cloud</groupId>
    <artifactId>spring-boot-extension-autoconfigure</artifactId>
    <version>${version}</version>
</dependency>
Gradle-Groovy
implementation 'io.github.livk-cloud:spring-boot-extension-autoconfigure:${version}'
Groovy-Kotlin
implementation("io.github.livk-cloud:spring-boot-extension-autoconfigure:${version}")

3.3.1. curator

CuratorOperations

Curator相关所有对zookeeper的操作
由CuratorTemplate进行实现
采用CuratorFramework作为底层实现

使用示例

public static void main(String[] args){
  CuratorFramework framework = createFramework();
  framework.start();
  CuratorTemplate template = new CuratorTemplate(framework);
}
curator-spring-boot-starter

artifactId: curator-spring-boot-starter

根据CuratorProperties自动注册CuratorFramework、RetryPolicy、CuratorTemplate以及Curator Actuator

3.3.2. disruptor

DisruptorScan

类似于MyBatisScan用与扫描DisruptorEvent标记的实体
根据实体和DisruptorEvent生成SpringDisruptor<DisruptorEventWrapper<T>>队列

使用示例

@DisruptorScan("com.livk.disruptor")
public class Config{

}
DisruptorEventProducer

Disruptor生产者

使用示例

@org.springframework.stereotype.Component
public class DisruptorMqServiceImpl implements DisruptorMqService {

    private final DisruptorEventProducer<MessageModel> producer;

    public DisruptorMqServiceImpl(SpringDisruptor<MessageModel> disruptor) {
        producer = new DisruptorEventProducer<>(disruptor);
    }

    @Override
    public void send(String message) {
        log.info("record the message: {}", message);
        producer.send(toMessageModel(message));
    }

    @Override
    public void batch(List<String> messages) {
        List<MessageModel> messageModels = messages.stream().map(this::toMessageModel).toList();
        producer.sendBatch(messageModels);
    }

    private MessageModel toMessageModel(String message) {
        return MessageModel.builder().message(message).build();
    }

}
DisruptorEventConsumer

Disruptor消费者

使用示例

@org.springframework.stereotype.Component
public class Consumerimplements DisruptorEventConsumer<MessageModel> {

    private final ApplicationContext applicationContext;

    @Override
    public void onEvent(MessageModel wrapper, long sequence, boolean endOfBatch) {
        log.info("消费者消费的信息是:{} :{} :{} id:{}", wrapper, sequence, endOfBatch, applicationContext.getId());
    }

}
disruptor-spring-boot-starter

artifactId: disruptor-spring-boot-starter

3.3.3. dynamic

DynamicSource

标记Service方法上,动态数据源切换

使用示例

@DynamicSource("mysql")
public class UserService{

}
DataSourceInterceptor

动态数据源切换拦截器,注册为Spring Bean即可

DynamicDatasource

动态数据源,使用DataSourceContextHolder进行数据源切换

使用示例

@Configuration
public class Config {
  @Bean
    public DynamicDatasource dynamicDatasource() {
        DynamicDatasource dynamicDatasource = new DynamicDatasource();
        dynamicDatasource.setTargetDataSources(datasourceMap);
        dynamicDatasource.setDefaultTargetDataSource(datasourcePrimary);
        return dynamicDatasource;
    }
}
dynamic-datasource-boot-starter

artifactId: dynamic-datasource-boot-starter

根据DynamicDatasourceProperties注册DynamicDatasource和DataSourceInterceptor

需要添加EnableDynamicDatasource到Spring Configuration

3.3.4. fastexcel

Excel导入

使用注解 @ExcelImport 解析Excel(支持Spring Webflux)
fileName指定文件名称
parse使用自定义封装FastExcel的解析器 com.livk.excel.mvc.listener.InfoExcelListener
paramName指定需要传递至那个参数

@RestController
public class InfoController {

    @ExcelImport(parse = InfoExcelListener.class, paramName = "dataExcels")
    @PostMapping("uploadList")
    public HttpEntity<List<Info>> uploadList(List<Info> dataExcels) {
        return ResponseEntity.ok(dataExcels);
    }

    @ExcelImport(parse = InfoExcelListener.class, paramName = "dataExcels")
    @PostMapping("upload")
    public HttpEntity<List<Info>> upload(List<Info> dataExcels) {
        return ResponseEntity.ok(dataExcels);
    }

    @ExcelImport(parse = InfoExcelListener.class, paramName = "dataExcels")
    @PostMapping("uploadMono")
    public Mono<HttpEntity<List<Info>>> uploadMono(Mono<List<Info>> dataExcels) {
        return dataExcels.map(ResponseEntity::ok);
    }
}
Excel导出

使用注解 @ExcelReturn 或者 @ExcelController 解析Excel(支持Spring Webflux)
fileName指定下载文件名
suffix指定Excel后缀 默认xlsm
使用ExcelController之后,fileName为out,suffix为xlsm

返回结果为 List<?> Mono<List<?>> Flux<?> 是sheet名称即为sheet
返回结果为 Map<String,?> Mono<Map<String,?>> 是sheet名称即为map key

@RestController
@RequiredArgsConstructor
public class InfoController {

    @ExcelReturn(fileName = "outFile")
    @ExcelImport(parse = InfoExcelListener.class, paramName = "dataExcels")
    @PostMapping("uploadDownLoad")
    public List<Info> uploadDownLoadMono(List<Info> dataExcels) {
        return dataExcels;
    }

    @ExcelReturn(fileName = "outFile")
    @ExcelImport(parse = InfoExcelListener.class, paramName = "dataExcels")
    @PostMapping("uploadDownLoadMono")
    public Mono<List<Info>> uploadDownLoadMono(Mono<List<Info>> dataExcels) {
        return dataExcels;
    }

    @ExcelReturn(fileName = "outFile")
    @ExcelImport(parse = InfoExcelListener.class, paramName = "dataExcels")
    @PostMapping("uploadDownLoadFlux")
    public Flux<Info> uploadDownLoadFlux(Mono<List<Info>> dataExcels) {
        return dataExcels.flatMapMany(Flux::fromIterable);
    }
}
@ExcelController
public class Info2Controller {

    @PostMapping("download")
    public Map<String, List<Info>> download(@RequestBody List<Info> dataExcels) {
        return dataExcels.stream()
                .collect(Collectors.groupingBy(info -> String.valueOf(Long.parseLong(info.getPhone()) % 10)));
    }
}
fastexcel-spring-boot-starter

artifactId: fastexcel-spring-boot-starter

根据Servlet环境自动注册 ExcelMethodArgumentResolverExcelMethodReturnValueHandler

或Reactive环境自动注册 ReactiveExcelMethodReturnValueHandlerReactiveExcelMethodArgumentResolver

3.3.5. http-spring-boot-starter

artifactId: http-spring-boot-starter

使用示例,在接口上添加 @Provider 或者 @HttpExchange
兼容reactor Mono Flux
使用方式类似于Feign, 被注解标准的接口需要在Spring包扫描下

@Provider(url = "https://spring.io")
public interface RemoteService {

    @GetExchange()
    String get();

}
@Provider(url = "https://spring.io")
@Slf4j
@RestController
@RequiredArgsConstructor
public class HttpController {

    private final RemoteService service;

    @PostConstruct
    public void init() {
        log.info("get length:{}", service.get().trim().length());
    }

    @GetMapping("get")
    public HttpEntity<String> get() {
        return ResponseEntity.ok(service.get());
    }

}

3.3.6. limit

Limit注解

标记方法,用于对此方法进行限流

使用示例

public class UserService {

  @Limit(key = "livk:user", rate = 2, rateInterval = 30)
  public void save(User user) {

  }
}
LimitExecutor

限流执行器基类,实现注册为Spring Bean即可 提供RedissonLimitExecutor作为实现,需要Redisson依赖

LimitInterceptor

限流拦截器,注册为Spring Bean即,使用AOP实现

limit-spring-boot-starter

artifactId: limit-spring-boot-starter

3.3.7. lock

DistLock注解

标记方法,用于对此方法进行分布式锁

使用示例 key支持SpEL表达式,同时支持公平锁、读锁、写锁,并支持异步
在方法上添加 @OnLock(scope = LockScope.DISTRIBUTED_LOCK) (当前分布式锁仅支持Redisson、Curator)+

public class UserService {
  @PostMapping("/buy/distributed")
    @DistLock(key = "shop")
    public void buyLocal(@RequestParam(defaultValue = "2") Integer count) {

    }
}
DistributedLock

分布式锁基类,实现注册为Spring Bean即可 提供RedissonLock作为实现,需要Redisson依赖 提供CuratorLock作为实现,需要Curator依赖

DistributedLockInterceptor

分布式锁拦截器,注册为Spring Bean即,使用AOP实现

distributed-lock-boot-starter

artifactId: distributed-lock-boot-starter

3.3.8. mapstruct

使用示例
自定义转换器
继承 com.livk.autoconfigure.mapstruct.converter.Converter
并添加注解 @Mapper(componentModel = MappingConstants.ComponentModel.SPRING)

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface UserConverter extends Converter<User, UserVO> {

    @Mapping(target = "password", ignore = true)
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createTime", source = "createTime", dateFormat = DateUtils.YMD_HMS)
    @Mapping(target = "type", source = "type", numberFormat = "#")
    @Override
    User getSource(UserVO userVO);

    @Mapping(target = "createTime", source = "createTime", dateFormat = DateUtils.YMD_HMS)
    @Mapping(target = "type", source = "type", numberFormat = "#")
    @Override
    UserVO getTarget(User user);

}

Spring转换器 继承 org.springframework.core.convert.converter.Converter
并添加注解 @Mapper(componentModel = MappingConstants.ComponentModel.SPRING)

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface UserSpringConverter extends Converter<User, UserVO> {

    @Mapping(target = "createTime", source = "createTime", dateFormat = DateUtils.YMD_HMS)
    @Mapping(target = "type", source = "type", numberFormat = "#")
    @Override
    UserVO convert(@Nullable User user);

}

使用 `MapstructService`操作转换自定义转换器
使用 `ConversionService`操作转换Spring转换器

@RestController
@RequestMapping("user")
@RequiredArgsConstructor
public class UserController {

    public static final List<User> USERS = List.of(
            new User().setId(1).setUsername("livk").setPassword("123456").setType(1).setCreateTime(new Date()),
            new User().setId(2).setUsername("livk2").setPassword("123456").setType(2).setCreateTime(new Date()),
            new User().setId(3).setUsername("livk3").setPassword("123456").setType(3).setCreateTime(new Date()));

    // 自定义双向转换
    private final MapstructService service;

    // spring单向转换
    private final ConversionService conversionService;

    private final ConversionServiceAdapter conversionServiceAdapter;

    @PostConstruct
    public void init() {
        System.out.println(conversionService.convert(USERS.get(0), UserVO.class));
        service.convert(USERS, UserVO.class).forEach(System.out::println);
        SpringContextHolder.getBean(MapstructService.class).convert(USERS, UserVO.class).forEach(System.out::println);
    }

    @GetMapping
    public HttpEntity<Map<String, List<UserVO>>> list() {
        List<UserVO> userVOS = USERS.stream().map(user -> conversionService.convert(user, UserVO.class))
                .filter(Objects::nonNull).toList();
        return ResponseEntity
                .ok(Map.of("spring", userVOS,
                        "customize", service.convert(USERS, UserVO.class).toList()));
    }

    @GetMapping("/{id}")
    public HttpEntity<Map<String, UserVO>> getById(@PathVariable Integer id) {
        User u = USERS.stream().filter(user -> user.getId().equals(id)).findFirst().orElse(new User());
        UserVO userVOSpring = conversionService.convert(u, UserVO.class);
        return ResponseEntity.ok(Map.of("customize", service.convert(u, UserVO.class),
                "spring", userVOSpring,
                "adapter", conversionServiceAdapter.mapUserToUserVO(u)));
    }

}
mapstruct-spring-boot-starter

artifactId: mapstruct-spring-boot-starter

3.3.9. mybatis

mybatis-extension-boot-starter

artifactId: mybatis-extension-boot-starter

3.3.10. oss

oss-spring-boot-starter

artifactId: oss-spring-boot-starter

3.3.11. qrcode

qrcode-spring-boot-starter

artifactId: qrcode-spring-boot-starter

3.3.12. redis-ops-boot-starter

artifactId: redis-ops-boot-starter

3.3.13. redisearch

redisearch-spring-boot-starter

artifactId: redisearch-spring-boot-starter

3.3.14. redisson-spring-boot-starter

artifactId: redisson-spring-boot-starter

3.3.15. useragent

browscap-spring-boot-starter

artifactId: browscap-spring-boot-starter

yauaa-spring-boot-starter

artifactId: yauaa-spring-boot-starter

3.4. spring testcontainers support容器拓展

提供自定义testcontainers与spring boot的支持

4. 问题