15 Commits

Author SHA1 Message Date
4ce49e1205 v2.0.0 更新:
1、SpringBoot从3.1.1升级到3.5.3
    2、JDK从17升级到21并开启虚拟线程
    3、删除RocksDB相关配置,不再使用该缓存方案
    4、修改文件下载方式,使用StreamingResponseBody,支持大文件下载
    5、引入metona-cache-spring-boot-starter,使用此缓存方案
    6、重构在线日志页面及实现方式,不再使用读取日志文件方式,自定义日志拦截器实时获取日志
    7、不再生成自定义日志文件,日志打印从INFO改为DEBUG,打印更详细的内容
2025-07-18 15:29:26 +08:00
abfbc181b3 Merge remote-tracking branch 'origin/master' into develop
# Conflicts:
#	src/main/java/cn/somkit/fmt/action/DownloadAction.java
#	src/main/java/cn/somkit/fmt/action/UploadAction.java
#	src/main/java/cn/somkit/fmt/config/LogSocketConfig.java
#	src/main/java/cn/somkit/fmt/config/RocksDBConfig.java
#	src/main/java/cn/somkit/fmt/utils/LogSocketUtils.java
#	src/main/java/cn/somkit/fmt/utils/RocksDBUtils.java
#	src/main/resources/application.yml
#	src/main/resources/templates/logging.html
2025-07-18 15:00:42 +08:00
d48bf42532 代码提交 2025-07-18 14:59:07 +08:00
3334aa23d9 代码提交 2025-07-18 11:18:45 +08:00
515fb83408 代码提交 2025-07-18 09:32:46 +08:00
d6383ac74d 代码提交 2025-07-17 23:37:44 +08:00
3d3fe0cd96 代码提交 2025-07-17 23:00:52 +08:00
7400f85c88 代码提交 2025-07-17 21:11:57 +08:00
b336a78183 代码提交 2025-07-17 18:22:19 +08:00
WIN-VSNMD38DUOC\Administrator
2e0f8e82a8 修改文件下载方式,使用StreamingResponseBody,支持大文件下载 2024-12-06 17:29:36 +08:00
WIN-VSNMD38DUOC\Administrator
2ed8e1159c 修改版本号,修改打包参数 2024-05-09 17:58:53 +08:00
WIN-VSNMD38DUOC\Administrator
e5872de15b 新增系统设置页面,支持在线配置上传文件地址、日志文件地址等参数 2024-04-30 11:13:42 +08:00
WIN-VSNMD38DUOC\Administrator
e2d5ad6d1a 新增临时文件存放地址配置,支持相对路径 2024-04-28 17:42:15 +08:00
WIN-VSNMD38DUOC\Administrator
5f3e17f0cf 修改上传文件目录配置,使其支持使用相对路径 2024-04-28 16:50:07 +08:00
WIN-VSNMD38DUOC\Administrator
3e8bc50d1b 修改RocksDB文件目录配置,使其支持使用相对路径 2024-04-28 15:13:16 +08:00
35 changed files with 864 additions and 1243 deletions

View File

@@ -5,13 +5,12 @@
2. 文件下载、文件上传、在线日志
#### 软件架构
1. Jdk 17
1. Jdk 21
2. Maven 3.6.3
3. SpringBoot 3.1.1
4. SpringBoot Starter Thymeleaf 3.1.1
5. SpringBoot Starter WebSocket 3.1.1
6. RocksDB 8.3.2
7. Hutool Json 5.8.21
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

42
pom.xml
View File

@@ -5,53 +5,51 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.1</version>
<version>3.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.somkit</groupId>
<artifactId>fmt</artifactId>
<version>1.2.0</version>
<version>2.0.0</version>
<name>fmt</name>
<description>fmt</description>
<description>File Manage System for by SpringBoot</description>
<properties>
<java.version>17</java.version>
<java.version>21</java.version>
</properties>
<repositories>
<repository>
<id>metona-maven</id>
<url>https://gitee.com/thzxx/maven/raw/master</url>
</repository>
</repositories>
<dependencies>
<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.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>8.3.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.21</version>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>cn.metona</groupId>
<artifactId>metona-cache-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

View File

@@ -1,25 +1,26 @@
package cn.somkit.fmt.action;
import cn.somkit.fmt.annotation.ApiOperate;
import cn.somkit.fmt.config.RocksDBConfig;
import cn.somkit.fmt.utils.MD5Utils;
import cn.somkit.fmt.utils.OsInfoUtil;
import cn.somkit.fmt.utils.RocksDBUtils;
import jakarta.servlet.http.HttpServletResponse;
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.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@@ -37,7 +38,6 @@ public class DownloadAction {
private String linux_path;
@GetMapping("/index")
@ApiOperate(description = "下载页面跳转")
public ModelAndView index(String keyboard) throws Exception{
String path = OsInfoUtil.isWindows() ? windows_path :
OsInfoUtil.isLinux() ? linux_path : null;
@@ -45,48 +45,15 @@ public class DownloadAction {
File folder = new File(path);
File[] listOfFiles = folder.listFiles();
List<Map<String, Object>> list = new ArrayList<>();
List<String> keys = RocksDBUtils.getAllKey(RocksDBConfig.RocksDB_Column_Family);
if(listOfFiles != null){
for (File file : listOfFiles) {
if(!file.isFile()){
continue;
}
if(!keys.contains(file.getName())){
try (FileInputStream is = new FileInputStream(file)) {
String hash = MD5Utils.md5HashCode(is);
RocksDBUtils.put(RocksDBConfig.RocksDB_Column_Family, file.getName(), hash);
}catch (Exception e){e.printStackTrace();}
}
BigDecimal filesize = BigDecimal.valueOf(file.length())
.divide(BigDecimal.valueOf(1024 * 1024), 2, RoundingMode.HALF_UP);
Long time = file.lastModified();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String filetime = format.format(time);
Map<String, Object> map = new HashMap<>();
map.put("filename", file.getName());
map.put("filepath", file.getAbsolutePath());
map.put("filesize", filesize);
map.put("filetime", filetime);
Map<String, Object> map = getObjectMap(file);
list.add(map);
}
List<String> filenames = list.stream().map(map -> String.valueOf(map.get("filename"))).toList();
keys.forEach(key -> {
try {
if(!filenames.contains(key)){
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, key);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}else{
keys.forEach(key -> {
try {
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, key);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
if(StringUtils.hasText(keyboard) && !CollectionUtils.isEmpty(list)){
list = list.stream().filter(map -> String.valueOf(map.get("filename"))
@@ -99,37 +66,62 @@ public class DownloadAction {
return mv;
}
@RequestMapping("/execute")
@ApiOperate(description = "单文件下载")
public void execute(String path, Boolean temp, HttpServletResponse response) throws Exception {
File file = new File(path);
if (!file.isFile()) {
throw new FileNotFoundException("文件不存在");
private static Map<String, Object> getObjectMap(File file) {
BigDecimal filesize = BigDecimal.valueOf(file.length())
.divide(BigDecimal.valueOf(1024 * 1024), 2, RoundingMode.HALF_UP);
Long time = file.lastModified();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String filetime = format.format(time);
Map<String, Object> map = new HashMap<>();
map.put("filename", file.getName());
map.put("filepath", file.getAbsolutePath());
map.put("filesize", filesize);
map.put("filetime", filetime);
return map;
}
@GetMapping(value = "/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public DeferredResult<ResponseEntity<StreamingResponseBody>> downloadFileByPath(String path, Boolean temp) throws Exception {
final File file = new File(path);
final DeferredResult<ResponseEntity<StreamingResponseBody>> result = new DeferredResult<>();
if (!file.exists()) {
result.setErrorResult(new RuntimeException("文件不存在"));
return result;
}
String filename = file.getName();
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()));
try (FileInputStream fileInputStream = new FileInputStream(file);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(response.getOutputStream())) {
byte[] buffer_ = new byte[1024];
int n = bufferedInputStream.read(buffer_);
while (n != -1) {
bufferedOutputStream.write(buffer_);
n = bufferedInputStream.read(buffer_);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(file.getName(), StandardCharsets.UTF_8));
headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()));
StreamingResponseBody body = outputStream -> {
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException("IO错误: " + e.getMessage());
} finally {
// 尝试在流关闭后删除文件
try {
if(temp){
Files.deleteIfExists(file.toPath());
}
} catch (IOException e) {
// 记录日志或采取其他措施
System.err.println("删除文件失败: " + e.getMessage());
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(temp){//删除临时文件
FileSystemUtils.deleteRecursively(new File(path));
}
}
};
result.setResult(new ResponseEntity<>(body, headers, HttpStatus.OK));
return result;
}
@PostMapping("/packZip")
@ResponseBody
@ApiOperate(description = "批量文件打包下载")
public Map<String, Object> packZip(String filenames) throws Exception{
try {
String path = OsInfoUtil.isWindows() ? windows_path :
@@ -173,17 +165,14 @@ public class DownloadAction {
@PostMapping("/delete")
@ResponseBody
@ApiOperate(description = "单文件删除")
public String delete(String path) throws Exception{
File file = new File(path);
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, file.getName());
FileSystemUtils.deleteRecursively(file);
return "删除成功";
}
@PostMapping("/batchDel")
@ResponseBody
@ApiOperate(description = "批量文件删除")
public String batchDel(String filenames) throws Exception{
String path = OsInfoUtil.isWindows() ? windows_path :
OsInfoUtil.isLinux() ? linux_path : null;
@@ -195,7 +184,6 @@ public class DownloadAction {
if(!Arrays.asList(filenames.split(",")).contains(file.getName())){
continue;
}
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, file.getName());
FileSystemUtils.deleteRecursively(file);
}
return "删除成功";

View File

@@ -1,6 +1,5 @@
package cn.somkit.fmt.action;
import cn.somkit.fmt.annotation.ApiOperate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@@ -9,7 +8,6 @@ import org.springframework.web.servlet.ModelAndView;
public class IndexAction {
@RequestMapping("/")
@ApiOperate(description = "默认跳转")
public ModelAndView index(){
return new ModelAndView("redirect:/download/index");
}

View File

@@ -1,17 +1,28 @@
package cn.somkit.fmt.action;
import cn.somkit.fmt.annotation.ApiOperate;
import cn.metona.cache.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.ResponseBody;
@Controller
@RequestMapping("/logging")
public class LoggingAction {
@Autowired
private Cache<String, Object> cache;
@GetMapping("/index")
@ApiOperate(description = "在线日志页面跳转")
public String index() throws Exception{
return "logging";
}
@ResponseBody
@PostMapping("/close")
public void close(Boolean closed) throws Exception {
cache.put("closed", closed);
}
}

View File

@@ -1,14 +1,9 @@
package cn.somkit.fmt.action;
import cn.somkit.fmt.annotation.ApiOperate;
import cn.somkit.fmt.config.RocksDBConfig;
import cn.somkit.fmt.utils.MD5Utils;
import cn.somkit.fmt.utils.OsInfoUtil;
import cn.somkit.fmt.utils.RocksDBUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -30,14 +25,12 @@ public class UploadAction {
private String linux_path;
@GetMapping("/index")
@ApiOperate(description = "上传页面跳转")
public String index() throws Exception{
return "upload";
}
@PostMapping("/execute")
@ResponseBody
@ApiOperate(description = "文件上传")
public Map<String, Object> execute(HttpServletRequest request) throws Exception{
//多个文件上传 就只是简单的多文件上传保存在本地的磁盘
if (request instanceof MultipartHttpServletRequest mrequest) {
@@ -68,28 +61,9 @@ public class UploadAction {
assert path != null;
String filePath = "";
if (file != null && file.getSize() > 0) { // 有文件上传
String fileName = verify(file, null);// 创建文件名称
if(StringUtils.hasText(fileName)){
String hash = MD5Utils.md5HashCode(file.getInputStream());
RocksDBUtils.put(RocksDBConfig.RocksDB_Column_Family, fileName, hash);
filePath = path + File.separator + fileName;
File saveFile = new File(filePath) ;
file.transferTo(saveFile); // 文件保存
}
filePath = path + File.separator + file.getOriginalFilename();
File saveFile = new File(filePath) ;
file.transferTo(saveFile); // 文件保存
}
}
private String verify(MultipartFile file, String filename) throws Exception {
String key = StringUtils.hasText(filename) ? filename : Objects.requireNonNull(file.getOriginalFilename());
String hash = RocksDBUtils.get(RocksDBConfig.RocksDB_Column_Family, key);
if(!StringUtils.hasText(hash)){
return StringUtils.hasText(filename) ? filename : file.getOriginalFilename();
}
String newHash = MD5Utils.md5HashCode(file.getInputStream());
if(!hash.equals(newHash)){
String newFilename = "(1)" + file.getOriginalFilename();
return verify(file, newFilename);
}
return null;
}
}

View File

@@ -1,14 +0,0 @@
package cn.somkit.fmt.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiOperate {
/**
* 描述
*/
String description() default "";
}

View File

@@ -1,93 +0,0 @@
package cn.somkit.fmt.annotation.aspect;
import cn.hutool.json.JSONUtil;
import cn.somkit.fmt.annotation.ApiOperate;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class ApiOperateAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** 换行符 */
private static final String LINE_SEPARATOR = System.lineSeparator();
/** 以自定义 @ApiOperate 注解为切点 */
@Pointcut("@annotation(apiOperate)")
public void ApiOperate(ApiOperate apiOperate) {}
/**
* 环绕注入
* @param joinPoint
* @param apiOperate
* @return
* @throws Throwable
*/
@Around(value = "ApiOperate(apiOperate)", argNames = "joinPoint,apiOperate")
public Object doAround(ProceedingJoinPoint joinPoint, ApiOperate apiOperate) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
long timeConsuming = System.currentTimeMillis() - startTime;
// 打印请求相关参数
logger.info("========================================== Start ==========================================");
// 打印请求 url
logger.info("URL : {}", request.getRequestURL().toString());
// 打印描述信息
logger.info("Description : {}", apiOperate.description());
// 打印 Http method
logger.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
logger.info("IP : {}", request.getRemoteAddr());
// 打印请求Headers
logger.info("Header Args : {}", headers(request));
// 打印请求入参
logger.info("Request Args : {}", Arrays.toString(joinPoint.getArgs()));
// 打印出参
logger.info("Response Args : {}", JSONUtil.toJsonStr(result));
// 执行耗时
logger.info("Time-Consuming : {} ms", timeConsuming);
// 接口结束后换行,方便分割查看
logger.info("=========================================== End ===========================================" + LINE_SEPARATOR);
return result;
}
/**
* 获取所有header参数
* @param request
* @return Map<String, String> headers
*/
private Map<String, String> headers(HttpServletRequest request){
Map<String, String> headerMap = new HashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = request.getHeader(name);
headerMap.put(name, value);
}
return headerMap;
}
}

View File

@@ -1,36 +0,0 @@
package cn.somkit.fmt.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class LogSocketConfig {
public static String Log_File_Path = "./logs/fmt-server.log";
public static String Max_Read_Length = "500";
public static String Read_Interval = "1000";
@Value("${somkit.logging.socket.log-file-path}")
public void setLog_File_Path(String log_File_Path) {
if(StringUtils.hasText(log_File_Path)){
Log_File_Path = log_File_Path;
}
}
@Value("${somkit.logging.socket.max-read-length}")
public void setMax_Read_Length(String max_Read_Length) {
if(StringUtils.hasText(max_Read_Length)){
Max_Read_Length = max_Read_Length;
}
}
@Value("${somkit.logging.socket.read-interval}")
public void setRead_Interval(String read_Interval) {
if(StringUtils.hasText(read_Interval)){
Read_Interval = read_Interval;
}
}
}

View File

@@ -1,36 +0,0 @@
package cn.somkit.fmt.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
public class RocksDBConfig {
public static String RocksDB_Path_Windows = "D://RocksDB";
public static String RocksDB_Path_Linux = "/usr/local/rocksdb";
public static String RocksDB_Column_Family = "default";
@Value("${somkit.db.rocks.path.windows}")
public void setRocksDB_Path_Windows(String rocksDB_Path_Windows) {
if(StringUtils.hasText(rocksDB_Path_Windows)){
RocksDB_Path_Windows = rocksDB_Path_Windows;
}
}
@Value("${somkit.db.rocks.path.linux}")
public void setRocksDB_Path_Linux(String rocksDB_Path_Linux) {
if(StringUtils.hasText(rocksDB_Path_Linux)){
RocksDB_Path_Linux = rocksDB_Path_Linux;
}
}
@Value("${somkit.db.rocks.column-family}")
public void setRocksDB_Column_Family(String rocksDB_Column_Family){
if(StringUtils.hasText(rocksDB_Column_Family)){
RocksDB_Column_Family = rocksDB_Column_Family;
}
}
}

View File

@@ -0,0 +1,49 @@
package cn.somkit.fmt.entity;
import java.time.LocalDateTime;
public class LoggerMessage {
String level;
String loggerName;
String message;
String timestamp;
public LoggerMessage(String level, String loggerName, String message, String timestamp) {
this.level = level;
this.loggerName = loggerName;
this.message = message;
this.timestamp = timestamp;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public String getLoggerName() {
return loggerName;
}
public void setLoggerName(String loggerName) {
this.loggerName = loggerName;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
}

View File

@@ -0,0 +1,44 @@
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();
}
}
}

View File

@@ -18,7 +18,6 @@ public class WebSocketAutoConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketServerHandler(), "/socket/ws")//设置连接路径和处理
.setAllowedOrigins("*")//允许跨域访问
.addInterceptors(new WebSocketServerInterceptor());//设置拦截器
.setAllowedOrigins("*");//允许跨域访问
}
}

View File

@@ -1,27 +0,0 @@
package cn.somkit.fmt.socket;
public class WebSocketEntity {
/**
* 发送类型 HeartBeat:心跳 Message:消息
*/
private String send;
private Object data;
public String getSend() {
return send;
}
public void setSend(String send) {
this.send = send;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}

View File

@@ -1,27 +0,0 @@
package cn.somkit.fmt.socket;
public enum WebSocketEnums {
//消息来源
SEND_TP_HEARTBEAT("HeartBeat", "心跳"),
SEND_TP_LOGGING("Logging", "日志")
;
private String code;
private String name;
WebSocketEnums(){};
WebSocketEnums(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}

View File

@@ -1,59 +0,0 @@
package cn.somkit.fmt.socket;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class WebSocketOnline {
//在线连接数
private static final AtomicInteger onlineCount = new AtomicInteger(0);
//在线连接
private static final Map<String, WebSocketSession> onlineMap = new ConcurrentHashMap<>();
//日志读取位置记录
public static final Map<String, Integer> logLengthMap = new ConcurrentHashMap<>();
public static synchronized int getOnlineCount() {
return onlineCount.get();
}
public static void setOnline(String id, WebSocketSession session) throws Exception{
//如果已经存在,先把原来的连接关闭
if(onlineMap.containsKey(id)){
if(onlineMap.get(id).isOpen()){
onlineMap.get(id).close();
}
}
onlineMap.put(id, session);
onlineCount.set(getOnlineCount() + 1);
}
public static WebSocketSession getOnline(String id) throws Exception{
return onlineMap.get(id);
}
public static List<WebSocketSession> getOnlineList() throws Exception{
List<WebSocketSession> list = new ArrayList<>();
for (String key : onlineMap.keySet()) {
list.add(onlineMap.get(key));
}
return list;
}
public static void closeOnline(String id) throws Exception{
onlineMap.remove(id);
onlineCount.set(getOnlineCount() - 1);
}
public static void sendMessage(String id, String entityJsonStr) throws Exception {
WebSocketSession session = getOnline(id);
if(session != null && session.isOpen()){
session.sendMessage(new TextMessage(entityJsonStr));
}
}
}

View File

@@ -1,67 +1,53 @@
package cn.somkit.fmt.socket;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.somkit.fmt.utils.ErrorUtil;
import cn.somkit.fmt.utils.LogSocketUtils;
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.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import java.util.Objects;
import java.io.IOException;
@Component
public class WebSocketServerHandler implements WebSocketHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private Cache<String, Object> cache;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String id = session.getAttributes().get("id").toString();
WebSocketOnline.setOnline(id, session);
logger.info("有新窗口开始监听:" + id + ", 当前在线人数为:" + WebSocketOnline.getOnlineCount());
try {
//返回一个心跳检测
WebSocketEntity entity = new WebSocketEntity();
entity.setSend(WebSocketEnums.SEND_TP_HEARTBEAT.getCode());
entity.setData("心跳检测");
WebSocketOnline.sendMessage(id, JSONUtil.toJsonStr(entity));
}catch (Exception e){
logger.error("连接成功发送心跳异常:{}", ErrorUtil.errorInfoToString(e));
}
push(session);
}
//读取并实时发送日志
LogSocketUtils.LoggingChannel(id);
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
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
String id = session.getAttributes().get("id").toString();
String messageStr = message.getPayload().toString();
WebSocketEntity entity = JSONUtil.toBean(messageStr, WebSocketEntity.class);
if(Objects.equals(WebSocketEnums.SEND_TP_HEARTBEAT.getCode(), entity.getSend())) {
//心跳检测消息,原路返回客户端
WebSocketOnline.sendMessage(id, JSONUtil.toJsonStr(entity));
}
push(session);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable e) throws Exception {
String id = session.getAttributes().get("id").toString();
//发生错误时主动关闭连接
if(session.isOpen()){
session.close();
}
WebSocketOnline.closeOnline(id);
logger.error("{}连接发生错误:{}", id, ErrorUtil.errorInfoToString(e));
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
String id = session.getAttributes().get("id").toString();
WebSocketOnline.closeOnline(id);
logger.info("连接关闭:{},状态:{},当前在线人数为:{}", id, closeStatus.toString(), WebSocketOnline.getOnlineCount());
}
@Override

View File

@@ -1,32 +0,0 @@
package cn.somkit.fmt.socket;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
public class WebSocketServerInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
HttpServletRequest rs = ((ServletServerHttpRequest) request).getServletRequest();
HttpServletResponse hp = ((ServletServerHttpResponse)response).getServletResponse();
//request 参数放入 attributes中
rs.getParameterMap().forEach((k, v) -> {
attributes.put(k, v[0]);
});
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -0,0 +1,29 @@
package cn.somkit.fmt.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class AppContext implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return context;
}
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
public static Object getBean(String name) {
return context.getBean(name);
}
}

View File

@@ -1,40 +0,0 @@
package cn.somkit.fmt.utils;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 捕获报错日志处理工具类
*/
public class ErrorUtil {
/**
* Exception出错的栈信息转成字符串
* 用于打印到日志中
*/
public static String errorInfoToString(Throwable e) {
StringWriter sw = null;
PrintWriter pw = null;
try {
sw = new StringWriter();
pw = new PrintWriter(sw);
// 将出错的栈信息输出到printWriter中
e.printStackTrace(pw);
pw.flush();
sw.flush();
} finally {
if (sw != null) {
try {
sw.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (pw != null) {
pw.close();
}
}
return sw.toString();
}
}

View File

@@ -1,94 +0,0 @@
package cn.somkit.fmt.utils;
import cn.hutool.json.JSONUtil;
import cn.somkit.fmt.config.LogSocketConfig;
import cn.somkit.fmt.socket.WebSocketEntity;
import cn.somkit.fmt.socket.WebSocketEnums;
import cn.somkit.fmt.socket.WebSocketOnline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.util.StringUtils;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
public class LogSocketUtils {
private static final Logger logger = LoggerFactory.getLogger(LogSocketUtils.class);
public static void LoggingChannel(String socketSessionId) {
//获取日志信息
new Thread(() -> {
try {
boolean first = true;
while (WebSocketOnline.getOnline(socketSessionId) != null) {
BufferedReader reader = null;
try {
//日志文件路径
String filePath = LogSocketConfig.Log_File_Path;
//字符流
reader = new BufferedReader(new FileReader(filePath));
Object[] lines = reader.lines().toArray();
//只取从上次之后产生的日志
Integer length = WebSocketOnline.logLengthMap.get(socketSessionId);
Object[] copyOfRange = Arrays.copyOfRange(lines, length == null ? 0 : length, lines.length);
//对日志进行着色,更加美观 PS注意这里要根据日志生成规则来操作
for (int i = 0; i < copyOfRange.length; i++) {
String line = (String) copyOfRange[i];
//先转义
line = line.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;");
String[] stars = line.split("-&gt;");
if(stars.length == 2){
line = line.replace(stars[0], "<span style='color: #298a8a;'>"+stars[0]+"</span>");
}
//处理等级
line = line.replace("[DEBUG ]", "[<span style='color: blue;'>DEBUG</span>]");
line = line.replace("[INFO ]", "[<span style='color: green;'>INFO</span>]");
line = line.replace("[WARN ]", "[<span style='color: orange;'>WARN</span>]");
line = line.replace("[ERROR ]", "[<span style='color: red;'>ERROR</span>]");
copyOfRange[i] = line;
}
//存储最新一行开始
WebSocketOnline.logLengthMap.put(socketSessionId, lines.length);
//第一次如果太大截取最新的200行就够了避免传输的数据太大
int maxLength = Integer.parseInt(LogSocketConfig.Max_Read_Length);
if(first && copyOfRange.length > maxLength){
copyOfRange = Arrays.copyOfRange(copyOfRange, copyOfRange.length - maxLength, copyOfRange.length);
first = false;
}
String result = StringUtils.join(copyOfRange, "<br/>");
WebSocketEntity entity = new WebSocketEntity();
entity.setSend(WebSocketEnums.SEND_TP_LOGGING.getCode());
entity.setData(result);
WebSocketOnline.sendMessage(socketSessionId, JSONUtil.toJsonStr(entity));
//休眠一秒
Thread.sleep(Long.parseLong(LogSocketConfig.Read_Interval));
} catch (Exception e) {
logger.error("读取日志异常:{}", ErrorUtil.errorInfoToString(e));
} finally {
try {
assert reader != null;
reader.close();
} catch (IOException ioe) {
logger.error("字符流关闭异常:{}", ErrorUtil.errorInfoToString(ioe));
}
}
}
} catch (Exception e) {
logger.error("读取日志异常:{}", ErrorUtil.errorInfoToString(e));
}
}).start();
}
}

View File

@@ -0,0 +1,28 @@
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;
}
}
}

View File

@@ -1,235 +0,0 @@
package cn.somkit.fmt.utils;
import cn.somkit.fmt.config.RocksDBConfig;
import org.rocksdb.*;
import org.springframework.util.ObjectUtils;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class RocksDBUtils {
private static RocksDB rocksDB;
public static ConcurrentMap<String, ColumnFamilyHandle> columnFamilyHandleMap = new ConcurrentHashMap<>();
public static int GET_KEYS_BATCH_SIZE = 100000;
static {
try {
String rocksDBPath = null; //RocksDB文件目录
if (OsInfoUtil.isWindows()) {
rocksDBPath = RocksDBConfig.RocksDB_Path_Windows; // 指定windows系统下RocksDB文件目录
} else if(OsInfoUtil.isLinux()){
rocksDBPath = RocksDBConfig.RocksDB_Path_Linux; // 指定linux系统下RocksDB文件目录
}
RocksDB.loadLibrary();
Options options = new Options();
options.setCreateIfMissing(true); //如果数据库不存在则创建
List<byte[]> cfArr = RocksDB.listColumnFamilies(options, rocksDBPath); // 初始化所有已存在列族
List<ColumnFamilyDescriptor> columnFamilyDescriptors = new ArrayList<>(); //ColumnFamilyDescriptor集合
if (!ObjectUtils.isEmpty(cfArr)) {
for (byte[] cf : cfArr) {
columnFamilyDescriptors.add(new ColumnFamilyDescriptor(cf, new ColumnFamilyOptions()));
}
} else {
columnFamilyDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, new ColumnFamilyOptions()));
}
DBOptions dbOptions = new DBOptions();
dbOptions.setCreateIfMissing(true);
List<ColumnFamilyHandle> columnFamilyHandles = new ArrayList<>(); //ColumnFamilyHandle集合
rocksDB = RocksDB.open(dbOptions, rocksDBPath, columnFamilyDescriptors, columnFamilyHandles);
for (int i = 0; i < columnFamilyDescriptors.size(); i++) {
ColumnFamilyHandle columnFamilyHandle = columnFamilyHandles.get(i);
String cfName = new String(columnFamilyDescriptors.get(i).getName(), StandardCharsets.UTF_8);
columnFamilyHandleMap.put(cfName, columnFamilyHandle);
}
System.out.println("RocksDB init success!! path:" + rocksDBPath);
} catch (Exception e) {
System.out.println("RocksDB init failure!! error:" + e.getMessage());
e.printStackTrace();
}
}
private RocksDBUtils(){}
public static ColumnFamilyHandle cfAddIfNotExist(String cfName) throws RocksDBException {
ColumnFamilyHandle columnFamilyHandle;
if (!columnFamilyHandleMap.containsKey(cfName)) {
columnFamilyHandle = rocksDB.createColumnFamily(new ColumnFamilyDescriptor(cfName.getBytes(), new ColumnFamilyOptions()));
columnFamilyHandleMap.put(cfName, columnFamilyHandle);
System.out.println("cfAddIfNotExist success!! cfName:" + cfName);
} else {
columnFamilyHandle = columnFamilyHandleMap.get(cfName);
}
return columnFamilyHandle;
}
/**
* 列族,删除(如果存在)
*/
public static void cfDeleteIfExist(String cfName) throws RocksDBException {
if (columnFamilyHandleMap.containsKey(cfName)) {
rocksDB.dropColumnFamily(columnFamilyHandleMap.get(cfName));
columnFamilyHandleMap.remove(cfName);
System.out.println("cfDeleteIfExist success!! cfName:" + cfName);
} else {
System.out.println("cfDeleteIfExist containsKey!! cfName:" + cfName);
}
}
/**
* 增
*/
public static void put(String cfName, String key, String value) throws RocksDBException {
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
rocksDB.put(columnFamilyHandle, key.getBytes(), value.getBytes());
}
/**
* 增(批量)
*/
public static void batchPut(String cfName, Map<String, String> map) throws RocksDBException {
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
WriteOptions writeOptions = new WriteOptions();
WriteBatch writeBatch = new WriteBatch();
for (Map.Entry<String, String> entry : map.entrySet()) {
writeBatch.put(columnFamilyHandle, entry.getKey().getBytes(), entry.getValue().getBytes());
}
rocksDB.write(writeOptions, writeBatch);
}
/**
* 删
*/
public static void delete(String cfName, String key) throws RocksDBException {
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
rocksDB.delete(columnFamilyHandle, key.getBytes());
}
/**
* 查
*/
public static String get(String cfName, String key) throws RocksDBException {
String value = null;
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
byte[] bytes = rocksDB.get(columnFamilyHandle, key.getBytes());
if (!ObjectUtils.isEmpty(bytes)) {
value = new String(bytes, StandardCharsets.UTF_8);
}
return value;
}
/**
* 查(多个键值对)
*/
public static Map<String, String> multiGetAsMap(String cfName, List<String> keys) throws RocksDBException {
Map<String, String> map = new HashMap<>(keys.size());
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
List<ColumnFamilyHandle> columnFamilyHandles;
List<byte[]> keyBytes = keys.stream().map(String::getBytes).collect(Collectors.toList());
columnFamilyHandles = IntStream.range(0, keys.size()).mapToObj(i -> columnFamilyHandle).collect(Collectors.toList());
List<byte[]> bytes = rocksDB.multiGetAsList(columnFamilyHandles, keyBytes);
for (int i = 0; i < bytes.size(); i++) {
byte[] valueBytes = bytes.get(i);
String value = "";
if (!ObjectUtils.isEmpty(valueBytes)) {
value = new String(valueBytes, StandardCharsets.UTF_8);
}
map.put(keys.get(i), value);
}
return map;
}
/**
* 查(多个值)
*/
public static List<String> multiGetValueAsList(String cfName, List<String> keys) throws RocksDBException {
List<String> values = new ArrayList<>(keys.size());
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
List<ColumnFamilyHandle> columnFamilyHandles = new ArrayList<>();
List<byte[]> keyBytes = keys.stream().map(String::getBytes).collect(Collectors.toList());
for (int i = 0; i < keys.size(); i++) {
columnFamilyHandles.add(columnFamilyHandle);
}
List<byte[]> bytes = rocksDB.multiGetAsList(columnFamilyHandles, keyBytes);
for (byte[] valueBytes : bytes) {
String value = "";
if (!ObjectUtils.isEmpty(valueBytes)) {
value = new String(valueBytes, StandardCharsets.UTF_8);
}
values.add(value);
}
return values;
}
/**
* 查(所有键)
*/
public static List<String> getAllKey(String cfName) throws RocksDBException {
List<String> list = new ArrayList<>();
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
for (rocksIterator.seekToFirst(); rocksIterator.isValid(); rocksIterator.next()) {
list.add(new String(rocksIterator.key(), StandardCharsets.UTF_8));
}
}
return list;
}
/**
* 分片查(键)
*/
public static List<String> getKeysFrom(String cfName, String lastKey) throws RocksDBException {
List<String> list = new ArrayList<>(GET_KEYS_BATCH_SIZE);
// 获取列族Handle
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName);
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
if (lastKey != null) {
rocksIterator.seek(lastKey.getBytes(StandardCharsets.UTF_8));
rocksIterator.next();
} else {
rocksIterator.seekToFirst();
}
// 一批次最多 GET_KEYS_BATCH_SIZE 个 key
while (rocksIterator.isValid() && list.size() < GET_KEYS_BATCH_SIZE) {
list.add(new String(rocksIterator.key(), StandardCharsets.UTF_8));
rocksIterator.next();
}
}
return list;
}
/**
* 查(所有键值)
*/
public static Map<String, String> getAll(String cfName) throws RocksDBException {
Map<String, String> map = new HashMap<>();
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
for (rocksIterator.seekToFirst(); rocksIterator.isValid(); rocksIterator.next()) {
map.put(new String(rocksIterator.key(), StandardCharsets.UTF_8), new String(rocksIterator.value(), StandardCharsets.UTF_8));
}
}
return map;
}
/**
* 查总条数
*/
public static int getCount(String cfName) throws RocksDBException {
int count = 0;
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
for (rocksIterator.seekToFirst(); rocksIterator.isValid(); rocksIterator.next()) {
count++;
}
}
return count;
}
}

View File

@@ -15,26 +15,52 @@ spring:
multipart:
max-file-size: 1024MB
max-request-size: 10240MB
threads:
virtual:
enabled: true
somkit:
upload:
path:
windows: D://data/install/upload
linux: /mnt/files
db:
rocks:
path:
windows: D://RocksDB//fmt
linux: /usr/local/rocksdb/fmt
column-family: default
logging:
socket:
#日志文件地址
log-file-path: ./logs/fmt-server.log
#最大读取展示行数
max-read-length: 500
#读取间隔时间 毫秒
read-interval: 1000
logging:
config: classpath:logback-spring.xml
metona:
cache:
# 缓存类型,支持以下类型:
# - CONCURRENT_HASH_MAP: 基于 ConcurrentHashMap 的线程安全缓存
# - WEAK_HASH_MAP: 基于 WeakHashMap 的弱引用缓存
# - LINKED_HASH_MAP: 基于 LinkedHashMap 的 LRU 缓存
type: LINKED_HASH_MAP
# 缓存的初始容量,默认值为 16
initial-capacity: 128
# 缓存的最大容量,当缓存条目数超过该值时,会根据策略移除旧条目
# 仅对 LINKED_HASH_MAP 类型有效
maximum-size: 2000
# 写入后过期时间(单位由 time-unit 指定)
# 默认值为 -1表示永不过期
expire-after-write: -1
# 访问后过期时间(单位由 time-unit 指定)
# 默认值为 -1表示永不过期
expire-after-access: -1
# 时间单位,支持以下值:
# - NANOSECONDS: 纳秒
# - MICROSECONDS: 微秒
# - MILLISECONDS: 毫秒(默认)
# - SECONDS: 秒
# - MINUTES: 分钟
# - HOURS: 小时
# - DAYS: 天
time-unit: MILLISECONDS
# 是否记录缓存统计信息(如命中率、加载次数等)
# 默认值为 false
record-stats: true

View File

@@ -30,6 +30,16 @@
<outputDirectory>${file.separator}bin</outputDirectory>
</fileSet>
<!--拷贝版本记录到jar包的外部版本记录目录下面-->
<fileSet>
<directory>${basedir}/版本记录</directory>
<includes>
<include>*.md</include>
</includes>
<filtered>true</filtered>
<outputDirectory>${file.separator}versions</outputDirectory>
</fileSet>
<!--拷贝lib包到jar包的外部lib下面-->
<fileSet>
<directory>${project.build.directory}/lib</directory>

View File

@@ -1,5 +1,5 @@
chcp 65001
SET JAR=D:/fmt/fmt-1.2.0.jar
SET JAR=D:/fmt/fmt-2.0.0.jar
SET JAR_CONFIG=D:/fmt/config/
SET JAR_LIB=D:/fmt/lib/
java -Dfile.encoding=utf-8 -jar %JAR% --spring.config.location=%JAR_CONFIG% --spring.lib.location=%JAR_LIB%

View File

@@ -1,8 +1,8 @@
#!/bin/bash
JAR=/home/deploy/fmt/fmt-1.2.0.jar
JAR=/home/deploy/fmt/fmt-2.0.0.jar
JAR_CONFIG=/home/deploy/fmt/config/
JAR_LIB=/home/deploy/fmt/lib/
JAVA_HOME=/usr/local/jdk-17.0.7
JAVA_HOME=/usr/local/jdk-21
JAVA=$JAVA_HOME/bin/java
nohup $JAVA -jar $JAR --spring.config.location=$JAR_CONFIG --spring.lib.location=$JAR_LIB -Djava.ext.dirs=$JAVA_HOME/lib &
tail -f nohup.out

View File

@@ -3,15 +3,10 @@
<!-- Spring Boot 默认日志配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义日志的根目录 -->
<property name="LOG_HOME" value="./logs"/>
<!-- 定义日志文件名称 -->
<property name="APP_NAME" value="fmt-server"/>
<!-- 日志输出到控制台 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
<filter class="cn.somkit.fmt.filter.LogStashFilter">
<level>DEBUG</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
@@ -19,43 +14,8 @@
</encoder>
</appender>
<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名称 -->
<file>${LOG_HOME}/${APP_NAME}.log</file>
<!--
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
TimeBasedRollingPolicy 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--
滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
%i当文件大小超过maxFileSize时按照i进行文件滚动
-->
<fileNamePattern>${LOG_HOME}/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是365则只保存最近365天的文件删除之前的旧文件。注意删除旧文件是
那些为了归档而创建的目录也会被删除。
-->
<MaxHistory>365</MaxHistory>
<!--
当日志文件超过maxFileSize指定的大小是根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的必须配置timeBasedFileNamingAndTriggeringPolicy
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] [%logger{50}] -> %msg%n</pattern>
<charset>UTF-8</charset>
</layout>
</appender>
<!-- 指定日志输出级别以及启动的Appender -->
<root level="INFO">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

View File

@@ -0,0 +1,459 @@
class LogMonitorAdaptive {
constructor(container, opts = {}) {
this.container = typeof container === 'string'
? document.querySelector(container)
: container;
if (!this.container) throw new Error('容器未找到');
/* ========== 配置项及其含义 ========== */
this.cfg = {
/* --- 尺寸相关 --- */
width: null, // 日志容器宽度。null=100%,可填数字(px)或字符串('80%'/'20rem')
height: null, // 日志容器高度。null=100vh可填数字(px)或字符串('400px'/'50vh')
/* --- 日志条数与滚动 --- */
maxLines: 5000, // 最大保留日志条数,超出后自动删除最旧日志
autoScroll: true, // 出现新日志时是否自动滚动到底部
/* --- 主题与外观 --- */
theme: 'dark', // 主题:'dark' | 'light',控制整体配色
fontSize: 14, // 日志文字大小,单位 px
wordWrap: false, // 日志内容是否自动换行true=换行false=横向滚动)
/* --- 日志内容格式 --- */
showTimestamp: true, // 是否显示时间戳
showLevel: true, // 是否显示日志级别标签
timeFormat: 'HH:mm:ss.SSS', // 时间戳格式,可自定义
/* --- 日志级别 & 过滤 --- */
levels: ['debug', 'info', 'warn', 'error', 'success', 'system'], // 可用日志级别
// 过滤功能基于 levels如只想显示 info/warn可在这里删减
/* --- 功能开关 --- */
enableFilter: true, // 顶部是否显示级别过滤复选框
enableSearch: true, // 是否启用搜索框与高亮功能
enableExport: true, // 是否允许导出日志文件
enableClear: true, // 是否提供“清空”按钮
enablePause: true, // 是否提供“暂停/继续”按钮
enableThemeToggle: true, // 是否提供“切换主题”按钮
enableFullscreen: true, // 是否提供“全屏”按钮
enableFontSize: true, // 是否提供“字体大小 +/-”按钮
enableWordWrap: true, // 是否提供“换行/不换行”切换按钮
/* --- 暂停/继续 的回调函数 --- */
onTogglePause: () => {},
/* --- 创建完成 的回调函数 --- */
onCreated: () => {},
...opts
};
/* 强制确保 levels 是数组,防止 forEach 报错 */
if (!Array.isArray(this.cfg.levels)) {
this.cfg.levels = ['info', 'warn', 'error'];
}
this.isPaused = false;
this.isFullscreen = false;
this.logs = [];
this.filters = new Set();
this.searchTerm = '';
this.highlightIndex = -1;
this.highlightEls = [];
this.initDOM();
this.bindResize();
this.bindGlobalEvents();
//执行回调函数
if(this.cfg.onCreated && typeof this.cfg.onCreated === 'function'){
this.cfg.onCreated();
}
}
/* ------------------------ 初始化 ------------------------ */
initDOM() {
this.container.innerHTML = '';
this.setContainerSize(); // 宽高自适应
this.applyContainerStyle();
// 顶部导航栏
this.navbar = document.createElement('div');
Object.assign(this.navbar.style, {
display: 'flex',
alignItems: 'center',
padding: '8px 12px',
backgroundColor: this.cfg.theme === 'dark' ? '#2d2d30' : '#f3f3f3',
borderBottom: `1px solid ${this.cfg.theme === 'dark' ? '#555' : '#ccc'}`,
gap: '10px',
fontSize: '14px',
flexWrap: 'wrap'
});
// 按钮组
if (this.cfg.enablePause) {
this.pauseBtn = this.createBtn('⏸ 暂停', () => this.togglePause(), {w: '72px'});
this.navbar.appendChild(this.pauseBtn);
}
if (this.cfg.enableClear) {
this.navbar.appendChild(this.createBtn('🗑 清空', () => this.clear(), {w: '72px'}));
}
if (this.cfg.enableExport) {
this.navbar.appendChild(this.createBtn('💾 导出', () => this.export(), {w: '72px'}));
}
// 主题切换按钮
if (this.cfg.enableThemeToggle) {
this.themeBtn = this.createBtn(
this.cfg.theme === 'dark' ? '☀️ 明亮' : '🌙 暗黑',
() => this.toggleTheme(),
{w: '80px'}
);
this.navbar.appendChild(this.themeBtn);
}
// 字体大小
if (this.cfg.enableFontSize) {
this.navbar.appendChild(this.createBtn('A-', () => this.changeFontSize(-1), {w: '36px'}));
this.navbar.appendChild(this.createBtn('A+', () => this.changeFontSize(1), {w: '36px'}));
}
// 换行
if (this.cfg.enableWordWrap) {
this.wrapBtn = this.createBtn(
this.cfg.wordWrap ? '⏎ 换行' : '⏎ 不换行',
() => this.toggleWordWrap(),
{w: '90px'}
);
this.navbar.appendChild(this.wrapBtn);
}
// 全屏
if (this.cfg.enableFullscreen) {
this.navbar.appendChild(this.createBtn('⛶ 全屏', () => this.toggleFullscreen(), {w: '72px'}));
}
// 过滤复选框
if (this.cfg.enableFilter) {
this.cfg.levels.forEach(lvl => {
this.navbar.appendChild(this.createCheckbox(lvl));
});
}
// 搜索框
if (this.cfg.enableSearch) {
const searchWrap = document.createElement('span');
searchWrap.style.marginLeft = 'auto';
searchWrap.style.display = 'flex';
searchWrap.style.alignItems = 'center';
searchWrap.style.gap = '4px';
this.searchInput = document.createElement('input');
Object.assign(this.searchInput.style, {
padding: '6px 8px',
fontSize: '14px',
border: `1px solid ${this.cfg.theme === 'dark' ? '#555' : '#bbb'}`,
borderRadius: '4px',
backgroundColor: this.cfg.theme === 'dark' ? '#3c3c3c' : '#fff',
color: 'inherit',
width: '260px'
});
this.searchInput.placeholder = '搜索…';
this.searchInput.oninput = () => {
this.searchTerm = this.searchInput.value.trim();
this.renderVisible();
};
searchWrap.appendChild(this.searchInput);
['↑', '↓'].forEach((arr, idx) => {
searchWrap.appendChild(
this.createBtn(arr, () => this.navigateHighlight(idx), {w: '30px'})
);
});
this.navbar.appendChild(searchWrap);
}
// 日志视窗
this.viewport = document.createElement('div');
this.applyViewportStyle();
this.container.appendChild(this.navbar);
this.container.appendChild(this.viewport);
this.renderVisible();
}
/* ------------------------ 样式 ------------------------ */
setContainerSize() {
// 如果用户未指定宽高,则自适应
if (this.cfg.width === null) {
this.container.style.width = '100%';
} else {
this.container.style.width = typeof this.cfg.width === 'number' ? `${this.cfg.width}px` : this.cfg.width;
}
if (this.cfg.height === null) {
//this.container.style.height = '100vh';
const bodyMargin = parseFloat(getComputedStyle(document.body).marginTop || 0) +
parseFloat(getComputedStyle(document.body).marginBottom || 0);
this.container.style.height = `calc(100vh - ${bodyMargin}px - 2px)`;
} else {
this.container.style.height = typeof this.cfg.height === 'number' ? `${this.cfg.height}px` : this.cfg.height;
}
}
applyContainerStyle() {
Object.assign(this.container.style, {
display: 'flex',
flexDirection: 'column',
border: `1px solid ${this.cfg.theme === 'dark' ? '#444' : '#ccc'}`,
borderRadius: '6px',
fontFamily: 'Consolas, "Courier New", monospace',
fontSize: `${this.cfg.fontSize}px`,
lineHeight: 1.45,
backgroundColor: this.cfg.theme === 'dark' ? '#1e1e1e' : '#fafafa',
color: this.cfg.theme === 'dark' ? '#d4d4d4' : '#222',
overflow: 'hidden'
});
}
applyViewportStyle() {
Object.assign(this.viewport.style, {
flex: 1,
overflowY: 'auto',
padding: '8px',
whiteSpace: this.cfg.wordWrap ? 'pre-wrap' : 'pre',
wordBreak: 'break-word'
});
}
/* ------------------------ 工具 ------------------------ */
createBtn(text, handler, {w} = {}) {
const btn = document.createElement('button');
btn.textContent = text;
Object.assign(btn.style, {
padding: '6px 10px',
fontSize: '14px',
backgroundColor: this.cfg.theme === 'dark' ? '#444' : '#e7e7e7',
color: 'inherit',
border: `1px solid ${this.cfg.theme === 'dark' ? '#666' : '#ccc'}`,
borderRadius: '4px',
cursor: 'pointer',
minWidth: w || 'auto'
});
btn.onmouseenter = () => btn.style.backgroundColor = this.cfg.theme === 'dark' ? '#555' : '#d0d0d0';
btn.onmouseleave = () => btn.style.backgroundColor = this.cfg.theme === 'dark' ? '#444' : '#e7e7e7';
btn.onclick = handler;
return btn;
}
createCheckbox(level) {
const lbl = document.createElement('label');
lbl.style.color = this.cfg.theme === 'dark' ? '#ccc' : '#333';
lbl.style.fontSize = '13px';
lbl.style.display = 'flex';
lbl.style.alignItems = 'center';
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.checked = true;
chk.style.margin = '0 4px';
chk.onchange = () => {
if (chk.checked) this.filters.delete(level);
else this.filters.add(level);
this.renderVisible();
};
lbl.appendChild(chk);
lbl.append(level.toUpperCase());
return lbl;
}
/* ------------------------ 日志渲染 ------------------------ */
log(message, level = 'info', ts = this.formatTime(new Date())) {
if (!this.cfg.levels.includes(level)) level = 'info';
if (this.isPaused) return;
ts = this.cfg.showTimestamp ? ts : '';
this.logs.push({message, level, ts, id: Date.now() + Math.random()});
if (this.logs.length > this.cfg.maxLines) this.logs.shift();
this.renderVisible();
}
renderVisible() {
this.viewport.innerHTML = '';
this.highlightEls = [];
this.highlightIndex = -1;
const filtered = this.logs.filter(l => !this.filters.has(l.level));
const frag = document.createDocumentFragment();
const regex = this.searchTerm ? new RegExp(`(${this.searchTerm})`, 'gi') : null;
filtered.forEach(entry => {
const line = document.createElement('div');
line.style.display = 'flex';
line.style.margin = '1px 0';
if (this.cfg.showTimestamp && entry.ts) {
const tsSpan = document.createElement('span');
tsSpan.textContent = entry.ts + ' ';
tsSpan.style.color = this.cfg.theme === 'dark' ? '#6a9955' : '#008000';
tsSpan.style.minWidth = '90px';
line.appendChild(tsSpan);
}
if (this.cfg.showLevel) {
const lvlSpan = document.createElement('span');
lvlSpan.textContent = `[${entry.level.toUpperCase()}]`;
const colors = {
debug: '#9c27b0', info: '#2196f3', warn: '#ff9800',
error: '#f44336', success: '#4caf50', system: '#00bcd4'
};
lvlSpan.style.color = colors[entry.level] || colors.info;
lvlSpan.style.minWidth = '70px';
lvlSpan.style.fontWeight = 'bold';
line.appendChild(lvlSpan);
}
const msgSpan = document.createElement('span');
msgSpan.style.flex = 1;
let msg = entry.message;
if (regex) {
if (regex.test(entry.message)) {
line.style.backgroundColor = 'rgba(255,235,59,0.25)';
this.highlightEls.push(line);
}
msg = entry.message.replace(regex, '<mark style="background:#ffeb3b;color:#000;">$1</mark>');
}
msgSpan.innerHTML = msg;
line.appendChild(msgSpan);
frag.appendChild(line);
});
this.viewport.appendChild(frag);
if (this.cfg.autoScroll && !this.isPaused) {
this.viewport.scrollTop = this.viewport.scrollHeight;
}
}
/* ------------------------ 交互 ------------------------ */
togglePause() {
this.isPaused = !this.isPaused;
this.pauseBtn.textContent = this.isPaused ? '▶ 继续' : '⏸ 暂停';
this.pauseBtn.style.backgroundColor = this.isPaused
? (this.cfg.theme === 'dark' ? '#d32f2f' : '#ff5252')
: (this.cfg.theme === 'dark' ? '#444' : '#e7e7e7');
//执行回调函数
if(this.cfg.onTogglePause && typeof this.cfg.onTogglePause === 'function'){
this.cfg.onTogglePause(this.isPaused);
}
}
toggleTheme() {
this.cfg.theme = this.cfg.theme === 'dark' ? 'light' : 'dark';
this.themeBtn.textContent = this.cfg.theme === 'dark' ? '☀️ 明亮' : '🌙 暗黑';
this.applyContainerStyle();
this.navbar.style.backgroundColor = this.cfg.theme === 'dark' ? '#2d2d30' : '#f3f3f3';
this.renderVisible();
}
changeFontSize(delta) {
this.cfg.fontSize = Math.max(10, Math.min(24, this.cfg.fontSize + delta));
this.container.style.fontSize = `${this.cfg.fontSize}px`;
}
toggleWordWrap() {
this.cfg.wordWrap = !this.cfg.wordWrap;
this.wrapBtn.textContent = this.cfg.wordWrap ? '⏎ 不换行' : '⏎ 换行';
this.applyViewportStyle();
}
toggleFullscreen() {
if (!this.isFullscreen) {
if (this.container.requestFullscreen) this.container.requestFullscreen();
else if (this.container.webkitRequestFullscreen) this.container.webkitRequestFullscreen();
else if (this.container.msRequestFullscreen) this.container.msRequestFullscreen();
} else {
if (document.exitFullscreen) document.exitFullscreen();
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
else if (document.msExitFullscreen) document.msExitFullscreen();
}
this.isFullscreen = !this.isFullscreen;
}
navigateHighlight(dir) {
if (!this.highlightEls.length) return;
this.highlightIndex = (this.highlightIndex + (dir === 0 ? -1 : 1) + this.highlightEls.length) % this.highlightEls.length;
this.highlightEls.forEach((el, idx) => {
el.style.outline = idx === this.highlightIndex ? '2px solid #ffeb3b' : 'none';
});
this.highlightEls[this.highlightIndex].scrollIntoView({block: 'nearest', behavior: 'smooth'});
}
clear() {
this.logs = [];
this.renderVisible();
}
export() {
const lines = this.logs.map(l => `${l.ts} [${l.level.toUpperCase()}] ${l.message}`);
const blob = new Blob([lines.join('\n')], {type: 'text/plain;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `log_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
a.click();
URL.revokeObjectURL(url);
}
formatTime(date) {
const t = date.toTimeString().slice(0, 8);
const ms = String(date.getMilliseconds()).padStart(3, '0');
return `${t}.${ms}`;
}
bindResize() {
window.addEventListener('resize', () => {
this.setContainerSize(); // 响应窗口变化
});
}
bindGlobalEvents() {
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && this.searchInput) {
this.searchInput.value = '';
this.searchTerm = '';
this.renderVisible();
}
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
this.clear();
}
});
}
/* 快捷方法 */
info(msg, ts = this.formatTime(new Date())) {
this.log(msg, 'info', ts);
}
warn(msg, ts = this.formatTime(new Date())) {
this.log(msg, 'warn', ts);
}
error(msg, ts = this.formatTime(new Date())) {
this.log(msg, 'error', ts);
}
success(msg, ts = this.formatTime(new Date())) {
this.log(msg, 'success', ts);
}
debug(msg, ts = this.formatTime(new Date())) {
this.log(msg, 'debug', ts);
}
system(msg, ts = this.formatTime(new Date())) {
this.log(msg, 'system', ts);
}
}

View File

@@ -1,251 +0,0 @@
/**
* WebSocket封装工具类
* <pre>
* 代码示例:
* let FmtSocket = new FmtSocket(
* {url:'websocket服务器地址',id:'唯一标识',timeout:心跳检测间隔时间(毫秒)}
* );
* //调用初始化方法
* FmtSocket.init((data) => {
* //初始化完成之后的回调函数 ata为监听到的数据
* //可以在这里做接收到消息后的逻辑处理
* console.log(data);
* });
* //调用发送消息方法 message 消息字符串或对象
* FmtSocket.sendMsg(message, () => {
* //发送消息成功之后的回调函数
* });
* </pre>
*/
class FmtSocket {
id;
#options;
#ws;
#entity;
#deepProxyObj;
#proxyMsg;
/**
* 构造函数
* @param {Object} options
*/
constructor(options) {
this.#options = this.#extend({
url: 'http://127.0.0.1/socket/ws',//后台websocket服务地址
id: this.#uuid(),//如果没传此参数创建一个随机的UUID传给后台服务作为标识
timeout: 30000 //心跳检测间隔时间 默认30秒
}, options);
this.id = this.#options.id;
this.#ws = null;
//send:发送类型 HeartBeat:心跳 Logging:消息
this.#entity = {send: null, data: null};
}
/**
* 初始化websocket
* @param {Function} callback (type, data) 回调函数
*/
init(callback){
if(callback && typeof callback === 'function'){
this.#proxyMsg = [];
this.#deepProxyObj = this.#deepProxy(this.#proxyMsg,callback);
}
this.#createWebSocket();
}
/**
* 对象、数组变化监听(增删改)
* @param {Object} obj
* @param {Function} callback
* @return {Object}
*/
#deepProxy(obj, callback){
if (typeof obj === 'object') {
for (let key in obj) {
if (typeof obj[key] === 'object') {
obj[key] = this.#deepProxy(obj[key], callback);
}
}
}
return new Proxy(obj, {
/**
* @param {Object, Array} target 设置值的对象
* @param {String} key 属性
* @param {any} value 值
* @param {Object} receiver this
*/
set: (target, key, value, receiver)=> {
if (typeof value === 'object') {
value = this.#deepProxy(value, callback);
}
let cbType = target[key] === undefined ? 'create' : 'modify';
//排除数组修改length回调
if (!(Array.isArray(target) && key === 'length')) {
if (cbType === 'create') {
callback(value);
}
}
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key);
}
});
}
#createWebSocket(){
try {
if ('WebSocket' in window) {
this.#ws = new WebSocket((this.#options.url + "?id=" + this.#options.id)
.replace("http", "ws")
.replace("https", "wss"));
} else {
alert('浏览器不支持WebSocket通讯');
return false;
}
this.#onopen();
this.#onmessage();
this.#onerror();
this.#onclose();
//监听窗口关闭事件当窗口关闭时主动去关闭websocket连接防止连接还没断开就关闭窗口server端会抛异常。
window.onbeforeunload = ()=> {
this.#ws.close();
};
} catch (e) {
console.log("WebSocket:链接失败", e);
alert('WebSocket:链接失败');
}
}
/**
* 关闭websocket链接
* @param {Function} callback
*/
closeWebSocket(callback){
if(this.#ws){
this.#ws.close();
}
if(callback && typeof callback === 'function'){
callback();
}
}
/**
* 发送消息
* @param {String, Object} message
* @param {Function} callback
*/
sendMsg(message, callback){
if(!message || typeof message !== 'string' || typeof message !== 'object'){
alert("FmtSocket sendMsg message is not null or message is not a string or object");
return false;
}
if(callback && typeof callback !== 'function'){
alert("FmtSocket sendMsg callback is not a function");
return false;
}
this.#emptyObjectPropertyValues(this.#entity);
this.#entity.send = 'Message';
this.#entity.data = message;
if (this.#ws.readyState === this.#ws.CONNECTING) {
// 正在开启状态等待1s后重新调用
let socket = this;
setTimeout(function () {
socket.#ws.send(JSON.stringify(socket.#entity));
callback();
}, 1000)
}else{
this.#ws.send(JSON.stringify(this.#entity));
callback();
}
}
#onopen(){
this.#ws.onopen = (event) => {
console.log("WebSocket:链接开启");
}
}
#onmessage(){
this.#ws.onmessage = (event) => {
this.#emptyObjectPropertyValues(this.#entity);
this.#entity = JSON.parse(event.data);
//心跳检测
if(this.#entity.send === 'HeartBeat'){
this.#heartBeat();
}
if(this.#deepProxyObj){
this.#deepProxyObj.splice(0);//赋值前先清空,保证消息数组里只有一条最新的数据
let data = JSON.parse(JSON.stringify(this.#entity));
this.#deepProxyObj.push(data);
}
}
}
#onerror(){
this.#ws.onerror = (event) => {
console.log("WebSocket:发生错误", event);
}
}
#onclose(){
this.#ws.onclose = (event) => {
console.log("WebSocket:链接关闭");
}
}
#heartBeat(){
setTimeout(()=> {
this.#emptyObjectPropertyValues(this.#entity);
if (this.#ws.readyState === this.#ws.CLOSING
|| this.#ws.readyState === this.#ws.CLOSED){
alert("WebSocket通讯服务链接已关闭");
return false;
}
this.#entity.send = 'HeartBeat';
this.#entity.data = '心跳检测';
if (this.#ws.readyState === this.#ws.CONNECTING) {
// 正在开启状态等待1s后重新调用
setTimeout(()=> {
this.#ws.send(JSON.stringify(this.#entity));
}, 1000)
}else{
this.#ws.send(JSON.stringify(this.#entity));
}
},this.#options.timeout);
}
/**
* 扩展source相同属性的值去覆盖target, target如果没有这个属性就新增
* @param {Object} target
* @param {Object} source
* @returns {Object}
*/
#extend(target, source){
if(typeof target !== 'object') return null;
if(typeof source !== 'object') return null;
for (const key in source) {
// 使用for in会遍历数组所有的可枚举属性包括原型。
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
return target;
}
#uuid(len, radix){
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
let uuid = [], i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
// rfc4122, version 4 form
let r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
#emptyObjectPropertyValues(person){
Object.keys(person).forEach(key => (person[key] = null));
}
}

View File

@@ -40,7 +40,7 @@
<span class="ax-line"></span>
</div>
<div class="ax-item">
<a th:href="@{/logging/index}" class="ax-text">在线日志</a>
<a th:href="@{/logging/index}" target="_blank" class="ax-text">在线日志</a>
<span class="ax-line"></span>
</div>
</div>
@@ -104,7 +104,7 @@
const fileDown = (obj) => {
const path = obj.getAttribute("value");
window.open(Fmt.ctx() + '/download/execute?path=' + encodeURIComponent(path) + '&temp=' + false);
window.open(Fmt.ctx() + '/download/file?path=' + encodeURIComponent(path) + '&temp=' + false);
};
window.onload = () => {
@@ -135,7 +135,7 @@
};
Fmt.axios(options).then((result) => {
message.update({content: '压缩包' + result.filename + "创建完成,准备下载文件......", result: 'success'}).show();
window.open(Fmt.ctx() + '/download/execute?path=' + encodeURIComponent(result.path) + '&temp=' + true);
window.open(Fmt.ctx() + '/download/file?path=' + encodeURIComponent(result.path) + '&temp=' + true);
}).catch((err) => {
console.log(err);
new axMessage({content: err, result: 'error', iconShow: true}).show();

View File

@@ -1,62 +1,72 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="https://www.thymeleaf.org">
<html lang="zh-CN">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-touch-fullscreen" content="yes"/>
<meta name="format-detection" content="email=no" />
<meta name="wap-font-scale" content="no" />
<meta name="viewport" content="user-scalable=no, width=device-width" />
<meta content="telephone=no" name="format-detection" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="UTF-8">
<title>在线日志</title>
<link th:href="@{/axui-v2.1.1/css/ax.css}" rel="stylesheet" type="text/css" >
<link th:href="@{/axui-v2.1.1/css/ax-response.css}" rel="stylesheet" type="text/css" >
</head>
<body>
<header class="ax-header">
<div class="ax-row">
<div class="ax-col">
<a th:href="@{/download/index}" class="ax-logo">
<img th:src="@{/common/images/logo.png}" alt="File Management"/>
</a>
</div>
<div class="ax-nav">
<div class="ax-item">
<a th:href="@{/download/index}" class="ax-text">文件列表</a>
<span class="ax-line"></span>
</div>
<div class="ax-item">
<a th:href="@{/upload/index}" class="ax-text">文件上传</a>
<span class="ax-line"></span>
</div>
<div class="ax-item ax-selected">
<a th:href="@{/logging/index}" class="ax-text">在线日志</a>
<span class="ax-line"></span>
</div>
</div>
</div>
</header>
<div class="ax-space-header"></div>
<div id="RootDiv" class="ax-border ax-margin">
</div>
<script th:src="@{/axui-v2.1.1/js/ax.js}" type="text/javascript" charset="utf-8"></script>
<!-- 日志容器 -->
<div id="logContainer"></div>
<script th:src="@{/common/js/basic.js}" type="text/javascript" charset="utf-8"></script>
<script th:src="@{/common/js/fmt-socket.js}" type="text/javascript" charset="utf-8"></script>
<script th:src="@{/common/js/LogMonitorAdaptive.js}" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" th:inline="javascript" charset="utf-8">
window.onload = () => {
const socket = new FmtSocket({url: Fmt.ctx() + '/socket/ws'});
socket.init((data) => {
if (data.send === 'Logging') {
const log = data.data;
const logDiv = document.createElement('div');
logDiv.innerHTML = '<pre><strong>' + log + '</strong></pre>';
document.querySelector('#RootDiv').appendChild(logDiv);
let ws = null;
const logger = new LogMonitorAdaptive('#logContainer', {
theme: 'dark',
maxLines: 10000,
fontSize: 14,
enableFilter: true,
enableSearch: true,
enableExport: true,
enableClear: true,
enablePause: true,
enableThemeToggle: true,
enableFullscreen: true,
enableFontSize: true,
enableWordWrap: true,
showTimestamp: true, // 是否显示时间戳
showLevel: true, // 是否显示日志级别标签
wordWrap: true, // 日志内容是否自动换行true=换行false=横向滚动)
//暂停/继续 回调函数
onTogglePause: async (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: () => {
console.log('日志容器已创建');
}
});
try {
if ('WebSocket' in window) {
ws = new WebSocket((Fmt.ctx() + '/socket/ws').replace("http", "ws").replace("https", "wss"));
} else {
alert('浏览器不支持WebSocket通讯');
return false;
}
ws.onmessage = function (event) {
if(event.data){
let data = JSON.parse(event.data);
logger.log(data.loggerName + ' : ' + data.message, data.level, data.timestamp);
}
}
//监听窗口关闭事件当窗口关闭时主动去关闭websocket连接防止连接还没断开就关闭窗口server端会抛异常。
window.onbeforeunload = ()=> {
ws.close();
};
} catch (e) {
alert('WebSocket:链接失败');
}
}
</script>
</body>

View File

@@ -33,7 +33,7 @@
<span class="ax-line"></span>
</div>
<div class="ax-item">
<a th:href="@{/logging/index}" class="ax-text">在线日志</a>
<a th:href="@{/logging/index}" target="_blank" class="ax-text">在线日志</a>
<span class="ax-line"></span>
</div>
</div>

View File

@@ -1,13 +0,0 @@
package cn.somkit.fmt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class FmtApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -17,3 +17,13 @@
上传文件格式新增支持js、css、html、vsdx、dmp、7z、ppt、pptx
AxUI的axAjax方法timeout默认值改为6000000
```
> v2.0.0
```
SpringBoot从3.1.1升级到3.5.3
JDK从17升级到21并开启虚拟线程
删除RocksDB相关配置不再使用该缓存方案
修改文件下载方式使用StreamingResponseBody支持大文件下载
引入metona-cache-spring-boot-starter使用此缓存方案
重构在线日志页面及实现方式,不再使用读取日志文件方式,自定义日志拦截器实时获取日志
不再生成自定义日志文件日志打印从INFO改为DEBUG打印更详细的内容
```