文章目录
- Pre
- 需求
- 使用步骤
- 1. 引入pom依赖
- 2. 配置
- 2. 使用注解
- 实现
- 自动装配类 和 属性文件
- FileStorageFactory
- 本地存储实现
- FTP存储实现
- SFTP存储实现
- S3存储实现(MINIO)
- spring.factories
- pom
Pre
Spring Boot - 手把手教小师妹自定义Spring Boot Starter
需求
系统中,文件存储是个非常常规的需求,大家都需要重复开发,何不封装一个starter支持多协议文件存储的呢?
目前规划了如下的功能:
- 支持 多种存储, FTP , SFTP ,本地存储 , S3协议客户端(MINIO、 阿里云等)
- 支持自定义属性配置
- 开箱即用
使用步骤
各位看官,先看看符不符合你的需要,先演示下开发完成后的如何集成到自己的业务系统中。
1. 引入pom依赖
代码语言:javascript复制 <dependency>
<groupId>com.artisangroupId>
<artifactId>artisan-filestorage-spring-boot-starterartifactId>
<version>1.0version>
dependency>
2. 配置
代码语言:javascript复制artisan:
filestorage:
storage-type: s3
ftp:
host: 192.168.126.140
port: 21
username: ftptest
password: ftptest
mode: Passive
base-path: /artisan
s3:
endpoint: http://192.168.126.140:9000
access-key: admin
access-secret: password
bucket: artisan-bucket
sftp:
base-path: /root/abc
username: root
password: artisan
host: 192.168.126.140
port: 22
local:
base-path: D://test
核心: 根据 storage-type 来决定实例化哪种实例对象。 其它配置为实例对象的属性配置。
2. 使用注解
代码语言:javascript复制package com.artisan.doc.controller;
import cn.hutool.core.io.IoUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import net.zfsy.frame.file.storage.FileStorageFactory;
import net.zfsy.frame.operatelog.core.util.ServletUtils;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
@Api(tags = "S3文件存储")
@RestController
@RequestMapping("/s3")
@Validated
@Slf4j
public class S3FileController {
@Resource
private FileStorageFactory fileStorageFactory;
@PostMapping("/upload")
@ApiOperation("上传文件")
@ApiImplicitParams({
@ApiImplicitParam(name = "path", value = "文件相对路径", example = "soft", dataTypeClass = String.class),
@ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class)
})
public String uploadFile(String path, @RequestParam("file") MultipartFile file) throws Exception {
return fileStorageFactory.getStorage().createFile(path, file.getOriginalFilename(), IoUtil.readBytes(file.getInputStream()));
}
@DeleteMapping("/delete")
@ApiOperation("删除文件")
@ApiImplicitParams({
@ApiImplicitParam(name = "path", value = "文件相对路径", required = true, dataTypeClass = String.class),
@ApiImplicitParam(name = "fileName", value = "文件名称", required = true, dataTypeClass = String.class)
})
public void deleteFile(String path, @RequestParam("fileName") String fileName) throws Exception {
fileStorageFactory.getStorage().deleteFile(path, fileName);
}
@GetMapping("/get")
@ApiOperation("下载文件")
@ApiImplicitParams({
@ApiImplicitParam(name = "path", value = "文件相对路径", required = true, dataTypeClass = String.class),
@ApiImplicitParam(name = "fileName", value = "文件名称", required = true, dataTypeClass = String.class)
})
public void getFileContent(HttpServletResponse response,
String path, @RequestParam("fileName") String fileName) throws Exception {
byte[] content = fileStorageFactory.getStorage().getFileContent(path, fileName);
if (content == null) {
log.warn("[getFileContent][path({}) fileName({}) 文件不存在]", path, fileName);
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
ServletUtils.writeAttachment(response, fileName, content);
}
}
实现
自动装配类 和 属性文件
代码语言:javascript复制/**
* @author 小工匠
* @version 1.0
* @date 2022/4/16 19:12
* @mark: show me the code , change the world
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ArtisanFileUploadProperties.class)
@ConditionalOnProperty(prefix = ArtisanFileUploadProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class ArtisanFileUploadAutoConfiguration implements InitializingBean, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(ZhongFuFileUploadAutoConfiguration.class);
private ZhongFuFileUploadProperties config;
public ZhongFuFileUploadAutoConfiguration(ZhongFuFileUploadProperties config) {
this.config = config;
}
/**
*
* @return 文件存储工厂对象
*/
@Bean
public FileStorageFactory fileStorageFactory(){
return new FileStorageFactory(config);
}
@Override
public void destroy() {
logger.info("<== 【销毁--自动化配置】----多协议文件上传组件【ZhongFuFileUploadAutoConfiguration】");
}
@Override
public void afterPropertiesSet() {
logger.info("==> 【初始化--自动化配置】----多协议文件上传组件【ZhongFuFileUploadAutoConfiguration】");
}
}
代码语言:javascript复制package net.zfsy.frame.file.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* @author 小工匠
* @version 1.0
* @date 2022/4/18 22:55
* @mark: show me the code , change the world
*/
@ConfigurationProperties(ArtisanFileUploadProperties.PREFIX)
@Data
public class ArtisanFileUploadProperties {
/**
* 属性配置前缀
*/
public static final String PREFIX = "zf.filestorage";
/**
* 文件服务类型
* 1. file:本地磁盘
* 2. ftp:FTP 服务器
* 3. sftp:SFTP 服务器
* 4. s3:支持 S3 协议的云存储服务,比如 MinIO、阿里云、华为云、腾讯云、七牛云等等
*/
private StorageType storageType;
private LocalStorageProperties local;
private FtpStorageProperties ftp;
private SftpStorageProperties sftp;
private S3StorageProperties s3;
/**
* Type of Storage to use.
*/
public enum StorageType {
/**
* 本地存储
*/
local,
/**
* ftp存储
*/
ftp,
/**
* sftp存储
*/
sftp,
/**
* s3协议的存储,比如minio
*/
s3
}
/**
* Local
*/
@Data
public static class LocalStorageProperties {
/**
* 基础路径
*/
@NotEmpty(message = "基础路径不能为空")
private String basePath;
}
/**
* FTP
*/
@Data
public static class FtpStorageProperties {
/**
* 基础路径
*
* 1. basePath为null或""上传到当前路径
* 2. basePath为相对路径则相对于当前路径的子路径
* 3. basePath为绝对路径则上传到此路径
*/
@NotEmpty(message = "基础路径不能为空")
private String basePath;
/**
* 主机地址
*/
@NotEmpty(message = "host 不能为空")
private String host;
/**
* 主机端口
*/
@NotNull(message = "port 不能为空")
private Integer port;
/**
* 用户名
*/
@NotEmpty(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
private String password;
/**
* 连接模式
*
* 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串
* Active 主动模式
* Passive 被动模式 (推荐)
*/
@NotEmpty(message = "连接模式不能为空")
private String mode;
}
@Data
public static class SftpStorageProperties {
/**
* 基础路径
*/
@NotEmpty(message = "基础路径不能为空")
private String basePath;
/**
* 主机地址
*/
@NotEmpty(message = "host 不能为空")
private String host;
/**
* 主机端口
*/
@NotNull(message = "port 不能为空")
private Integer port;
/**
* 用户名
*/
@NotEmpty(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
private String password;
}
/**
* S3协议
*/
@Data
public static class S3StorageProperties {
/**
* 节点地址 MinIO:http://127.0.0.1:9000
*/
@NotNull(message = "endpoint 不能为空")
private String endpoint;
/**
* 存储 Bucket
*/
@NotNull(message = "bucket 不能为空")
private String bucket;
/**
* 访问 Key
*/
@NotNull(message = "accessKey 不能为空")
private String accessKey;
/**
* 访问 Secret
*/
@NotNull(message = "accessSecret 不能为空")
private String accessSecret;
}
}
FileStorageFactory
代码语言:javascript复制/**
* @author 小工匠
* @version 1.0
* @date 2022/4/19 18:40
* @mark: show me the code , change the world
*/
public class FileStorageFactory {
private Logger logger = LoggerFactory.getLogger(FileStorageFactory.class);
/**
* 存储配置信息
*/
private ZhongFuFileUploadProperties config;
/**
* 文件存储类型 和 实例化存储对象 映射关系
*/
private Map<String, FileStorage> uploader = new ConcurrentHashMap<>();
/**
* 构造函数
*
* @param config
*/
public FileStorageFactory(ZhongFuFileUploadProperties config) {
this.config = config;
}
/**
* @return 文件存储对象
*/
public FileStorage getStorage() {
// 获取配置文件中配置的存储类型
String type = config.getStorageType().name();
// 缓存对象,避免重复创建
if (ZhongFuFileUploadProperties.StorageType.local.name().equalsIgnoreCase(type) && uploader.get(type) == null) {
uploader.put(type, new LocalFileStorage(config));
} else if (ZhongFuFileUploadProperties.StorageType.ftp.name().equalsIgnoreCase(type) && uploader.get(type) == null) {
uploader.put(type, new FtpFileStorage(config));
} else if (ZhongFuFileUploadProperties.StorageType.sftp.name().equalsIgnoreCase(type) && uploader.get(type) == null) {
uploader.put(type, new SftpFileStorage(config));
} else if (ZhongFuFileUploadProperties.StorageType.s3.name().equalsIgnoreCase(type) && uploader.get(type) == null) {
uploader.put(type, new S3FileStorage(config));
} else {
if (uploader.get(type) == null) {
uploader.put(type, new LocalFileStorage(config));
logger.warn("未找到配置的文件存储类型, 将使用默认LocalFileStorage");
}
}
// 返回实例化存储对象
return uploader.get(type);
}
}
代码语言:javascript复制/**
* 文件 Storage 接口
* @author artisan
*/
public interface FileStorage {
/**
* 保存文件
*
* @param path 文件路径
* @param path 文件名称
* @param content 文件内容
* @return 文件路径
*/
String createFile(String path, String fileName, byte[] content) throws Exception;
/**
* 删除文件
*
* @param path 相对路径
* @throws Exception 删除文件时,抛出 Exception 异常
*/
void deleteFile(String path, String fileName) throws Exception;
/**
* 获得文件内容
*
* @param path 文件路径
* @param fileName 文件名
* @return 文件内容
*/
byte[] getFileContent(String path, String fileName) throws Exception;
}
代码语言:javascript复制/**
* @author 小工匠
* @version 1.0
* @date 2022/4/19 10:42
* @mark: show me the code , change the world
*/
public abstract class AbstractFileStorage implements FileStorage {
/**
* 业务扩展
*/
public final void ext() {
doExt();
}
/**
* 自定义业务扩展
*/
protected abstract void doExt();
本地存储实现
代码语言:javascript复制/**
* 本地文件 Storage 实现类
*
* @author artisan
*/
public class LocalFileStorage extends AbstractFileStorage {
private Logger logger = LoggerFactory.getLogger(LocalFileStorage.class);
private ZhongFuFileUploadProperties config;
public LocalFileStorage(ZhongFuFileUploadProperties config) {
this.config = config;
ZhongFuFileUploadProperties.LocalStorageProperties local = this.config.getLocal();
Assert.notNull(local, "本地存储配置信息不能为空,请配置 basePath 属性");
// 补全风格 Linux 是 /,Windows 是
if (!local.getBasePath().endsWith(File.separator)) {
local.setBasePath(local.getBasePath() File.separator);
}
logger.info("初次调用, 实例化LocalFileStorage");
}
@Override
public String createFile(String path, String fileName, byte[] content) {
// 执行写入
File file = FileUtil.writeBytes(content, getAbsFilePath(path, fileName));
logger.info("LOCAL-文件写入操作:入参path->{} , 文件名->{} , 文件存储路径->{}", path, fileName, file.getAbsolutePath());
return file.getAbsolutePath();
}
@Override
public void deleteFile(String path, String fileName) {
String filePath = getAbsFilePath(path, fileName);
FileUtil.del(filePath);
logger.info("LOCAL-文件删除操作:入参path->{} , 绝对路径->{}", path, filePath);
}
@Override
public byte[] getFileContent(String path, String fileName) {
String filePath = getAbsFilePath(path, fileName);
logger.info("LOCAL-文件读取操作:入参path->{} , 绝对路径->{}", path, filePath);
return FileUtil.readBytes(filePath);
}
/**
* @param path 相对路径
* @param fileName 文件名名称
* @return 文件绝对路径
*/
private String getAbsFilePath(String path, String fileName) {
return StrUtil.isBlank(path) ? (config.getLocal().getBasePath() File.separator fileName) : (config.getLocal().getBasePath() path File.separator fileName);
}
@Override
protected void doExt() {
}
FTP存储实现
代码语言:javascript复制/**
* Ftp Storage 实现类
*
* @author artisan
*/
public class FtpFileStorage extends AbstractFileStorage {
private Logger logger = LoggerFactory.getLogger(FtpFileStorage.class);
private Ftp ftp;
private ZhongFuFileUploadProperties config;
public FtpFileStorage(ZhongFuFileUploadProperties config) {
this.config = config;
ZhongFuFileUploadProperties.FtpStorageProperties ftpConfig = config.getFtp();
Assert.notNull(ftpConfig, "ftp客户端配置信息不能为空");
// TODO fix me when publish (File.separator )测试 临时使用 "/"
if (!ftpConfig.getBasePath().endsWith("/")) {
ftpConfig.setBasePath(ftpConfig.getBasePath() "/");
}
// 初始化 Ftp 对象
this.ftp = new Ftp(ftpConfig.getHost(), ftpConfig.getPort(), ftpConfig.getUsername(), ftpConfig.getPassword(),
CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(ftpConfig.getMode()));
logger.info("初次调用, 实例化FtpFileStorage");
}
@Override
protected void doExt() {
}
/**
* @param path 文件相对路径
* @param fileName 文件名称
* @param content 文件内容
* @return 文件存储路径
*/
@Override
public String createFile(String path, String fileName, byte[] content) throws Exception {
// 执行写入
String destPath = config.getFtp().getBasePath() path.trim();
boolean success = ftp.upload(destPath, fileName, new ByteArrayInputStream(content));
if (!success) {
throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", path));
}
logger.info("FTP-文件写入操作:入参path->{} , 文件名->{} ", path, fileName);
return path File.separator fileName;
}
@Override
public byte[] getFileContent(String path, String fileName) {
String filePath = config.getFtp().getBasePath() path.trim();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ftp.download(filePath, fileName, out);
logger.info("FTP-文件读取操作:入参path->{} , 绝对路径->{}", path, filePath);
return out.toByteArray();
}
@Override
public void deleteFile(String path, String fileName) throws Exception {
// TODO fix me when publish 测试 临时使用 "/"
String filePath = config.getFtp().getBasePath() path.trim() "/" fileName;
boolean success = ftp.delFile(filePath);
if (!success) {
throw new FtpException(StrUtil.format("删除文件 ({}) 失败", filePath));
}
logger.info("FTP-文件删除操作:入参path->{} , 绝对路径->{}", path, filePath);
}
SFTP存储实现
代码语言:javascript复制/**
* @author 小工匠
* @version 1.0
* @date 2022/4/20 10:21
* @mark: show me the code , change the world
*/
public class SftpFileStorage extends AbstractFileStorage {
private Logger logger = LoggerFactory.getLogger(SftpFileStorage.class);
private Sftp sftp;
private ZhongFuFileUploadProperties config;
public SftpFileStorage(ZhongFuFileUploadProperties config) {
this.config = config;
ZhongFuFileUploadProperties.SftpStorageProperties sftpStorageProperties = this.config.getSftp();
Assert.notNull(sftpStorageProperties, "Sftp客户端不能为空");
// 补全风格。例如说 Linux 是 /,Windows 是 TODO
if (!sftpStorageProperties.getBasePath().endsWith(File.separator)) {
sftpStorageProperties.setBasePath(sftpStorageProperties.getBasePath() "/");
}
// 初始化 Ftp 对象
this.sftp = new Sftp(sftpStorageProperties.getHost(), sftpStorageProperties.getPort(), sftpStorageProperties.getUsername(), sftpStorageProperties.getPassword());
// 创建目录
sftp.mkdir(sftpStorageProperties.getBasePath());
logger.info("初次调用, 实例化SftpFileStorage");
}
@Override
protected void doExt() {
}
@Override
public String createFile(String path, String fileName, byte[] content) throws Exception {
// 创建目录
sftp.mkdir(getDir(path));
// 获取文件存储路径 TODO
String destPath = getDestPath(path, fileName);
// 根据文件名,创建文件
File file = createFileByFileName(content, fileName);
// 执行写入
boolean success = sftp.upload(destPath, file);
if (!success) {
throw new SftpException(500, StrUtil.format("SFTP上传文件到目标目录 ({}) 失败", destPath));
}
logger.info("SFTP-文件写入操作:入参path->{} , 文件名->{} ", path, fileName);
// 拼接返回路径
return destPath;
}
@Override
public void deleteFile(String path, String fileName) throws Exception {
String destPath = getDestPath(path, fileName);
sftp.delFile(destPath);
logger.info("Sftp-文件删除操作:入参path->{} , 文件名->{} , 文件存储路径->{}", path, fileName, destPath);
}
@Override
public byte[] getFileContent(String path, String fileName) throws Exception {
String filePath = getDestPath(path,fileName);
File destFile = new File(fileName);
sftp.download(filePath, destFile);
return FileUtil.readBytes(destFile);
}
/**
* @param path
* @param fileName
* @return 文件路径
*/
private String getDestPath(String path, String fileName) {
String destPath = config.getSftp().getBasePath() path.trim() "/" fileName;
return destPath;
}
/**
* @param path
* @return 文件目录
*/
private String getDir(String path) {
return config.getSftp().getBasePath() path.trim();
}
/**
* 创建文件
*
* @param fileName 文件名
* @return 文件
*/
@SneakyThrows
public static File createFileByFileName(byte[] data ,String fileName) {
File file = new File(fileName);
// 标记 JVM 退出时,自动删除
file.deleteOnExit();
// 写入内容
FileUtil.writeBytes(data, file);
return file;
}
}
S3存储实现(MINIO)
代码语言:javascript复制/**
* @author 小工匠
* @version 1.0
* @date 2022/4/20 8:44
* @mark: show me the code , change the world
*/
public class S3FileStorage extends AbstractFileStorage {
private Logger logger = LoggerFactory.getLogger(LocalFileStorage.class);
private MinioClient client;
private ZhongFuFileUploadProperties config;
public S3FileStorage(ZhongFuFileUploadProperties config) {
this.config = config;
ZhongFuFileUploadProperties.S3StorageProperties s3StorageProperties = this.config.getS3();
Assert.notNull(s3StorageProperties, "S3协议客户端不能为空");
validate(Validation.buildDefaultValidatorFactory().getValidator(), s3StorageProperties);
// 初始化客户端
client = MinioClient.builder()
// Endpoint URL
.endpoint(buildEndpointURL(s3StorageProperties))
// 认证密钥
.credentials(s3StorageProperties.getAccessKey(), s3StorageProperties.getAccessSecret())
.build();
// 创建Bucket
checkBucket(s3StorageProperties.getBucket());
logger.info("初次调用, 实例化S3FileStorage");
}
/**
* 检查Bucket是否存在,不存在 创建
*
* @param bucketName bucket 名称
*/
private void checkBucket(String bucketName) {
try {
if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
MakeBucketArgs makeArgs = MakeBucketArgs.builder().bucket(bucketName).build();
client.makeBucket(makeArgs);
logger.info("bucket {} 不存在, 自动创建该bucket", bucketName);
}
} catch (Exception e) {
logger.error(" 自动创建bucket {} 异常", bucketName, e.getMessage());
}
}
/**
* 基于 endpoint 构建调用云服务的 URL 地址
*
* @return URI 地址
*/
private String buildEndpointURL(ZhongFuFileUploadProperties.S3StorageProperties s3StorageProperties) {
return s3StorageProperties.getEndpoint();
}
@Override
protected void doExt() {
}
@Override
public String createFile(String path, String fileName, byte[] content) throws Exception {
// TODO
String filePath = path "/" fileName;
// 执行上传
client.putObject(PutObjectArgs.builder()
// bucket 必须传递
.bucket(config.getS3().getBucket())
// 相对路径作为 key
.object(filePath)
// 文件内容
.stream(new ByteArrayInputStream(content), content.length, -1)
.build());
String url = config.getS3().getEndpoint() "/" config.getS3().getBucket() "/" filePath;
logger.info("S3-文件写入操作:入参path->{} , 文件名->{} ,文件路径->{}", path, fileName, url);
// 拼接返回路径
return url;
}
@Override
public void deleteFile(String path, String fileName) throws Exception {
// TODO
String filePath = path "/" fileName;
client.removeObject(RemoveObjectArgs.builder()
// bucket 必须传递
.bucket(config.getS3().getBucket())
// 相对路径作为 key
.object(filePath)
.build());
logger.info("S3-文件删除操作:入参path->{} , 文件名->{} , 文件存储路径->{}", path, fileName);
}
@Override
public byte[] getFileContent(String path, String fileName) throws Exception {
// TODO
String filePath = path "/" fileName;
GetObjectResponse response = client.getObject(GetObjectArgs.builder()
// bucket 必须传递
.bucket(config.getS3().getBucket())
// 相对路径作为 key
.object(filePath)
.build());
return IoUtil.readBytes(response);
}
/**
*
* @param validator
* @param object
* @param groups
*/
private void validate(Validator validator, Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (CollUtil.isNotEmpty(constraintViolations)) {
throw new ConstraintViolationException(constraintViolations);
}
}
}
spring.factories
代码语言:javascript复制# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.artisan.frame.file.config.ArtisanFileUploadAutoConfiguration
pom
代码语言:javascript复制<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-coreartifactId>
dependency>
<dependency>
<groupId>commons-netgroupId>
<artifactId>commons-netartifactId>
dependency>
<dependency>
<groupId>com.jcraftgroupId>
<artifactId>jschartifactId>
dependency>
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-annotationsartifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-allartifactId>
<scope>testscope>
dependency>
dependencies>
别忘了 spring-boot-configuration-processor 哦
代码语言:javascript复制 <dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>