若依框架结合自己的项目需要修改的几个地方

desc:com后面的xxx替换成自己的公司名

1. 安装 lombok(xxx-common/pom.xml)

1
2
3
4
5
6
<!-- lombok -->  
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</dependency>

2. mybatis 配置改为 mybatis-plus(配置文件中修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# MyBatis-plus配置 
mybatis-plus:
# 搜索指定包别名
typeAliasesPackage: com.xxx.**.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations:
- classpath*:mapper/**/*Mapper.xml
- classpath*:com/xxx/**/*Mapper.xml
- classpath*:com/xxx/**/**/*Mapper.xml
global-config:
db-config:
id-type: auto
banner: false
configuration:
map-underscore-to-camel-case: true

mybatis 配置改为 mybatis-plus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.xxx.framework.config;  

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
* Mybatis Plus 配置
* @author ruoyi
*/@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {

/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
return interceptor;
}

/**
* 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
*/ public PaginationInnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型为mysql
paginationInnerInterceptor.setDbType(DbType.MYSQL);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInnerInterceptor.setMaxLimit(-1L);
return paginationInnerInterceptor;
}
}

3. 运行环境从 maven 配置中取,打包时间

运行环境从 maven 配置中取

1
2
3
4
5
6
7
<!-- src/main/resources 启用 maven 变量注入, application.yml 注入 @profile.active@, @maven.build.timestamp@ 等变量 -->  
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 运行环境 -->  
<profiles>
<!-- 开发环境 -->
<profile>
<id>dev</id>
<activation> <activeByDefault>true</activeByDefault>
</activation>
<properties> <profile.active>dev</profile.active>
</properties>
</profile>
<!-- 测试环境 -->
<profile>
<id>test</id>
<properties> <profile.active>test</profile.active>
</properties>
</profile>
<!-- 正式环境 -->
<profile>
<id>prod</id>
<properties> <profile.active>prod</profile.active>
</properties>
</profile>
</profiles>

打包时间

1
2
3
4
5
6
7
# 环境  
spring:
profiles:
active: @profile.active@

# 打包时间
build.time: @maven.build.timestamp@

4. 打包后的 jar 包添加 环境名称

打包后的jar包添加环境名称

1
<finalName>${project.artifactId}-${profile.active}</finalName>

将 mybatis mapper.xml 也打包到 jar 包内

5. 系统日志查看页面

系统日志查看页面
系统日志查看页面

1
2
3
4
5
6
7
8
9
10
11
# 日志配置  
logging:
level:
com.xxx: debug
org.springframework: warn
file:
name: ./logs/bwg_admin.log
logback:
rollingpolicy:
max-history: 180
max-file-size: 100MB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package com.xxx.web.controller.monitor;  


import com.xxx.common.core.controller.BaseController;
import com.xxx.common.core.domain.AjaxResult;
import com.xxx.common.core.domain.model.LoginUser;
import com.xxx.common.exception.ServiceException;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;

import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
* LogFileController * @author xxx
*/@RestController
@RequestMapping("log")
public class LogFileController extends BaseController {

private final static String logDir = "./logs";

/**
* 日志列表
*/
@GetMapping("list")
public AjaxResult list() {
File dir = new File(logDir);
File[] logs = dir.listFiles();
if (Objects.isNull(logs) || logs.length == 0) {
return success();
}
List<FileVo> list = Arrays.stream(logs).map(LogFileController::toFileVo).sorted(Comparator.comparing(FileVo::lastModified).reversed()).toList();
AjaxResult success = success();
success.put("rows", list);
success.put("total", list.size());

success.put("totalSizeFormat", formatSize(list.stream().mapToLong(item -> item.fileSize).sum()));
success.put("freeSpace", formatSize(dir.getFreeSpace())); // 剩余磁盘空间
success.put("totalSpace", formatSize(dir.getTotalSpace())); // 总磁盘空间
return success;
}

/**
* 下载日志
* @param fileName 文件名
*/
@PostMapping("download")
public void downloadLog(@RequestParam String fileName, HttpServletResponse response) throws IOException {

LoginUser loginUser = getLoginUser();
if (!loginUser.getUser().isAdmin()) {
throw new ServiceException("权限不足");
}

// ⚠️ 固定日志目录,避免任意文件下载漏洞
File dir = new File(logDir);
File logFile = new File(dir, fileName);

// 1️⃣ 校验
if (!logFile.exists() || !logFile.isFile()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}

// 防止访问其他目录
if (!logFile.getCanonicalPath().startsWith(dir.getCanonicalPath())) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}

// 2️⃣ 设置响应头
response.setContentType("application/octet-stream");
response.setCharacterEncoding("UTF-8");

// 解决中文文件名问题
String encodedName = URLEncoder.encode(logFile.getName(), StandardCharsets.UTF_8).replaceAll("\\+", "%20");

response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);

// 3️⃣ 流式写出(不会一次性读入内存)
try (InputStream in = new FileInputStream(logFile);
OutputStream out = response.getOutputStream()) {

byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}

/**
* 打包下载, 自动把 gz 解压并打包进 zip
* @param files 英文冒号分隔的文件名字符串
*/
@PostMapping("download/zip")
public void downloadZip(@RequestParam String files, HttpServletResponse response) throws IOException {

LoginUser loginUser = getLoginUser();
if (!loginUser.getUser().isAdmin()) {
throw new ServiceException("权限不足");
}

File dir = new File(logDir);

response.setContentType("application/zip");
response.setCharacterEncoding("UTF-8");

String zipName = "logs-" + System.currentTimeMillis() + ".zip";
zipName = URLEncoder.encode(zipName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + zipName);

String[] fileArray = files.split(":");

try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {

for (String fileName : fileArray) {

File file = new File(dir, fileName);
if (!file.exists() || !file.isFile()) continue;

// 防路径穿越(重点)
if (!file.getCanonicalPath().startsWith(dir.getCanonicalPath())) {
continue;
}

// 判断是否是 .gz 文件
if (fileName.endsWith(".gz")) {
String targetName = fileName.substring(0, fileName.length() - 3); // 去掉 .gz zos.putNextEntry(new ZipEntry(targetName));

try (InputStream fis = new FileInputStream(file);
InputStream gis = new GZIPInputStream(fis)) {

byte[] buffer = new byte[8192];
int len;
while ((len = gis.read(buffer)) != -1) {
zos.write(buffer, 0, len);
}
}

zos.closeEntry();

} else {
// 普通文件直接写入 ZIP zos.putNextEntry(new ZipEntry(fileName));

try (InputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
zos.write(buffer, 0, len);
}
}

zos.closeEntry();
}
}
}
}

public record FileVo(String fileType, // 文件类型, 目录, 文件
long fileSize, // 文件大小
String fileSizeFormat, // 文件大小格式化
String fileName, // 文件名
String filePath, // 文件路径
Date lastModified // 最后修改时间
) {
}
public static FileVo toFileVo(File file) {
long fileSize = file.isFile() ? file.length() : 0L;
return new FileVo(
file.isDirectory() ? "目录" : "文件",
fileSize,
formatSize(fileSize),
file.getName(),
file.getAbsolutePath(),
new Date(file.lastModified())
);
}

/**
* 文件大小格式化
* @param size 文件大小
* @return 格式化后的文字
*/
public static String formatSize(long size) {
if (size < 1024) {
return size + " B";
}

double kb = size / 1024.0;
if (kb < 1024) {
return String.format("%.2f KB", kb);
}

double mb = kb / 1024.0;
if (mb < 1024) {
return String.format("%.2f MB", mb);
}

double gb = mb / 1024.0;
return String.format("%.2f GB", gb);
}
}

6. jackson日期格式化,token默认令牌修改

jackson日期格式化,token默认令牌修改

1
2
3
4
5
6
7
8
9
10
11
jackson:  
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# token配置
token:
# 令牌自定义标识
header: xxx
# 令牌密钥
secret: xxx
# 令牌有效期(默认xxx分钟)
expireTime: xxx

7. java版本修改为21, 全局异常拦截器增强

java版本修改为21, 全局异常拦截器增强

1
<java.version>21</java.version>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package com.xxx.framework.web.exception;  

import com.xxx.common.constant.HttpStatus;
import com.xxx.common.core.domain.AjaxResult;
import com.xxx.common.core.text.Convert;
import com.xxx.common.exception.ServiceException;
import com.xxx.common.utils.StringUtils;
import com.xxx.common.utils.html.EscapeUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.sql.SQLIntegrityConstraintViolationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* 全局异常处理器
* @author ruoyi
*/@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

/**
* 权限校验异常
*/
@ExceptionHandler(AccessDeniedException.class)
public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
}

/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return AjaxResult.error(e.getMessage());
}

/**
* 业务异常
*/
@ExceptionHandler(ServiceException.class)
public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) {
log.error(e.getMessage(), e);
Integer code = e.getCode();
return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
}

/**
* 请求路径中缺少必需的路径变量
*/
@ExceptionHandler(MissingPathVariableException.class)
public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
}

/**
* 请求参数类型不匹配
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String value = Convert.toStr(e.getValue());
if (StringUtils.isNotEmpty(value)) {
value = EscapeUtil.clean(value);
}
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
}

/**
* key 重复异常
*/
@ExceptionHandler(DuplicateKeyException.class)
public AjaxResult DuplicateKeyException(SQLIntegrityConstraintViolationException e, HttpServletRequest request) {
return AjaxResult.error(e.getMessage());
}

/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
}

@ExceptionHandler(DataIntegrityViolationException.class)
public AjaxResult DataIntegrityViolationException(DataIntegrityViolationException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',数据库约束异常", requestURI, e);

String message = e.getMostSpecificCause().getMessage();

return AjaxResult.error(message);
}

/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
}

/**
* 自定义验证异常
*/
@ExceptionHandler(BindException.class)
public AjaxResult handleBindException(BindException e, HttpServletRequest request) {

String requestURI = request.getRequestURI();
log.warn("请求地址: {}, 参数校验失败", requestURI, e);

String collect = e.getBindingResult().getAllErrors().stream().map(objectError -> switch (objectError) {
case FieldError fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage();
default -> objectError.getObjectName() + objectError.getDefaultMessage();
}).collect(Collectors.joining("; "));

return AjaxResult.error(collect);
}

/**
* 参数校验异常
*/
@ExceptionHandler(HandlerMethodValidationException.class)
public AjaxResult handlerMethodValidationException(HandlerMethodValidationException e, HttpServletRequest request) {

String requestURI = request.getRequestURI();
log.warn("请求地址: {}, 参数校验失败", requestURI, e);

List<String> errors = new ArrayList<>();

e.getValueResults().forEach(result -> result.getResolvableErrors().forEach(error -> {
if (Objects.isNull(error.getArguments()) || error.getArguments().length == 0) {
errors.add(error.getDefaultMessage());
return;
}
for (Object argument : error.getArguments()) {
if (argument instanceof DefaultMessageSourceResolvable defaultMessageSourceResolvable) {
errors.add(defaultMessageSourceResolvable.getDefaultMessage() + " " + error.getDefaultMessage());
}
}
}));
return AjaxResult.error(errors.isEmpty() ? "参数校验失败" : String.join("; ", errors));
}

/**
* 资源未找到
*/
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(org.springframework.http.HttpStatus.NOT_FOUND)
public AjaxResult noResourceFoundException(NoResourceFoundException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
return AjaxResult.error(404, "资源未找到: " + requestURI);
}
}