feat(oss): 增加对象存储模块并支持多种存储策略

- 新增对象存储服务模块 `han-note-oss`,集成 Rustfs、阿里云 OSS 及腾讯云 Cos 存储
- 提供统一的 `FileStrategy` 接口及 `FileStrategyFactory` 工厂类,根据存储类型动态选择存储策略
- 实现阿里云 OSS、腾讯云 Cos 和 Rustfs 具体存储逻辑
- 增加文件上传接口 `FileController`,支持接收文件并返回访问路径
- 完成用户密码更新接口,使用`spring.security`对密码进行加密
This commit is contained in:
Hanserwei
2025-10-03 17:52:25 +08:00
parent 5c406b48f9
commit 0d71d8e209
33 changed files with 974 additions and 1 deletions

View File

@@ -0,0 +1,88 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定父项目 -->
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-oss</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-oss-biz</artifactId>
<name>${project.artifactId}</name>
<description>对象存储业务层</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--Rustfs对象存储-->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<!-- 阿里云 OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<!-- 腾讯云 OSS -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.oss;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HannoteOssBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteOssBizApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
package com.hanserwei.hannote.oss.config;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AliyunOSSConfig {
@Resource
private AliyunOSSProperties aliyunOSSProperties;
/**
* 构建 阿里云 OSS 客户端
*
* @return 阿里云 OSS 客户端
*/
@Bean
public OSS aliyunOSSClient() {
// 设置访问凭证
DefaultCredentialProvider credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(
aliyunOSSProperties.getAccessKey(), aliyunOSSProperties.getSecretKey());
// 创建 OSSClient 实例
return new OSSClientBuilder().build(aliyunOSSProperties.getEndpoint(), credentialsProvider);
}
}

View File

@@ -0,0 +1,14 @@
package com.hanserwei.hannote.oss.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "storage.aliyun-oss")
@Component
@Data
public class AliyunOSSProperties {
private String endpoint;
private String accessKey;
private String secretKey;
}

View File

@@ -0,0 +1,48 @@
package com.hanserwei.hannote.oss.config;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.endpoint.EndpointBuilder;
import com.qcloud.cos.region.Region;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CosConfig {
@Resource
private CosProperties cosProperties;
@Bean
public COSClient cosClient() {
// 1. 初始化用户身份信息SecretId, SecretKey
COSCredentials cred = new BasicCOSCredentials(
cosProperties.getSecretId(),
cosProperties.getSecretKey()
);
// 2. 设置 bucket 的地域
Region region = new Region(cosProperties.getRegion());
ClientConfig clientConfig = new ClientConfig(region);
if (cosProperties.getEndpoint() != null && !cosProperties.getEndpoint().isEmpty()) {
clientConfig.setEndpointBuilder(new EndpointBuilder() {
@Override
public String buildGeneralApiEndpoint(String bucketName) {
// 所有 API 请求都会使用自定义域名
return cosProperties.getEndpoint();
}
@Override
public String buildGetServiceApiEndpoint() {
return cosProperties.getEndpoint();
}
});
}
// 3. 构建 COSClient
return new COSClient(cred, clientConfig);
}
}

View File

@@ -0,0 +1,16 @@
package com.hanserwei.hannote.oss.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(prefix = "storage.cos")
public class CosProperties {
private String endpoint;
private String secretId;
private String secretKey;
private String appId;
private String region;
}

View File

@@ -0,0 +1,28 @@
package com.hanserwei.hannote.oss.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import java.net.URI;
@Configuration
public class RustfsConfig {
@Resource
private RustfsProperties rustfsProperties;
@Bean
public S3Client minioClient() {
// 构建 Rustfs 客户端
return S3Client.builder()
.endpointOverride(URI.create(rustfsProperties.getEndpoint()))
.region(Region.US_EAST_1)
.credentialsProvider(() -> AwsBasicCredentials.create(rustfsProperties.getAccessKey(), rustfsProperties.getSecretKey()))
.forcePathStyle(true)
.build();
}
}

View File

@@ -0,0 +1,14 @@
package com.hanserwei.hannote.oss.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "storage.rustfs")
@Component
@Data
public class RustfsProperties {
private String endpoint;
private String accessKey;
private String secretKey;
}

View File

@@ -0,0 +1,27 @@
package com.hanserwei.hannote.oss.controller;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.oss.service.FileService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/file")
@Slf4j
public class FileController {
@Resource
private FileService fileService;
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Response<?> uploadFile(@RequestPart(value = "file") MultipartFile file) {
return fileService.uploadFile(file);
}
}

View File

@@ -0,0 +1,23 @@
package com.hanserwei.hannote.oss.enums;
import com.hanserwei.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 通用异常状态码 -----------
SYSTEM_ERROR("OSS-10000", "出错啦,后台小维正在努力修复中..."),
PARAM_NOT_VALID("OSS-10001", "参数错误!!!"),
// ----------- 业务异常状态码 -----------
;
// 异常码
private final String errorCode;
// 错误信息
private final String errorMsg;
}

View File

@@ -0,0 +1,103 @@
package com.hanserwei.hannote.oss.exception;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.oss.enums.ResponseCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Optional;
@SuppressWarnings("LoggingSimilarMessage")
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获自定义业务异常
*
* @return Response.fail(e)
*/
@ExceptionHandler({ApiException.class})
@ResponseBody
public Response<Object> handleApiException(HttpServletRequest request, ApiException e) {
log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMsg());
return Response.fail(e);
}
/**
* 捕获参数校验异常
*
* @return Response.fail(errorCode, errorMessage)
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 获取 BindingResult
BindingResult bindingResult = e.getBindingResult();
StringBuilder sb = new StringBuilder();
// 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com';
Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> {
errors.forEach(error ->
sb.append(error.getField())
.append(" ")
.append(error.getDefaultMessage())
.append(", 当前值: '")
.append(error.getRejectedValue())
.append("'; ")
);
});
// 错误信息
String errorMessage = sb.toString();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 捕获 guava 参数校验异常
*
* @return Response.fail(ResponseCodeEnum.PARAM_NOT_VALID)
*/
@ExceptionHandler({IllegalArgumentException.class})
@ResponseBody
public Response<Object> handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 错误信息
String errorMessage = e.getMessage();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 其他类型异常
*
* @param request 请求
* @param e 异常
* @return Response.fail(ResponseCodeEnum.SYSTEM_ERROR)
*/
@ExceptionHandler({Exception.class})
@ResponseBody
public Response<Object> handleOtherException(HttpServletRequest request, Exception e) {
log.error("{} request error, ", request.getRequestURI(), e);
return Response.fail(ResponseCodeEnum.SYSTEM_ERROR);
}
}

View File

@@ -0,0 +1,34 @@
package com.hanserwei.hannote.oss.factory;
import com.hanserwei.hannote.oss.strategy.FileStrategy;
import com.hanserwei.hannote.oss.strategy.impl.AliyunOSSFileStrategy;
import com.hanserwei.hannote.oss.strategy.impl.CosFileStrategy;
import com.hanserwei.hannote.oss.strategy.impl.RustfsFileStrategy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RefreshScope
public class FileStrategyFactory {
@Value("${storage.type}")
private String strategyType;
@Bean
@RefreshScope
public FileStrategy getFileStrategy() {
if (strategyType == null) {
throw new IllegalArgumentException("存储类型不能为空");
}
return switch (strategyType.toLowerCase()) {
case "rustfs" -> new RustfsFileStrategy();
case "aliyun" -> new AliyunOSSFileStrategy();
case "cos" -> new CosFileStrategy();
default -> throw new IllegalArgumentException("不可用的存储类型: " + strategyType);
};
}
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.hannote.oss.service;
import com.hanserwei.framework.common.response.Response;
import org.springframework.web.multipart.MultipartFile;
public interface FileService {
/**
* 上传文件
*
* @param file 文件
* @return 文件上传结果
*/
Response<?> uploadFile(MultipartFile file);
}

View File

@@ -0,0 +1,25 @@
package com.hanserwei.hannote.oss.service.impl;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.oss.service.FileService;
import com.hanserwei.hannote.oss.strategy.FileStrategy;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
public class FileServiceImpl implements FileService {
@Resource
private FileStrategy fileStrategy;
private static final String BUCKET_NAME = "han-note";
@Override
public Response<?> uploadFile(MultipartFile file) {
// 上传文件
String url = fileStrategy.uploadFile(file, BUCKET_NAME);
return Response.success(url);
}
}

View File

@@ -0,0 +1,16 @@
package com.hanserwei.hannote.oss.strategy;
import org.springframework.web.multipart.MultipartFile;
public interface FileStrategy {
/**
* 文件上传
*
* @param file 文件
* @param bucketName 存储桶名称
* @return 文件访问路径
*/
String uploadFile(MultipartFile file, String bucketName);
}

View File

@@ -0,0 +1,58 @@
package com.hanserwei.hannote.oss.strategy.impl;
import com.aliyun.oss.OSS;
import com.hanserwei.hannote.oss.config.AliyunOSSProperties;
import com.hanserwei.hannote.oss.strategy.FileStrategy;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.util.UUID;
@Slf4j
public class AliyunOSSFileStrategy implements FileStrategy {
@Resource
private AliyunOSSProperties aliyunOSSProperties;
@Resource
private OSS ossClient;
@Override
@SneakyThrows
public String uploadFile(MultipartFile file, String bucketName) {
log.info("## 上传文件至阿里云 OSS ...");
// 判断文件是否为空
if (file == null || file.getSize() == 0) {
log.error("==> 上传文件异常:文件大小为空 ...");
throw new RuntimeException("文件大小不能为空");
}
// 文件的原始名称
String originalFileName = file.getOriginalFilename();
// 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串)
String key = UUID.randomUUID().toString().replace("-", "");
// 获取文件的后缀,如 .jpg
String suffix = null;
if (originalFileName != null) {
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
}
// 拼接上文件后缀,即为要存储的文件名
String objectName = String.format("%s%s", key, suffix);
log.info("==> 开始上传文件至阿里云 OSS, ObjectName: {}", objectName);
// 上传文件至阿里云 OSS
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(file.getInputStream().readAllBytes()));
// 返回文件的访问链接
String url = String.format("https://%s.%s/%s", bucketName, aliyunOSSProperties.getEndpoint(), objectName);
log.info("==> 上传文件至阿里云 OSS 成功,访问路径: {}", url);
return url;
}
}

View File

@@ -0,0 +1,65 @@
package com.hanserwei.hannote.oss.strategy.impl;
import com.hanserwei.hannote.oss.config.CosProperties;
import com.hanserwei.hannote.oss.strategy.FileStrategy;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.ObjectMetadata;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.UUID;
@SuppressWarnings("DuplicatedCode")
@Slf4j
public class CosFileStrategy implements FileStrategy {
@Resource
private CosProperties cosProperties;
@Resource
private COSClient cosClient;
@Override
@SneakyThrows
public String uploadFile(MultipartFile file, String bucketName) {
log.info("## 上传文件至腾讯云Cos ...");
// 判断文件是否为空
if (file == null || file.getSize() == 0) {
log.error("==> 上传文件异常:文件大小为空 ...");
throw new RuntimeException("文件大小不能为空");
}
// 文件的原始名称
String originalFileName = file.getOriginalFilename();
// 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串)
String key = UUID.randomUUID().toString().replace("-", "");
// 获取文件的后缀,如 .jpg
String suffix = null;
if (originalFileName != null) {
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
}
// 拼接上文件后缀,即为要存储的文件名
String objectName = String.format("%s%s", key, suffix);
log.info("==> 开始上传文件至腾讯云Cos, ObjectName: {}", objectName);
// 设置元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 执行上传
try (InputStream inputStream = file.getInputStream()) {
cosClient.putObject(bucketName, objectName, inputStream, metadata);
}
// 返回文件的访问链接
String url = String.format("https://%s/%s", cosProperties.getEndpoint(), objectName);
log.info("==> 上传文件至腾讯云 Cos 成功,访问路径: {}", url);
return url;
}
}

View File

@@ -0,0 +1,73 @@
package com.hanserwei.hannote.oss.strategy.impl;
import com.hanserwei.hannote.oss.config.RustfsProperties;
import com.hanserwei.hannote.oss.strategy.FileStrategy;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.InputStream;
import java.util.UUID;
@Slf4j
public class RustfsFileStrategy implements FileStrategy {
@Resource
private RustfsProperties rustfsProperties;
@Resource
private S3Client rustfsClient;
@Override
@SneakyThrows
public String uploadFile(MultipartFile file, String bucketName) {
log.info("## 上传文件至 Rustfs ...");
// 判断文件是否为空
if (file == null || file.isEmpty()) {
log.error("==> 上传文件异常:文件为空 ...");
throw new RuntimeException("文件不能为空");
}
// 文件的原始名称
String originalFileName = file.getOriginalFilename();
String contentType = file.getContentType();
// 生成存储对象的名称
String key = UUID.randomUUID().toString().replace("-", "");
String suffix = "";
if (originalFileName != null && originalFileName.contains(".")) {
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
}
// 拼接最终文件名
String objectName = key + suffix;
log.info("==> 开始上传文件至 Rustfs, ObjectName: {}", objectName);
// 执行上传
try (InputStream inputStream = file.getInputStream()) {
rustfsClient.putObject(
PutObjectRequest.builder()
.bucket(bucketName) // 方法参数传入 bucketName
.key(objectName)
.contentType(contentType)
.build(),
RequestBody.fromInputStream(inputStream, file.getSize())
);
}
// 返回文件的访问链接注意Rustfs 是否支持直链要看配置)
String url = String.format("%s/%s/%s",
rustfsProperties.getEndpoint().replaceAll("/$", ""), // 去掉结尾的斜杠
bucketName,
objectName);
log.info("==> 上传文件至 Rustfs 成功,访问路径: {}", url);
return url;
}
}

View File

@@ -0,0 +1,9 @@
server:
port: 8081 # 项目启动的端口
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境
storage:
type: rustfs # 对象存储类型

View File

@@ -0,0 +1,19 @@
spring:
application:
name: han-note-oss # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
config:
server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
file-extension: yaml # 配置文件格式
refresh-enabled: true # 是否开启动态刷新

View File

@@ -0,0 +1,58 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property scope="context" name="appName" value="oss"/>
<!-- 自定义日志输出路径,以及日志名称前缀 -->
<property name="LOG_FILE" value="./logs/${appName}.%d{yyyy-MM-dd}"/>
<!-- 每行日志输出的格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的命名格式 -->
<fileNamePattern>${LOG_FILE}-%i.log</fileNamePattern>
<!-- 保留 30 天的日志文件 -->
<maxHistory>30</maxHistory>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>0</totalSizeCap>
<!-- 重启服务时,是否清除历史日志,不推荐清理 -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 是否丢弃日志, 0 表示不丢弃。默认情况下,如果队列满 80%, 会丢弃 TRACE、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 本地 dev 开发环境 -->
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/> <!-- 输出控制台日志 -->
<appender-ref ref="ASYNC_FILE"/> <!-- 打印日志到文件中。PS: 本地环境下,如果不想打印日志到文件,可注释掉此行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/> <!-- 生产环境下,仅打印日志到文件中 -->
</root>
</springProfile>
</configuration>