Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b8b90af04 | |||
| 690e9288b8 | |||
| 2d93f70704 | |||
| 9f74a467ba | |||
| 4b36e35ace | |||
| 40f07bdd6e | |||
| 71ca306336 | |||
| 5274830cc6 | |||
| 3b6c8bedf6 | |||
| b37da657fa | |||
| 1931697a87 | |||
|
|
b882e85134 | ||
| e17e31edfd | |||
| bb99e48275 | |||
| 7d8cda234e |
88
README.md
88
README.md
@@ -1,31 +1,77 @@
|
|||||||
# fmt
|
# fmt
|
||||||
|
|
||||||
#### 介绍
|
## 介绍
|
||||||
1. 简易文件传输管理系统
|
|
||||||
2. 文件下载、文件上传、在线日志
|
|
||||||
|
|
||||||
#### 软件架构
|
`fmt` 是一个基于 Spring Boot 的文件管理工具,旨在提供便捷的文件上传、下载、打包、删除等操作。它结合了简洁的前端界面与强大的后端功能,适用于需要进行文件管理的各类应用场景。
|
||||||
1. Jdk 21
|
|
||||||
2. Maven 3.6.3
|
|
||||||
3. SpringBoot 3.5.3
|
|
||||||
4. SpringBoot Starter Thymeleaf 3.5.3
|
|
||||||
5. SpringBoot Starter WebSocket 3.5.3
|
|
||||||
7. Hutool 5.8.25
|
|
||||||
7. AXUI 2.1.1
|
|
||||||
|
|
||||||
|
## 软件架构
|
||||||
|
|
||||||
#### 安装教程
|
本项目采用前后端一体化架构,后端基于 Spring Boot 框架,使用 Java 语言开发,前端采用 HTML、CSS 和 JavaScript 技术,结合 `axui` 框架进行界面渲染。主要模块包括:
|
||||||
|
|
||||||
1. 项目导入IDEA,配置Jdk、Maven,根据自己实际环境修改配置文件,然后启动项目
|
- **文件上传模块**:支持文件上传并保存至服务器。
|
||||||
2. 下载发行版本压缩包,解压后,修改bin目录下的启动文件配置,然后执行即可
|
- **文件下载模块**:支持文件下载及 ZIP 打包下载。
|
||||||
|
- **文件管理模块**:支持文件删除、批量删除等操作。
|
||||||
|
- **日志模块**:实时推送日志信息。
|
||||||
|
- **WebSocket 模块**:用于前后端实时通信。
|
||||||
|
- **工具类模块**:包括 MD5 工具、操作系统工具等。
|
||||||
|
|
||||||
#### 使用说明
|
## 安装教程
|
||||||
|
|
||||||
1. 启动项目,访问http://127.0.0.1:8098/fmt
|
1. **克隆项目**:
|
||||||
|
```bash
|
||||||
|
git clone https://gitee.com/thzxx/fmt.git
|
||||||
|
```
|
||||||
|
|
||||||
#### 参与贡献
|
2. **构建项目**:
|
||||||
|
使用 Maven 构建项目:
|
||||||
|
```bash
|
||||||
|
cd fmt
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
1. Fork 本仓库
|
3. **运行项目**:
|
||||||
2. 新建 Feat_xxx 分支
|
启动 Spring Boot 应用:
|
||||||
3. 提交代码
|
```bash
|
||||||
4. 新建 Pull Request
|
java -jar target/fmt.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **访问项目**:
|
||||||
|
打开浏览器访问:
|
||||||
|
```
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
- **上传文件**:
|
||||||
|
访问 `/upload/index` 页面,选择文件进行上传。
|
||||||
|
|
||||||
|
- **下载文件**:
|
||||||
|
访问 `/download/index` 页面,点击文件进行下载,或使用 `/download/file` 接口下载指定路径文件。
|
||||||
|
|
||||||
|
- **打包下载**:
|
||||||
|
使用 `/download/packZip` 接口,传入文件名列表进行 ZIP 打包下载。
|
||||||
|
|
||||||
|
- **删除文件**:
|
||||||
|
使用 `/download/delete` 或 `/download/batchDel` 接口进行单个或批量删除。
|
||||||
|
|
||||||
|
- **查看日志**:
|
||||||
|
访问 `/logging/index` 页面,实时查看日志信息。
|
||||||
|
|
||||||
|
- **WebSocket 实时通信**:
|
||||||
|
前端通过 WebSocket 连接 `/ws`,接收服务器推送的消息。
|
||||||
|
|
||||||
|
## 参与贡献
|
||||||
|
|
||||||
|
欢迎贡献代码和改进项目。请遵循以下步骤:
|
||||||
|
|
||||||
|
1. Fork 项目。
|
||||||
|
2. 创建新分支。
|
||||||
|
3. 提交代码。
|
||||||
|
4. 创建 Pull Request。
|
||||||
|
|
||||||
|
如发现 Bug 或有改进建议,请提交 Issue 或直接联系项目维护者。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目采用 MIT 许可证。详情请查看项目根目录下的 `LICENSE` 文件。
|
||||||
8
pom.xml
8
pom.xml
@@ -10,7 +10,7 @@
|
|||||||
</parent>
|
</parent>
|
||||||
<groupId>cn.somkit</groupId>
|
<groupId>cn.somkit</groupId>
|
||||||
<artifactId>fmt</artifactId>
|
<artifactId>fmt</artifactId>
|
||||||
<version>2.0.0</version>
|
<version>2.1.3</version>
|
||||||
<name>fmt</name>
|
<name>fmt</name>
|
||||||
<description>File Manage System for by SpringBoot</description>
|
<description>File Manage System for by SpringBoot</description>
|
||||||
<properties>
|
<properties>
|
||||||
@@ -51,6 +51,12 @@
|
|||||||
<artifactId>metona-cache-spring-boot-starter</artifactId>
|
<artifactId>metona-cache-spring-boot-starter</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.metona</groupId>
|
||||||
|
<artifactId>metona-mq-mini-pro</artifactId>
|
||||||
|
<version>2.0.1</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package cn.somkit.fmt;
|
package cn.somkit.fmt;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class FmtApplication {
|
public class FmtApplication {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FmtApplication.class);
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(FmtApplication.class, args);
|
SpringApplication.run(FmtApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package cn.somkit.fmt.action;
|
package cn.somkit.fmt.action;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import cn.somkit.fmt.utils.OsInfoUtil;
|
import cn.somkit.fmt.utils.OsInfoUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -31,6 +34,8 @@ import java.util.zip.ZipOutputStream;
|
|||||||
@RequestMapping("/download")
|
@RequestMapping("/download")
|
||||||
public class DownloadAction {
|
public class DownloadAction {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
@Value("${somkit.upload.path.windows}")
|
@Value("${somkit.upload.path.windows}")
|
||||||
private String windows_path;
|
private String windows_path;
|
||||||
|
|
||||||
@@ -41,6 +46,7 @@ public class DownloadAction {
|
|||||||
public ModelAndView index(String keyboard) throws Exception{
|
public ModelAndView index(String keyboard) throws Exception{
|
||||||
String path = OsInfoUtil.isWindows() ? windows_path :
|
String path = OsInfoUtil.isWindows() ? windows_path :
|
||||||
OsInfoUtil.isLinux() ? linux_path : null;
|
OsInfoUtil.isLinux() ? linux_path : null;
|
||||||
|
logger.info("获取文件存储路径:{}", path);
|
||||||
assert path != null;
|
assert path != null;
|
||||||
File folder = new File(path);
|
File folder = new File(path);
|
||||||
File[] listOfFiles = folder.listFiles();
|
File[] listOfFiles = folder.listFiles();
|
||||||
@@ -59,6 +65,7 @@ public class DownloadAction {
|
|||||||
list = list.stream().filter(map -> String.valueOf(map.get("filename"))
|
list = list.stream().filter(map -> String.valueOf(map.get("filename"))
|
||||||
.contains(keyboard)).collect(Collectors.toList());
|
.contains(keyboard)).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
logger.info("关键词:{},文件数量:{}", keyboard, list.size());
|
||||||
ModelAndView mv = new ModelAndView();
|
ModelAndView mv = new ModelAndView();
|
||||||
mv.setViewName("download");
|
mv.setViewName("download");
|
||||||
mv.addObject("files", list);
|
mv.addObject("files", list);
|
||||||
@@ -66,7 +73,7 @@ public class DownloadAction {
|
|||||||
return mv;
|
return mv;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, Object> getObjectMap(File file) {
|
private Map<String, Object> getObjectMap(File file) {
|
||||||
BigDecimal filesize = BigDecimal.valueOf(file.length())
|
BigDecimal filesize = BigDecimal.valueOf(file.length())
|
||||||
.divide(BigDecimal.valueOf(1024 * 1024), 2, RoundingMode.HALF_UP);
|
.divide(BigDecimal.valueOf(1024 * 1024), 2, RoundingMode.HALF_UP);
|
||||||
Long time = file.lastModified();
|
Long time = file.lastModified();
|
||||||
@@ -77,6 +84,7 @@ public class DownloadAction {
|
|||||||
map.put("filepath", file.getAbsolutePath());
|
map.put("filepath", file.getAbsolutePath());
|
||||||
map.put("filesize", filesize);
|
map.put("filesize", filesize);
|
||||||
map.put("filetime", filetime);
|
map.put("filetime", filetime);
|
||||||
|
logger.info("文件信息:{}", JSONUtil.toJsonStr(map));
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +110,7 @@ public class DownloadAction {
|
|||||||
}
|
}
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
logger.error("IO错误:", e);
|
||||||
throw new RuntimeException("IO错误: " + e.getMessage());
|
throw new RuntimeException("IO错误: " + e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
// 尝试在流关闭后删除文件
|
// 尝试在流关闭后删除文件
|
||||||
@@ -111,7 +120,7 @@ public class DownloadAction {
|
|||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// 记录日志或采取其他措施
|
// 记录日志或采取其他措施
|
||||||
System.err.println("删除文件失败: " + e.getMessage());
|
logger.error("删除文件失败:{}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
package cn.somkit.fmt.action;
|
package cn.somkit.fmt.action;
|
||||||
|
|
||||||
import cn.metona.cache.Cache;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/logging")
|
@RequestMapping("/logging")
|
||||||
public class LoggingAction {
|
public class LoggingAction {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private Cache<String, Object> cache;
|
|
||||||
|
|
||||||
@GetMapping("/index")
|
@GetMapping("/index")
|
||||||
public String index() throws Exception{
|
public String index() throws Exception{
|
||||||
return "logging";
|
return "logging";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResponseBody
|
|
||||||
@PostMapping("/close")
|
|
||||||
public void close(Boolean closed) throws Exception {
|
|
||||||
cache.put("closed", closed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package cn.somkit.fmt.action;
|
|||||||
|
|
||||||
import cn.somkit.fmt.utils.OsInfoUtil;
|
import cn.somkit.fmt.utils.OsInfoUtil;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -18,6 +20,8 @@ import java.util.*;
|
|||||||
@RequestMapping("/upload")
|
@RequestMapping("/upload")
|
||||||
public class UploadAction {
|
public class UploadAction {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
@Value("${somkit.upload.path.windows}")
|
@Value("${somkit.upload.path.windows}")
|
||||||
private String windows_path;
|
private String windows_path;
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ public class UploadAction {
|
|||||||
try {
|
try {
|
||||||
this.saveFile(file); // 保存上传信息
|
this.saveFile(file); // 保存上传信息
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("文件保存失败:{}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +60,7 @@ public class UploadAction {
|
|||||||
* @throws Exception 上传异常
|
* @throws Exception 上传异常
|
||||||
*/
|
*/
|
||||||
public void saveFile(MultipartFile file) throws Exception {
|
public void saveFile(MultipartFile file) throws Exception {
|
||||||
|
try {
|
||||||
String path = OsInfoUtil.isWindows() ? windows_path :
|
String path = OsInfoUtil.isWindows() ? windows_path :
|
||||||
OsInfoUtil.isLinux() ? linux_path : null;
|
OsInfoUtil.isLinux() ? linux_path : null;
|
||||||
assert path != null;
|
assert path != null;
|
||||||
@@ -64,6 +69,10 @@ public class UploadAction {
|
|||||||
filePath = path + File.separator + file.getOriginalFilename();
|
filePath = path + File.separator + file.getOriginalFilename();
|
||||||
File saveFile = new File(filePath) ;
|
File saveFile = new File(filePath) ;
|
||||||
file.transferTo(saveFile); // 文件保存
|
file.transferTo(saveFile); // 文件保存
|
||||||
|
logger.info("文件保存成功:{}", filePath);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/main/java/cn/somkit/fmt/config/LogMonitorAppender.java
Normal file
35
src/main/java/cn/somkit/fmt/config/LogMonitorAppender.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package cn.somkit.fmt.config;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||||
|
import ch.qos.logback.core.UnsynchronizedAppenderBase;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import cn.metona.mq.MQToolkit;
|
||||||
|
import cn.metona.mq.core.MessageQueue;
|
||||||
|
import cn.somkit.fmt.entity.LoggerMessage;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
|
||||||
|
public class LogMonitorAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
|
||||||
|
|
||||||
|
private static final String Queue_Name = "log-monitor";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void append(ILoggingEvent event) {
|
||||||
|
LoggerMessage msg = new LoggerMessage(
|
||||||
|
event.getLevel().toString(),
|
||||||
|
event.getThreadName(),
|
||||||
|
event.getLoggerName(),
|
||||||
|
event.getFormattedMessage(),
|
||||||
|
DateUtil.format(Instant.ofEpochMilli(event.getTimeStamp()).atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
||||||
|
"yyyy-MM-dd HH:mm:ss.SSS"),
|
||||||
|
ProcessHandle.current().pid()
|
||||||
|
);
|
||||||
|
//发送日志信息到日志监控队列
|
||||||
|
MessageQueue.QueueStats queueStats = MQToolkit.getQueueStats(Queue_Name);
|
||||||
|
if(queueStats != null && queueStats.isRunning() && !queueStats.isPaused()){
|
||||||
|
MQToolkit.sendMessage(Queue_Name, "log.monitor", JSONUtil.toJsonStr(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/java/cn/somkit/fmt/config/LogMonitorConfig.java
Normal file
50
src/main/java/cn/somkit/fmt/config/LogMonitorConfig.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package cn.somkit.fmt.config;
|
||||||
|
|
||||||
|
import cn.metona.mq.MQToolkit;
|
||||||
|
import cn.metona.mq.consumer.MessageConsumer;
|
||||||
|
import cn.metona.mq.utils.MonitorUtils;
|
||||||
|
import cn.somkit.fmt.socket.SocketManage;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class LogMonitorConfig {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
private static final String Queue_Name = "log-monitor";
|
||||||
|
|
||||||
|
private static final String Consumer_Id = "log-monitor-consumer";
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void run(){
|
||||||
|
//创建日志监控队列
|
||||||
|
MQToolkit.createQueue(Queue_Name, Boolean.FALSE);
|
||||||
|
//创建日志监控消费者
|
||||||
|
MessageConsumer consumer = MQToolkit.createConsumer(Consumer_Id, Queue_Name, message -> {
|
||||||
|
List<WebSocketSession> list = SocketManage.all();
|
||||||
|
if(list != null && !list.isEmpty()){
|
||||||
|
list.forEach(session -> {
|
||||||
|
if(session.isOpen()){
|
||||||
|
try {
|
||||||
|
session.sendMessage(new TextMessage(message.body()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("发送消息失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//启动消费者
|
||||||
|
consumer.start();
|
||||||
|
// 显示队列统计
|
||||||
|
MonitorUtils.printQueueStats(Queue_Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package cn.somkit.fmt.config;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Logger;
|
||||||
|
import ch.qos.logback.classic.LoggerContext;
|
||||||
|
import jakarta.servlet.ServletContextEvent;
|
||||||
|
import jakarta.servlet.ServletContextListener;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class LogbackConfigListener implements ServletContextListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void contextInitialized(ServletContextEvent event) {
|
||||||
|
// 配置logback
|
||||||
|
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||||
|
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||||
|
|
||||||
|
// 添加自定义appender
|
||||||
|
LogMonitorAppender appender = new LogMonitorAppender();
|
||||||
|
appender.setContext(loggerContext);
|
||||||
|
appender.start();
|
||||||
|
rootLogger.addAppender(appender);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
package cn.somkit.fmt.entity;
|
package cn.somkit.fmt.entity;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
public class LoggerMessage {
|
public class LoggerMessage {
|
||||||
String level;
|
String level;
|
||||||
String loggerName;
|
String threadName;
|
||||||
|
String name;
|
||||||
String message;
|
String message;
|
||||||
String timestamp;
|
String timestamp;
|
||||||
|
long number;
|
||||||
|
|
||||||
public LoggerMessage(String level, String loggerName, String message, String timestamp) {
|
public LoggerMessage(String level, String threadName, String loggerName, String message, String timestamp, long number) {
|
||||||
this.level = level;
|
this.level = level;
|
||||||
this.loggerName = loggerName;
|
this.threadName = threadName;
|
||||||
|
this.name = loggerName;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
|
this.number = number;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLevel() {
|
public String getLevel() {
|
||||||
@@ -23,12 +25,20 @@ public class LoggerMessage {
|
|||||||
this.level = level;
|
this.level = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLoggerName() {
|
public String getThreadName() {
|
||||||
return loggerName;
|
return threadName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoggerName(String loggerName) {
|
public void setThreadName(String threadName) {
|
||||||
this.loggerName = loggerName;
|
this.threadName = threadName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
@@ -46,4 +56,12 @@ public class LoggerMessage {
|
|||||||
public void setTimestamp(String timestamp) {
|
public void setTimestamp(String timestamp) {
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getNumber() {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNumber(long number) {
|
||||||
|
this.number = number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package cn.somkit.fmt.filter;
|
|
||||||
|
|
||||||
import ch.qos.logback.classic.Level;
|
|
||||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
|
||||||
import ch.qos.logback.core.filter.Filter;
|
|
||||||
import ch.qos.logback.core.spi.FilterReply;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.somkit.fmt.entity.LoggerMessage;
|
|
||||||
import cn.somkit.fmt.utils.LoggerQueue;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
|
|
||||||
public class LogStashFilter extends Filter<ILoggingEvent> {
|
|
||||||
|
|
||||||
Level level;
|
|
||||||
|
|
||||||
public LogStashFilter() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FilterReply decide(ILoggingEvent e) {
|
|
||||||
LoggerMessage msg = new LoggerMessage(
|
|
||||||
e.getLevel().toString(),
|
|
||||||
e.getLoggerName(),
|
|
||||||
e.getFormattedMessage(),
|
|
||||||
DateUtil.format(Instant.ofEpochMilli(e.getTimeStamp()).atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
|
||||||
"yyyy-MM-dd HH:mm:ss.SSS")
|
|
||||||
);
|
|
||||||
LoggerQueue.getInstance().push(msg); // 单例阻塞队列
|
|
||||||
return FilterReply.NEUTRAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLevel(String level) {
|
|
||||||
this.level = Level.toLevel(level);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
if (this.level != null) {
|
|
||||||
super.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
src/main/java/cn/somkit/fmt/socket/SocketManage.java
Normal file
31
src/main/java/cn/somkit/fmt/socket/SocketManage.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package cn.somkit.fmt.socket;
|
||||||
|
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class SocketManage {
|
||||||
|
|
||||||
|
private SocketManage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<String, WebSocketSession> onlineMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static void put(WebSocketSession session){
|
||||||
|
onlineMap.put(session.getId(), session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebSocketSession get(String sessionId){
|
||||||
|
return onlineMap.get(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void remove(String sessionId){
|
||||||
|
onlineMap.remove(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<WebSocketSession> all(){
|
||||||
|
return onlineMap.values().stream().toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,53 +1,29 @@
|
|||||||
package cn.somkit.fmt.socket;
|
package cn.somkit.fmt.socket;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import cn.metona.cache.Cache;
|
|
||||||
import cn.somkit.fmt.entity.LoggerMessage;
|
|
||||||
import cn.somkit.fmt.utils.LoggerQueue;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.socket.*;
|
import org.springframework.web.socket.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class WebSocketServerHandler implements WebSocketHandler {
|
public class WebSocketServerHandler implements WebSocketHandler {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private Cache<String, Object> cache;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||||
push(session);
|
SocketManage.put(session);
|
||||||
}
|
|
||||||
|
|
||||||
private void push(WebSocketSession session) throws IOException {
|
|
||||||
while (StrUtil.isBlankIfStr(cache.get("closed")) || !Boolean.parseBoolean(String.valueOf(cache.get("closed")))) {
|
|
||||||
LoggerMessage log = LoggerQueue.getInstance().poll();
|
|
||||||
if(log != null){
|
|
||||||
session.sendMessage(new TextMessage(JSONUtil.toJsonStr(log)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
||||||
push(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
||||||
|
SocketManage.remove(session.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||||
|
SocketManage.remove(session.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package cn.somkit.fmt.utils;
|
|
||||||
|
|
||||||
import cn.somkit.fmt.entity.LoggerMessage;
|
|
||||||
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
|
|
||||||
public final class LoggerQueue {
|
|
||||||
private static final LoggerQueue INSTANCE = new LoggerQueue();
|
|
||||||
private final BlockingQueue<LoggerMessage> queue = new LinkedBlockingQueue<>(100000);
|
|
||||||
|
|
||||||
public static LoggerQueue getInstance() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void push(LoggerMessage msg) {
|
|
||||||
queue.offer(msg); // 非阻塞插入
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoggerMessage poll() {
|
|
||||||
try {
|
|
||||||
return queue.take(); // 阻塞直到有数据
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
<!-- 日志输出到控制台 -->
|
<!-- 日志输出到控制台 -->
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<filter class="cn.somkit.fmt.filter.LogStashFilter">
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
<level>DEBUG</level>
|
<level>INFO</level>
|
||||||
</filter>
|
</filter>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<!-- 指定日志输出级别,以及启动的Appender -->
|
<!-- 指定日志输出级别,以及启动的Appender -->
|
||||||
<root level="DEBUG">
|
<root level="INFO">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
</root>
|
</root>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class LogMonitorAdaptive {
|
|||||||
timeFormat: 'HH:mm:ss.SSS', // 时间戳格式,可自定义
|
timeFormat: 'HH:mm:ss.SSS', // 时间戳格式,可自定义
|
||||||
|
|
||||||
/* --- 日志级别 & 过滤 --- */
|
/* --- 日志级别 & 过滤 --- */
|
||||||
levels: ['debug', 'info', 'warn', 'error', 'success', 'system'], // 可用日志级别
|
levels: ['DEBUG', 'INFO', 'WARN', 'ERROR', 'SUCCESS', 'SYSTEM'], // 可用日志级别
|
||||||
// 过滤功能基于 levels;如只想显示 info/warn,可在这里删减
|
// 过滤功能基于 levels;如只想显示 info/warn,可在这里删减
|
||||||
|
|
||||||
/* --- 功能开关 --- */
|
/* --- 功能开关 --- */
|
||||||
@@ -51,7 +51,7 @@ class LogMonitorAdaptive {
|
|||||||
|
|
||||||
/* 强制确保 levels 是数组,防止 forEach 报错 */
|
/* 强制确保 levels 是数组,防止 forEach 报错 */
|
||||||
if (!Array.isArray(this.cfg.levels)) {
|
if (!Array.isArray(this.cfg.levels)) {
|
||||||
this.cfg.levels = ['info', 'warn', 'error'];
|
this.cfg.levels = ['INFO', 'WARN', 'ERROR'];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isPaused = false;
|
this.isPaused = false;
|
||||||
@@ -309,7 +309,7 @@ class LogMonitorAdaptive {
|
|||||||
debug: '#9c27b0', info: '#2196f3', warn: '#ff9800',
|
debug: '#9c27b0', info: '#2196f3', warn: '#ff9800',
|
||||||
error: '#f44336', success: '#4caf50', system: '#00bcd4'
|
error: '#f44336', success: '#4caf50', system: '#00bcd4'
|
||||||
};
|
};
|
||||||
lvlSpan.style.color = colors[entry.level] || colors.info;
|
lvlSpan.style.color = colors[entry.level.toLowerCase()] || colors.info;
|
||||||
lvlSpan.style.minWidth = '70px';
|
lvlSpan.style.minWidth = '70px';
|
||||||
lvlSpan.style.fontWeight = 'bold';
|
lvlSpan.style.fontWeight = 'bold';
|
||||||
line.appendChild(lvlSpan);
|
line.appendChild(lvlSpan);
|
||||||
|
|||||||
@@ -31,14 +31,8 @@
|
|||||||
showLevel: true, // 是否显示日志级别标签
|
showLevel: true, // 是否显示日志级别标签
|
||||||
wordWrap: true, // 日志内容是否自动换行(true=换行,false=横向滚动)
|
wordWrap: true, // 日志内容是否自动换行(true=换行,false=横向滚动)
|
||||||
//暂停/继续 回调函数
|
//暂停/继续 回调函数
|
||||||
onTogglePause: async (isPaused) => {
|
onTogglePause: (isPaused) => {
|
||||||
const options = {
|
|
||||||
url: Fmt.ctx() + '/logging/close',
|
|
||||||
data: {closed: isPaused},
|
|
||||||
method: 'post'
|
|
||||||
};
|
|
||||||
await Fmt.axios(options).then((result) => console.log(result)).catch((err) => console.error(err));
|
|
||||||
ws.send('发送日志');
|
|
||||||
},
|
},
|
||||||
onCreated: () => {
|
onCreated: () => {
|
||||||
console.log('日志容器已创建');
|
console.log('日志容器已创建');
|
||||||
@@ -56,7 +50,8 @@
|
|||||||
ws.onmessage = function (event) {
|
ws.onmessage = function (event) {
|
||||||
if(event.data){
|
if(event.data){
|
||||||
let data = JSON.parse(event.data);
|
let data = JSON.parse(event.data);
|
||||||
logger.log(data.loggerName + ' : ' + data.message, data.level, data.timestamp);
|
let message = `${data.number} --- [ ${data.threadName} ] <span style="color: #198cff;">${data.name}</span> : ${data.message}`;
|
||||||
|
logger.log(message, data.level, data.timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,3 +27,23 @@
|
|||||||
重构在线日志页面及实现方式,不再使用读取日志文件方式,自定义日志拦截器实时获取日志
|
重构在线日志页面及实现方式,不再使用读取日志文件方式,自定义日志拦截器实时获取日志
|
||||||
不再生成自定义日志文件,日志打印从INFO改为DEBUG,打印更详细的内容
|
不再生成自定义日志文件,日志打印从INFO改为DEBUG,打印更详细的内容
|
||||||
```
|
```
|
||||||
|
> v2.0.1
|
||||||
|
常规BUG修复,参数名称修改
|
||||||
|
> v2.1.0
|
||||||
|
```
|
||||||
|
引入metona-mq-mini-pro消息队列,重构实时日志获取方式
|
||||||
|
```
|
||||||
|
> v2.1.1
|
||||||
|
```
|
||||||
|
升级metona-mq-mini-pro到2.0.0,重构实时日志获取方式
|
||||||
|
```
|
||||||
|
> v2.1.2
|
||||||
|
```
|
||||||
|
升级metona-mq-mini-pro到2.0.1
|
||||||
|
```
|
||||||
|
> v2.1.3
|
||||||
|
```
|
||||||
|
使用ServletContextListener方式获取实时日志
|
||||||
|
页面日志要素添加进程id和线程名称,优化样式
|
||||||
|
日志打印级别调整为INFO
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user