本文我們使用springboot集成minio,這裏我們沒有直接使用其starter,因爲在maven倉庫當中只有兩個版本,且使用不廣泛。這裏我們可以自己寫一個starter,其他項目直接引用就可以了。
先說一坑,minio的中文文檔版本跟最新的版本完全匹配不上,而英文官網呢,我有始終無法訪問,不知道小夥伴是不是碰到同樣的問題。
關於minio的搭建參考我的前一篇文章:https://www.jianshu.com/p/63dc2947ef91
話不多說,進入正題。
一、pom依賴
我是用的版本:
<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.1</version>
</dependency>
這裏有一坑啊,本來我使用的是最新的8.3.0版本,當所有代碼都寫完後,發現啓動報錯:
***************************
APPLICATION FAILED TO START
***************************
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
io.minio.S3Base.<clinit>(S3Base.java:105)
The following method did not exist:
okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody;
The method's class, okhttp3.RequestBody, is available from the following locations:
jar:file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar!/okhttp3/RequestBody.class
It was loaded from the following location:
file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of okhttp3.RequestBody
2021-08-25 13:01:29.975 [graph-editor: N/A] [ERROR] com.vtc.core.analysis.Slf4jFailureAnalysisReporter -
***************************
APPLICATION FAILED TO START
***************************
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
io.minio.S3Base.<clinit>(S3Base.java:105)
The following method did not exist:
okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody;
The method's class, okhttp3.RequestBody, is available from the following locations:
jar:file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar!/okhttp3/RequestBody.class
It was loaded from the following location:
file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of okhttp3.RequestBody
我以爲是okhttp這個版本或者包重複的問題,一頓鼓搗,發現沒用,最終解決方案是降低了minio的版本到8.2.1,遇到的小夥伴可以嘗試降版本。
二、配置文件
我們需要準備以下內容,配置文件yaml中的配置,分別是minio服務地址,用戶名,密碼,桶名稱:
minio:
endpoint: http://172.16.3.28:10000
accessKey: admin
secretKey: 12345678
bucketName: aaa
另外一部分,設置spring的上傳文件最大限制,如果仍然不行,請考慮是否是網關,或nginx仍然需要配置,nginx配置在最後的配置文件中我給出了100m的大小:
spring:
# 配置文件上傳大小限制
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
三、配置類
此處工需要兩個配置類,分別是屬性配置,用來讀取yaml的配置;另外是初始化MinioClient到spring容器:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* description: minio配置類
*
* @author: weirx
* @time: 2021/8/25 9:47
*/
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioPropertiesConfig {
/**
* 端點
*/
private String endpoint;
/**
* 用戶名
*/
private String accessKey;
/**
* 密碼
*/
private String secretKey;
/**
* 桶名稱
*/
private String bucketName;
}
import io.minio.MinioClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* description: 獲取配置文件信息
*
* @author: weirx
* @time: 2021/8/25 9:50
*/
@Configuration
@EnableConfigurationProperties(MinioPropertiesConfig.class)
public class MinioConfig {
@Resource
private MinioPropertiesConfig minioPropertiesConfig;
/**
* 初始化 MinIO 客戶端
*/
@Bean
public MinioClient minioClient() {
MinioClient minioClient = MinioClient.builder()
.endpoint(minioPropertiesConfig.getEndpoint())
.credentials(minioPropertiesConfig.getAccessKey(), minioPropertiesConfig.getSecretKey())
.build();
return minioClient;
}
}
四、工具類
提供一個簡易的工具類供其他服務直接調用,包括上傳、下載:
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.vtc.core.utils.DownLoadUtils;
import io.minio.*;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @description: minio工具類
* @author:weirx
* @date:2021/8/25 10:03
* @version:3.0
*/
@Component
public class MinioUtil {
@Value("${minio.bucketName}")
private String bucketName;
@Autowired
private MinioClient minioClient;
/**
* description: 判斷bucket是否存在,不存在則創建
*
* @return: void
* @author: weirx
* @time: 2021/8/25 10:20
*/
public void existBucket(String name) {
try {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(name).build());
if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* description: 上傳文件
*
* @param multipartFile
* @return: java.lang.String
* @author: weirx
* @time: 2021/8/25 10:44
*/
public List<String> upload(MultipartFile[] multipartFile) {
List<String> names = new ArrayList<>(multipartFile.length);
for (MultipartFile file : multipartFile) {
String fileName = file.getOriginalFilename();
String[] split = fileName.split("\\.");
if (split.length > 1) {
fileName = split[0] + "_" + System.currentTimeMillis() + "." + split[1];
} else {
fileName = fileName + System.currentTimeMillis();
}
InputStream in = null;
try {
in = file.getInputStream();
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(in, in.available(), -1)
.contentType(file.getContentType())
.build()
);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
names.add(fileName);
}
return names;
}
/**
* description: 下載文件
*
* @param fileName
* @return: org.springframework.http.ResponseEntity<byte [ ]>
* @author: weirx
* @time: 2021/8/25 10:34
*/
public ResponseEntity<byte[]> download(String fileName) {
ResponseEntity<byte[]> responseEntity = null;
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileName).build());
out = new ByteArrayOutputStream();
IOUtils.copy(in, out);
//封裝返回值
byte[] bytes = out.toByteArray();
HttpHeaders headers = new HttpHeaders();
try {
headers.add("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, Constants.UTF_8));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
headers.setContentLength(bytes.length);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setAccessControlExposeHeaders(Arrays.asList("*"));
responseEntity = new ResponseEntity<byte[]>(bytes, headers, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseEntity;
}
}
關於上面的下載文件的返回值問題,我們前端統一返回是這樣,如果其他項目想要使用可以自行修改啊,直接ResponseBody下載,等等的。此處主要參考如何使用MinioClient上傳,下載文件就好了。
五、測試一波
我們使用了springboot集成knife4j,直接通過網關訪問接口文檔,postman也是一樣的啊。我提供下面幾個簡單的接口來測試一下。
@ApiOperation(value = "minio上傳測試")
@PostMapping("/upload")
public List<String> upload(@RequestParam(name = "multipartFile") MultipartFile[] multipartFile) {
return minioUtil.upload(multipartFile);
}
@ApiOperation(value = "minio下載測試")
@GetMapping("/download")
public ResponseEntity<byte[]> download(@RequestParam String fileName) {
return minioUtil.download(fileName);
}
@ApiOperation(value = "minio創建桶")
@PostMapping("/existBucket")
public void existBucket(@RequestParam String bucketName) {
minioUtil.existBucket(bucketName);
}
接口頁面上傳文檔看看:
一個坑來了,發現返回成功了,文件名稱。但是在minio的控制檯沒有數據啊?
一看後臺報錯了,好長一片:
error occurred
ErrorResponse(code = SignatureDoesNotMatch, message = The request signature we calculated does not match the signature you provided. Check your key and signing method., bucketName = esmp, objectName = null, resource = /esmp, requestId = 169E753DE01FE2AF, hostId = 29aa9dc9-661b-432e-a25f-9856ad3a8250)
request={method=GET, url=http://172.16.3.28:10000/esmp?location=, headers=Host: 172.16.3.28:10000
Accept-Encoding: identity
User-Agent: MinIO (Windows 10; amd64) minio-java/8.2.1
Content-MD5: 1B2M2Y8AsgTpgAmY7PhCfg==
x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date: 20210825T052344Z
Authorization: AWS4-HMAC-SHA256 Credential=*REDACTED*/20210825/us-east-1/s3/aws4_request, SignedHeaders=content-md5;host;x-amz-content-sha256;x-amz-date, Signature=*REDACTED*
}
response={code=403, headers=Server: nginx/1.20.1
Date: Wed, 25 Aug 2021 05:23:43 GMT
Content-Type: application/xml
Content-Length: 367
Connection: keep-alive
Accept-Ranges: bytes
Content-Security-Policy: block-all-mixed-content
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Request-Id: 169E753DE01FE2AF
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block
}
at io.minio.S3Base.execute(S3Base.java:667)
at io.minio.S3Base.getRegion(S3Base.java:691)
at io.minio.S3Base.putObject(S3Base.java:2003)
at io.minio.S3Base.putObject(S3Base.java:1153)
at io.minio.MinioClient.putObject(MinioClient.java:1666)
at com.vtc.minio.util.MinioUtil.upload(MinioUtil.java:72)
at com.mvtech.graph.ui.GraphCanvasUI.upload(GraphCanvasUI.java:84)
at com.mvtech.graph.ui.GraphCanvasUI$$FastClassBySpringCGLIB$$5138ff62.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at com.baidu.unbiz.fluentvalidator.interceptor.FluentValidateInterceptor.invoke(FluentValidateInterceptor.java:211)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at com.mvtech.graph.ui.GraphCanvasUI$$EnhancerBySpringCGLIB$$e773947f.upload(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:665)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.github.xiaoymin.knife4j.spring.filter.ProductionSecurityFilter.doFilter(ProductionSecurityFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.github.xiaoymin.knife4j.spring.filter.SecurityBasicAuthFilter.doFilter(SecurityBasicAuthFilter.java:90)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:113)
at com.botany.spore.core.page.PageRequestFilter.doFilterInternal(PageRequestFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:109)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
什麼原因呢?因爲我的minio是集羣模式的,所以我用nginx負載了,此處就報錯了,關於錯誤的nginx配置和如何搭建環境都在我文章看開頭提的上一篇文章中。
此處改成單節點的配置立馬就好了,由負載端口10000改成單節點端口9000,之後就都ok了,無論上傳下載:
minio:
endpoint: http://172.16.3.28:9000
accessKey: admin
secretKey: 12345678
bucketName: aaa
如何解決nginx負載的問題呢?
這個問題和nginx反向代理作轉發的時候所攜帶的header有關係,minio在校驗signature是否有效的時候,必須從http header裏面獲取host,而我們這裏沒有對header作必要的處理。所以我們需要增加以下的配置:
proxy_set_header Host $http_host;
完整的nginx配置如下:
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
upstream minio {
server 172.16.3.28:9000 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.29:9000 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.30:9000 fail_timeout=10s max_fails=2 weight=1;
}
upstream minio-console {
server 172.16.3.28:10001 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.29:10001 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.30:10001 fail_timeout=10s max_fails=2 weight=1;
}
server {
listen 10000;
root /usr/share/nginx/html;
client_max_body_size 100m; # 文件最大不能超過100MB
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass http://minio;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_set_header Host $http_host;
}
error_page 404 /404.html;
location = /404.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
server {
listen 11000;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass http://minio-console;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_set_header Host $http_host;
}
error_page 404 /404.html;
location = /404.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}
再次上傳測試,成功了:
到此爲止就全部完成啦!需要作爲starter的小夥伴不要忘記配置spring.factories.