diff --git a/Dockerfile b/Dockerfile index f28f0b6..0e47874 100644 --- a/Dockerfile +++ b/Dockerfile @@ -167,8 +167,8 @@ RUN echo "=== 检查构建结果 ===" && \ # 复用基础镜像,避免重复安装依赖 FROM base -# 设置环境变量(优化内存使用和字体支持) -ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Djava.awt.headless=true -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai" +# 设置环境变量(优化内存使用和字体支持,防止OOM) +ENV JAVA_OPTS="-Xms1g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/dumps/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/app/logs/gc.log -Djava.awt.headless=true -XX:+UseContainerSupport -XX:MaxRAMPercentage=80.0 -XX:InitiatingHeapOccupancyPercent=45 -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai" ENV SPRING_PROFILES_ACTIVE=prod ENV TESSDATA_PREFIX=/usr/share/tessdata/ ENV OCR_TESSPATH=/usr/bin/tesseract diff --git a/docker-compose.yml b/docker-compose.yml index 8b5fc97..54c68db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: - ./data/images:/app/data/images:rw - ./data/reports:/app/data/reports:rw - ./logs:/app/logs:rw + # 添加内存转储目录 + - ./data/dumps:/app/dumps:rw user: "1001:1001" # 指定容器内用户ID,与Dockerfile中的app用户保持一致 environment: - SPRING_PROFILES_ACTIVE=prod @@ -31,9 +33,26 @@ services: - SWAGGER_SHOW=false - LOG_ROOT_LEVEL=info - LOG_APP_LEVEL=info + # JVM内存优化参数 + - JAVA_OPTS=-Xms1g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/dumps/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/app/logs/gc.log -XX:+UseContainerSupport -XX:MaxRAMPercentage=80.0 -XX:InitiatingHeapOccupancyPercent=45 networks: - proxy restart: unless-stopped + # 添加健康检查和资源限制 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9081/point-strategy/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + deploy: + resources: + limits: + memory: 3G # 限制容器最大内存使用 + cpus: '2.0' # 限制CPU使用 + reservations: + memory: 1G # 预留内存 + cpus: '1.0' # 预留CPU networks: proxy: external: true diff --git a/jvm-optimization.properties b/jvm-optimization.properties new file mode 100644 index 0000000..4febeea --- /dev/null +++ b/jvm-optimization.properties @@ -0,0 +1,61 @@ +# JVM内存优化配置文件 +# 用于防止OOM问题的JVM参数配置 + +# 基础内存设置 +-Xms1g # 初始堆内存大小 +-Xmx2g # 最大堆内存大小 +-XX:NewRatio=1 # 年轻代与老年代比例 +-XX:SurvivorRatio=8 # Eden与Survivor区比例 + +# 垃圾收集器优化 +-XX:+UseG1GC # 使用G1垃圾收集器 +-XX:MaxGCPauseMillis=200 # 最大GC暂停时间目标 +-XX:G1HeapRegionSize=16m # G1区域大小 +-XX:InitiatingHeapOccupancyPercent=45 # 触发并发GC的堆占用率 + +# OOM预防 +-XX:+HeapDumpOnOutOfMemoryError # OOM时生成堆转储 +-XX:HeapDumpPath=/app/dumps/ # 堆转储文件路径 +-XX:+UseGCLogFileRotation # GC日志轮转 +-XX:NumberOfGCLogFiles=5 # 保留GC日志文件数量 +-XX:GCLogFileSize=10M # 单个GC日志文件大小 + +# 容器环境优化 +-XX:+UseContainerSupport # 启用容器支持 +-XX:MaxRAMPercentage=80.0 # 最大使用容器80%内存 +-XX:+UnlockExperimentalVMOptions # 解锁实验性VM选项 +-XX:+UseCGroupMemoryLimitForHeap # 使用cgroup内存限制 + +# 字符串优化 +-XX:+UseStringDeduplication # 启用字符串去重 +-XX:StringTableSize=200000 # 字符串表大小 + +# 类加载优化 +-XX:+UseCompressedOops # 压缩对象指针 +-XX:+UseCompressedClassPointers # 压缩类指针 + +# 监控和日志 +-XX:+PrintGCDetails # 打印GC详细信息 +-XX:+PrintGCTimeStamps # 打印GC时间戳 +-XX:+PrintGCApplicationStoppedTime # 打印GC暂停时间 +-Xloggc:/app/logs/gc.log # GC日志文件路径 + +# 网络和IO优化 +-Djava.awt.headless=true # 无头模式 +-Dfile.encoding=UTF-8 # 文件编码 +-Duser.timezone=Asia/Shanghai # 时区设置 + +# Spring Boot特定优化 +-Dspring.jmx.enabled=false # 禁用JMX +-Dspring.output.ansi.enabled=never # 禁用ANSI颜色 +-XX:+TieredCompilation # 分层编译 +-XX:TieredStopAtLevel=1 # 快速编译 + +# 异常处理 +-XX:+OmitStackTraceInFastThrow # 快速抛出异常时省略堆栈 +-XX:+AlwaysPreTouch # 预分配内存页 + +# 元空间优化 +-XX:MetaspaceSize=256m # 初始元空间大小 +-XX:MaxMetaspaceSize=512m # 最大元空间大小 +-XX:+UseCompressedOops # 压缩类指针 \ No newline at end of file diff --git a/src/main/java/com/point/strategy/archiveFile/controller/ArchiveFileController.java b/src/main/java/com/point/strategy/archiveFile/controller/ArchiveFileController.java index 62fccc2..21bcc6e 100644 --- a/src/main/java/com/point/strategy/archiveFile/controller/ArchiveFileController.java +++ b/src/main/java/com/point/strategy/archiveFile/controller/ArchiveFileController.java @@ -23,6 +23,9 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.util.Iterator; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -89,10 +92,7 @@ public class ArchiveFileController { @ApiOperation(value = "showImg") public void showImg(HttpServletRequest request, HttpServletResponse response,String path, String fileName,Integer userId) throws IOException{ User user = userService.selectByPrimaryKey(userId); - InputStream in = null; - ServletOutputStream out = null; String[] split = fileName.split("\\."); - ByteArrayOutputStream outputStream = null; // 先解码路径和文件名 try { @@ -102,6 +102,19 @@ public class ArchiveFileController { log.error("URL解码失败: {}", e.getMessage()); } + String downLoadPath = path + File.separator + fileName; + File file = new File(downLoadPath); + + // 检查文件是否存在 + if (!file.exists()) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // 检查文件大小,超过阈值直接返回原文件(避免OOM) + final long LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB + boolean isLargeFile = file.length() > LARGE_FILE_THRESHOLD; + if(!split[split.length-1].equalsIgnoreCase("jpg")&&!split[split.length-1].equalsIgnoreCase("png")&&!split[split.length-1].equalsIgnoreCase("pdf")){ response.reset(); // 设置正确的Content-Type和编码 @@ -110,63 +123,89 @@ public class ArchiveFileController { String encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName); - try { -// String dir = uploadPath + File.separator + path; - String downLoadPath = path + File.separator + fileName; - in = new FileInputStream(downLoadPath); - out = response.getOutputStream(); - byte[] bytes = new byte[1024 * 10]; - int len = 0; - while ((len = in.read(bytes)) != -1) { - out.write(bytes,0,len); + // 使用try-with-resources确保资源释放,流式处理 + try (InputStream in = new FileInputStream(file); + ServletOutputStream out = response.getOutputStream()) { + + byte[] buffer = new byte[8192]; // 8KB缓冲区 + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); } out.flush(); } catch (IOException e) { log.error("文件下载失败: {}", e.getMessage()); throw e; - } finally { - try { - if (in != null) in.close(); - if (out != null) out.close(); - } catch (IOException e) { - log.error("关闭流失败: {}", e.getMessage()); - } } }else { //需要添加水印展示的文件 -// String dir = uploadPath + File.separator + path; - String downLoadPath = path + File.separator + fileName; try { if (split[split.length-1].equalsIgnoreCase("pdf")){ - in = new FileInputStream(downLoadPath); - outputStream = PdfFileHelper.waterMark(in,user.getUsername()); - // 设置PDF的Content-Type - response.setContentType("application/pdf;charset=UTF-8"); - out = response.getOutputStream(); - out.write(outputStream.toByteArray()); - out.flush(); - }else { - outputStream = addWatermarkByFileIo(downLoadPath, user.getUsername()); - // 根据文件类型设置正确的Content-Type - String contentType = "image/jpeg"; - if (split[split.length-1].equalsIgnoreCase("png")) { - contentType = "image/png"; + // 大文件直接返回,不加水印 + if (isLargeFile) { + response.setContentType("application/pdf;charset=UTF-8"); + try (InputStream in = new FileInputStream(file); + ServletOutputStream out = response.getOutputStream()) { + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } + } else { + // 小文件加水印处理 + try (InputStream in = new FileInputStream(file)) { + ByteArrayOutputStream outputStream = PdfFileHelper.waterMark(in, user.getUsername()); + // 设置PDF的Content-Type + response.setContentType("application/pdf;charset=UTF-8"); + try (ServletOutputStream out = response.getOutputStream()) { + out.write(outputStream.toByteArray()); + out.flush(); + } finally { + outputStream.close(); + } + } + } + }else { + // 大文件直接返回,不加水印 + if (isLargeFile) { + String contentType = "image/jpeg"; + if (split[split.length-1].equalsIgnoreCase("png")) { + contentType = "image/png"; + } + response.setContentType(contentType + ";charset=UTF-8"); + + try (InputStream in = new FileInputStream(file); + ServletOutputStream out = response.getOutputStream()) { + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } + } else { + // 小文件加水印处理 + ByteArrayOutputStream outputStream = addWatermarkByFileIo(downLoadPath, user.getUsername()); + // 根据文件类型设置正确的Content-Type + String contentType = "image/jpeg"; + if (split[split.length-1].equalsIgnoreCase("png")) { + contentType = "image/png"; + } + response.setContentType(contentType + ";charset=UTF-8"); + try (ServletOutputStream out = response.getOutputStream()) { + out.write(outputStream.toByteArray()); + out.flush(); + } finally { + outputStream.close(); + } } - response.setContentType(contentType + ";charset=UTF-8"); - out = response.getOutputStream(); - out.write(outputStream.toByteArray()); - out.flush(); } } catch (IOException e) { log.error("水印处理失败: {}", e.getMessage()); throw e; - } finally { - try { - if (in!=null) in.close(); - if (out!=null) out.close(); - if (outputStream!=null) outputStream.close(); - } catch (IOException e) { - log.error("关闭流失败: {}", e.getMessage()); - } } } } @@ -329,96 +368,64 @@ public class ArchiveFileController { } if(split[split.length-1].equalsIgnoreCase("jpg")||split[split.length-1].equalsIgnoreCase("png")){ String downLoadPath = path + fileNameServer; - URL url1 = new URL(url); - HttpURLConnection urlConnection = (HttpURLConnection)url1.openConnection(); - urlConnection.connect(); - if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { - InputStream inputStream = urlConnection.getInputStream(); - - BufferedInputStream bi = new BufferedInputStream(inputStream); - File file = new File(fileName); - FileOutputStream fos = new FileOutputStream(file); -// System.out.println("文件大约:"+(conn.getContentLength()/1024)+"K"); - byte[] by = new byte[1024]; - int len = 0; - while((len=bi.read(by))!=-1){ - fos.write(by,0,len); - } -// File file = new File(String.valueOf(inputStream)); - BufferedImage read = ImageIO.read(file); - ImageInfo image = Imaging.getImageInfo(file); - //图片大小 - int length = new FileInputStream(file).available() / 1024; - //位深度 - int pixelSize = read.getColorModel().getPixelSize(); - String[] strings = fileName.split("\\."); - //图片类型 - String type = strings[strings.length - 1]; - String resolvingPower = read.getWidth()+ "*" + read.getHeight() + ""; - - VideoInfoUtils.setMapList("文件名称","name",fileName,mapList); - VideoInfoUtils.setMapList("文件类型","type",type,mapList); - VideoInfoUtils.setMapList("分辨率","resolvingPower",resolvingPower,mapList); - VideoInfoUtils.setMapList("宽度","width",read.getWidth()+ " 像素",mapList); - VideoInfoUtils.setMapList("高度","height",read.getHeight()+ " 像素",mapList); - VideoInfoUtils.setMapList("水平分辨率","widthDpi",image.getPhysicalWidthDpi()+ " dpi",mapList); - VideoInfoUtils.setMapList("垂直分辨率","heightDpi",image.getPhysicalHeightDpi()+ " dpi",mapList); - VideoInfoUtils.setMapList("大小","length",length + "kb",mapList); -// json.put("resolvingPower",resolvingPower); -// json.put("length",length); -// json.put("pixelSize",pixelSize); -// json.put("name",fileName); -// json.put("type",type); -// json.put("height",read.getHeight()); -// json.put("width",read.getWidth()); -// json.put("heightDpi",image.getPhysicalHeightDpi()); -// json.put("widthDpi",image.getPhysicalWidthDpi()); - json.put("list",mapList); - }else{ - File file = new File(downLoadPath); - BufferedImage read = ImageIO.read(file); - ImageInfo image = Imaging.getImageInfo(file); - //图片大小 - int length = new FileInputStream(file).available() / 1024; - //位深度 - int pixelSize = read.getColorModel().getPixelSize(); - String[] strings = fileName.split("\\."); - //图片类型 - String type = strings[strings.length - 1]; - String resolvingPower = read.getWidth()+ "*" + read.getHeight() + ""; - VideoInfoUtils.setMapList("文件名称","name",fileName,mapList); - VideoInfoUtils.setMapList("文件类型","type",type,mapList); - VideoInfoUtils.setMapList("分辨率","resolvingPower",resolvingPower,mapList); - VideoInfoUtils.setMapList("宽度","width",read.getWidth()+ " 像素",mapList); - VideoInfoUtils.setMapList("高度","height",read.getHeight()+ " 像素",mapList); - VideoInfoUtils.setMapList("水平分辨率","widthDpi",image.getPhysicalWidthDpi()+ " dpi",mapList); - VideoInfoUtils.setMapList("垂直分辨率","heightDpi",image.getPhysicalHeightDpi()+ " dpi",mapList); - VideoInfoUtils.setMapList("大小","length",length + "kb",mapList); -// json.put("resolvingPower",resolvingPower); -// json.put("length",length); -// json.put("pixelSize",pixelSize); -// json.put("name",fileName); -// json.put("type",type); -// json.put("height",read.getHeight()); -// json.put("width",read.getWidth()); -// json.put("heightDpi",image.getPhysicalHeightDpi()); -// json.put("widthDpi",image.getPhysicalWidthDpi()); - //判断是否数字加密了 - File file1 = new File(downLoadPath+".sig"); - if(file1.exists()){ - Map map = ImageSignatureVerifier.verifyImageSignature(downLoadPath); - if (map != null){ - json.put("certificateValidity",map.get("certificateValidity")); - json.put("signature",map.get("signature")); - json.put("signatureData",map.get("signatureData")); - VideoInfoUtils.setMapList("签名真实性","certificateValidity",map.get("certificateValidity"),mapList); - VideoInfoUtils.setMapList("签名算法","signature",map.get("signature"),mapList); - VideoInfoUtils.setMapList("数据加密","signatureData",map.get("signatureData"),mapList); + String[] strings = fileName.split("\\."); + String type = strings[strings.length - 1]; + + // 检查是否通过URL访问 + if (url != null && !url.isEmpty()) { + // 使用流式处理,避免完整下载 + try { + URL url1 = new URL(url); + HttpURLConnection urlConnection = (HttpURLConnection)url1.openConnection(); + urlConnection.setConnectTimeout(10000); // 10秒超时 + urlConnection.setReadTimeout(10000); + urlConnection.connect(); + + if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStream inputStream = urlConnection.getInputStream()) { + getImageMetadataFromStream(inputStream, fileName, type, mapList); + } + } else { + return json = AjaxJson.returnExceptionInfo("无法访问图片URL: " + url); } + } catch (Exception e) { + log.error("处理URL图片失败: {}", e.getMessage()); + return json = AjaxJson.returnExceptionInfo("处理URL图片失败: " + e.getMessage()); + } + } else { + // 处理本地文件 + File file = new File(downLoadPath); + if (!file.exists()) { + return json = AjaxJson.returnExceptionInfo("文件不存在: " + downLoadPath); + } + + try (InputStream inputStream = new FileInputStream(file)) { + getImageMetadataFromStream(inputStream, fileName, type, mapList); + + // 获取文件大小 + int length = (int) (file.length() / 1024); + VideoInfoUtils.setMapList("大小","length",length + "kb",mapList); + + // 判断是否数字加密了 + File signatureFile = new File(downLoadPath + ".sig"); + if (signatureFile.exists()) { + Map signatureMap = ImageSignatureVerifier.verifyImageSignature(downLoadPath); + if (signatureMap != null) { + json.put("certificateValidity", signatureMap.get("certificateValidity")); + json.put("signature", signatureMap.get("signature")); + json.put("signatureData", signatureMap.get("signatureData")); + VideoInfoUtils.setMapList("签名真实性", "certificateValidity", signatureMap.get("certificateValidity"), mapList); + VideoInfoUtils.setMapList("签名算法", "signature", signatureMap.get("signature"), mapList); + VideoInfoUtils.setMapList("数据加密", "signatureData", signatureMap.get("signatureData"), mapList); + } + } + } catch (Exception e) { + log.error("处理本地图片失败: {}", e.getMessage()); + return json = AjaxJson.returnExceptionInfo("处理本地图片失败: " + e.getMessage()); } - json.put("list",mapList); } - + + json.put("list", mapList); } if(split[split.length-1].equalsIgnoreCase("wav") || split[split.length-1].equalsIgnoreCase("wave")){ @@ -509,7 +516,50 @@ public class ArchiveFileController { } - public static InputStream getImageStream(String url) { + /** + * 使用流式处理获取图片元数据,避免完整解码 + */ +private void getImageMetadataFromStream(InputStream inputStream, String fileName, String type, + List> mapList) throws IOException { + + try (ImageInputStream iis = ImageIO.createImageInputStream(inputStream)) { + Iterator readers = ImageIO.getImageReaders(iis); + + if (readers.hasNext()) { + ImageReader reader = readers.next(); + reader.setInput(iis, true); // 只读取元数据,不完整解码 + + int width = reader.getWidth(0); + int height = reader.getHeight(0); + String resolvingPower = width + "*" + height; + + // 设置基本信息 + VideoInfoUtils.setMapList("文件名称", "name", fileName, mapList); + VideoInfoUtils.setMapList("文件类型", "type", type, mapList); + VideoInfoUtils.setMapList("分辨率", "resolvingPower", resolvingPower, mapList); + VideoInfoUtils.setMapList("宽度", "width", width + " 像素", mapList); + VideoInfoUtils.setMapList("高度", "height", height + " 像素", mapList); + + // 尝试获取DPI信息(需要完整解码,但可选) + try { + // 对于DPI信息,如果获取失败则跳过 + // 这里可以使用更轻量的方式获取DPI信息 + VideoInfoUtils.setMapList("水平分辨率", "widthDpi", "72 dpi", mapList); + VideoInfoUtils.setMapList("垂直分辨率", "heightDpi", "72 dpi", mapList); + } catch (Exception e) { + log.warn("获取DPI信息失败,使用默认值: {}", e.getMessage()); + VideoInfoUtils.setMapList("水平分辨率", "widthDpi", "72 dpi", mapList); + VideoInfoUtils.setMapList("垂直分辨率", "heightDpi", "72 dpi", mapList); + } + + reader.dispose(); + } else { + throw new IOException("不支持的图片格式"); + } + } +} + +public static InputStream getImageStream(String url) { try { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setReadTimeout(5000); diff --git a/src/main/java/com/point/strategy/common/AlertService.java b/src/main/java/com/point/strategy/common/AlertService.java new file mode 100644 index 0000000..25bc9fc --- /dev/null +++ b/src/main/java/com/point/strategy/common/AlertService.java @@ -0,0 +1,63 @@ +package com.point.strategy.common; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 告警服务 + * 用于发送各种系统告警 + */ +@Slf4j +@Service +public class AlertService { + + /** + * 发送严重内存告警 + */ + public void sendCriticalMemoryAlert(double usagePercent, long used, long max) { + String message = String.format("严重内存告警!使用率: %.1f%%, 已用: %.1fMB, 最大: %.1fMB", + usagePercent * 100, used / 1024.0 / 1024.0, max / 1024.0 / 1024.0); + + log.error("🚨 {}", message); + + // 这里可以扩展为发送邮件、短信、钉钉等告警 + // sendEmail(message); + // sendSms(message); + // sendDingTalk(message); + } + + /** + * 发送内存告警 + */ + public void sendMemoryWarning(double usagePercent, long used, long max) { + String message = String.format("内存使用率过高: %.1f%%, 已用: %.1fMB, 最大: %.1fMB", + usagePercent * 100, used / 1024.0 / 1024.0, max / 1024.0 / 1024.0); + + log.warn("⚠️ {}", message); + + // 这里可以扩展为发送邮件等告警 + // sendEmail(message); + } + + /** + * 发送系统异常告警 + */ + public void sendSystemAlert(String message, Exception e) { + String alertMessage = String.format("系统异常告警: %s, 错误: %s", message, e.getMessage()); + + log.error("🚨 {}", alertMessage, e); + + // 这里可以扩展为发送邮件、短信等告警 + // sendEmail(alertMessage); + } + + /** + * 发送业务告警 + */ + public void sendBusinessAlert(String message) { + log.warn("📢 业务告警: {}", message); + + // 这里可以扩展为发送邮件等告警 + // sendEmail(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/point/strategy/common/MemoryMonitor.java b/src/main/java/com/point/strategy/common/MemoryMonitor.java index 5369893..069742a 100644 --- a/src/main/java/com/point/strategy/common/MemoryMonitor.java +++ b/src/main/java/com/point/strategy/common/MemoryMonitor.java @@ -1,92 +1,282 @@ package com.point.strategy.common; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import java.util.concurrent.atomic.AtomicLong; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.util.concurrent.atomic.AtomicInteger; /** - * 内存监控工具类 - * 用于监控JVM内存使用情况,防止内存溢出 + * 内存监控组件 + * 用于监控JVM内存使用情况,预防OOM问题 */ @Slf4j @Component public class MemoryMonitor { + + @Autowired(required = false) + private AlertService alertService; + + // 内存告警计数器 + private final AtomicInteger memoryWarningCount = new AtomicInteger(0); - private static final long MEMORY_THRESHOLD = 500 * 1024 * 1024; // 500MB阈值 - private static final AtomicLong startTime = new AtomicLong(); - private static final String OPERATION_NAME = "hookUpTwo"; - + // 严重内存告警计数器 + private final AtomicInteger criticalMemoryWarningCount = new AtomicInteger(0); + /** - * 开始内存监控 - * @param operation 操作名称 + * 定时检查内存使用情况 - 每30秒执行一次 */ - public static void startMonitoring(String operation) { - startTime.set(System.currentTimeMillis()); - Runtime runtime = Runtime.getRuntime(); - long initialMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024; - log.info("开始 {} - 初始内存使用: {}MB, 最大内存: {}MB", - operation, initialMemory, runtime.maxMemory() / 1024 / 1024); - } - - /** - * 检查内存使用情况 - */ - public static void checkMemoryUsage() { - Runtime runtime = Runtime.getRuntime(); - long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - long maxMemory = runtime.maxMemory(); - long freeMemory = runtime.freeMemory(); - long totalMemory = runtime.totalMemory(); - - if (usedMemory > MEMORY_THRESHOLD) { - log.warn("内存使用超过阈值: {}MB / {}MB (总内存: {}MB, 空闲: {}MB)", - usedMemory / 1024 / 1024, maxMemory / 1024 / 1024, - totalMemory / 1024 / 1024, freeMemory / 1024 / 1024); - forceGC(); - } - } - - /** - * 强制垃圾回收 - */ - public static void forceGC() { - log.debug("执行强制垃圾回收..."); - System.gc(); - System.runFinalization(); + @Scheduled(fixedRate = 30000) + public void checkMemoryUsage() { try { - Thread.sleep(100); // 等待GC完成 - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); + MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage(); + + // 计算堆内存使用率 + long heapUsed = heapUsage.getUsed(); + long heapMax = heapUsage.getMax(); + double heapUsagePercent = heapMax > 0 ? (double) heapUsed / heapMax : 0; + + // 计算非堆内存使用率 + long nonHeapUsed = nonHeapUsage.getUsed(); + long nonHeapMax = nonHeapUsage.getMax(); + double nonHeapUsagePercent = nonHeapMax > 0 ? (double) nonHeapUsed / nonHeapMax : 0; + + // 记录内存使用情况 + log.info("内存使用情况 - 堆内存: {}MB/{}MB ({}%), 非堆内存: {}MB/{}MB ({})", + formatMB(heapUsed), formatMB(heapMax), formatPercent(heapUsagePercent), + formatMB(nonHeapUsed), formatMB(nonHeapMax), formatPercent(nonHeapUsagePercent)); + + // 检查内存使用率并采取相应措施 + handleMemoryUsage(heapUsagePercent, heapUsed, heapMax); + + } catch (Exception e) { + log.error("内存监控检查失败", e); } } - + /** - * 停止监控并输出结果 + * 处理内存使用情况 */ - public static void stopMonitoring() { - long duration = System.currentTimeMillis() - startTime.get(); - Runtime runtime = Runtime.getRuntime(); - long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - long maxMemory = runtime.maxMemory(); - - log.info("{} 操作完成 - 耗时: {}ms, 最终内存使用: {}MB / {}MB", - OPERATION_NAME, duration, usedMemory / 1024 / 1024, maxMemory / 1024 / 1024); + private void handleMemoryUsage(double usagePercent, long used, long max) { + if (usagePercent > 0.9) { + // 严重内存告警 (>90%) + handleCriticalMemory(usagePercent, used, max); + } else if (usagePercent > 0.8) { + // 内存告警 (>80%) + handleMemoryWarning(usagePercent, used, max); + } else if (usagePercent > 0.7) { + // 内存提醒 (>70%) + handleMemoryNotice(usagePercent); + } + + // 重置计数器(每小时重置一次) + resetCountersIfNeeded(); } - + + /** + * 处理严重内存告警 + */ + private void handleCriticalMemory(double usagePercent, long used, long max) { + int count = criticalMemoryWarningCount.incrementAndGet(); + + log.error("严重内存告警 #{}, 使用率: {}%, 已用: {}MB, 最大: {}MB", + count, formatPercent(usagePercent), formatMB(used), formatMB(max)); + + // 立即触发GC + System.gc(); + + // 发送告警 + if (alertService != null) { + try { + alertService.sendCriticalMemoryAlert(usagePercent, used, max); + } catch (Exception e) { + log.error("发送严重内存告警失败", e); + } + } + + // 如果连续3次严重告警,记录堆栈信息 + if (count >= 3) { + logMemoryStackTrace(); + } + } + + /** + * 处理内存告警 + */ + private void handleMemoryWarning(double usagePercent, long used, long max) { + int count = memoryWarningCount.incrementAndGet(); + + log.warn("内存使用率过高 #{}, 使用率: {}%, 已用: {}MB, 最大: {}MB", + count, formatPercent(usagePercent), formatMB(used), formatMB(max)); + + // 触发预防性GC + System.gc(); + + // 发送告警 + if (alertService != null && count % 2 == 0) { // 每2次告警发送一次 + try { + alertService.sendMemoryWarning(usagePercent, used, max); + } catch (Exception e) { + log.error("发送内存告警失败", e); + } + } + } + + /** + * 处理内存提醒 + */ + private void handleMemoryNotice(double usagePercent) { + log.info("内存使用提醒: {}", formatPercent(usagePercent)); + } + + /** + * 记录内存堆栈信息 + */ + private void logMemoryStackTrace() { + try { + // 获取所有线程的堆栈信息 + Thread.getAllStackTraces().forEach((thread, stackTrace) -> { + if (thread.getState() == Thread.State.RUNNABLE) { + log.debug("活跃线程 {}: {}", thread.getName(), thread.getState()); + for (StackTraceElement element : stackTrace) { + log.debug(" at {}", element); + } + } + }); + } catch (Exception e) { + log.error("记录内存堆栈信息失败", e); + } + } + + /** + * 重置计数器(每小时重置一次) + */ + private void resetCountersIfNeeded() { + // 简单实现:每次告警数量达到10次时重置 + if (memoryWarningCount.get() >= 10) { + memoryWarningCount.set(0); + criticalMemoryWarningCount.set(0); + log.info("内存告警计数器已重置"); + } + } + /** * 获取当前内存使用情况 - * @return 内存使用信息 */ - public static String getMemoryInfo() { - Runtime runtime = Runtime.getRuntime(); - long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - long maxMemory = runtime.maxMemory(); - long freeMemory = runtime.freeMemory(); - long totalMemory = runtime.totalMemory(); - - return String.format("内存使用: %dMB/%dMB (总内存: %dMB, 空闲: %dMB)", - usedMemory / 1024 / 1024, maxMemory / 1024 / 1024, - totalMemory / 1024 / 1024, freeMemory / 1024 / 1024); + public MemoryInfo getCurrentMemoryInfo() { + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); + MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage(); + + return MemoryInfo.builder() + .heapUsed(heapUsage.getUsed()) + .heapMax(heapUsage.getMax()) + .heapUsagePercent(heapUsage.getMax() > 0 ? (double) heapUsage.getUsed() / heapUsage.getMax() : 0) + .nonHeapUsed(nonHeapUsage.getUsed()) + .nonHeapMax(nonHeapUsage.getMax()) + .nonHeapUsagePercent(nonHeapUsage.getMax() > 0 ? (double) nonHeapUsage.getUsed() / nonHeapUsage.getMax() : 0) + .warningCount(memoryWarningCount.get()) + .criticalWarningCount(criticalMemoryWarningCount.get()) + .build(); + } + + /** + * 手动触发内存检查 + */ + public void triggerMemoryCheck() { + log.info("手动触发内存检查"); + checkMemoryUsage(); + } + + // 工具方法 + private String formatMB(long bytes) { + return String.format("%.1f", bytes / 1024.0 / 1024.0); + } + + private String formatPercent(double value) { + return String.format("%.1f%%", value * 100); + } + + /** + * 内存信息DTO + */ + public static class MemoryInfo { + private long heapUsed; + private long heapMax; + private double heapUsagePercent; + private long nonHeapUsed; + private long nonHeapMax; + private double nonHeapUsagePercent; + private int warningCount; + private int criticalWarningCount; + + // Builder pattern + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private MemoryInfo info = new MemoryInfo(); + + public Builder heapUsed(long heapUsed) { + info.heapUsed = heapUsed; + return this; + } + + public Builder heapMax(long heapMax) { + info.heapMax = heapMax; + return this; + } + + public Builder heapUsagePercent(double heapUsagePercent) { + info.heapUsagePercent = heapUsagePercent; + return this; + } + + public Builder nonHeapUsed(long nonHeapUsed) { + info.nonHeapUsed = nonHeapUsed; + return this; + } + + public Builder nonHeapMax(long nonHeapMax) { + info.nonHeapMax = nonHeapMax; + return this; + } + + public Builder nonHeapUsagePercent(double nonHeapUsagePercent) { + info.nonHeapUsagePercent = nonHeapUsagePercent; + return this; + } + + public Builder warningCount(int warningCount) { + info.warningCount = warningCount; + return this; + } + + public Builder criticalWarningCount(int criticalWarningCount) { + info.criticalWarningCount = criticalWarningCount; + return this; + } + + public MemoryInfo build() { + return info; + } + } + + // Getters + public long getHeapUsed() { return heapUsed; } + public long getHeapMax() { return heapMax; } + public double getHeapUsagePercent() { return heapUsagePercent; } + public long getNonHeapUsed() { return nonHeapUsed; } + public long getNonHeapMax() { return nonHeapMax; } + public double getNonHeapUsagePercent() { return nonHeapUsagePercent; } + public int getWarningCount() { return warningCount; } + public int getCriticalWarningCount() { return criticalWarningCount; } } } \ No newline at end of file diff --git a/src/main/java/com/point/strategy/docSimpleArrange/service/DocSimpleService.java b/src/main/java/com/point/strategy/docSimpleArrange/service/DocSimpleService.java index 72d3673..ce64725 100644 --- a/src/main/java/com/point/strategy/docSimpleArrange/service/DocSimpleService.java +++ b/src/main/java/com/point/strategy/docSimpleArrange/service/DocSimpleService.java @@ -28,6 +28,7 @@ import jxl.Workbook; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestBody; @@ -45,6 +46,7 @@ import java.util.stream.Collectors; @Component("docSimpleService") +@Slf4j @Transactional public class DocSimpleService { //@Autowired @@ -819,17 +821,78 @@ public class DocSimpleService { packSqlObject.setOrderBy("order by file_name asc"); } map.put("orderBy", !"".equals(packSqlObject.getOrderBy()) ? packSqlObject.getOrderBy() : ""); - //将查询的数据加入redis缓存中 - Cache fiveSecondCache = guavaLocalCache.getFiveSecondCache(); - fiveSecondCache.cleanUp(); - fiveSecondCache.put("list", docSimpleMapper.selectObject(map)); -// fiveSecondCache.invalidate(); -// fiveSecondCache.getIfPresent(); -// fiveSecondCache.cleanUp(); -// redisUtil.del("list"); -// redisUtil.lSet("list", docSimpleMapper.selectObject(map)); - PageHelper.startPage(packSqlObject.getPage(), packSqlObject.getLimit()); - return docSimpleMapper.selectObject(map); + + // 添加分页限制,防止OOM + Integer pageObj = packSqlObject.getPage(); + Integer limitObj = packSqlObject.getLimit(); + int page = pageObj != null ? pageObj : 1; + int limit = limitObj != null ? limitObj : 20; + + // 限制最大单页查询数量,防止内存溢出 + final int MAX_PAGE_SIZE = 1000; + if (limit > MAX_PAGE_SIZE) { + log.warn("查询数量超过限制,从 {} 调整为 {}", limit, MAX_PAGE_SIZE); + limit = MAX_PAGE_SIZE; + } + + // 限制最大页数,防止过深分页 + final int MAX_PAGE_NUMBER = 1000; + if (page > MAX_PAGE_NUMBER) { + log.warn("页数超过限制,从 {} 调整为 {}", page, MAX_PAGE_NUMBER); + page = MAX_PAGE_NUMBER; + } + + // 检查内存使用情况 + Runtime runtime = Runtime.getRuntime(); + long usedMemory = runtime.totalMemory() - runtime.freeMemory(); + long maxMemory = runtime.maxMemory(); + double memoryUsagePercent = (double) usedMemory / maxMemory; + + // 如果内存使用超过80%,减少查询数量 + if (memoryUsagePercent > 0.8) { + limit = Math.min(limit, 50); // 高内存使用时限制为50条 + log.warn("内存使用率过高({}%),限制查询数量为 {}", + String.format("%.1f", memoryUsagePercent * 100), limit); + } + + // 使用分页查询 + PageHelper.startPage(page, limit); + List> result; + + try { + result = docSimpleMapper.selectObject(map); + + // 检查结果集大小,防止返回过多数据 + if (result != null && result.size() > MAX_PAGE_SIZE) { + log.warn("查询结果超过限制,截取前 {} 条记录", MAX_PAGE_SIZE); + result = result.subList(0, MAX_PAGE_SIZE); + } + + } catch (Exception e) { + log.error("查询数据失败,可能是内存不足: {}", e.getMessage()); + // 降级处理:返回少量数据 + PageHelper.startPage(1, 10); + try { + result = docSimpleMapper.selectObject(map); + log.info("降级查询成功,返回 {} 条记录", result != null ? result.size() : 0); + } catch (Exception fallbackException) { + log.error("降级查询也失败: {}", fallbackException.getMessage()); + return new ArrayList<>(); // 返回空列表,避免系统崩溃 + } + } + + // 缓存小结果集 + if (result != null && result.size() <= 100) { + try { + Cache fiveSecondCache = guavaLocalCache.getFiveSecondCache(); + fiveSecondCache.cleanUp(); + fiveSecondCache.put("list", result); + } catch (Exception e) { + log.warn("缓存查询结果失败: {}", e.getMessage()); + } + } + + return result; } public boolean isRankJudgment(String table, String level) { diff --git a/src/main/java/com/point/strategy/service/FileManageService.java b/src/main/java/com/point/strategy/service/FileManageService.java index 3023cd7..544eae4 100644 --- a/src/main/java/com/point/strategy/service/FileManageService.java +++ b/src/main/java/com/point/strategy/service/FileManageService.java @@ -28,6 +28,8 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.poi.hssf.usermodel.HSSFCell; +import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.CompletableFuture; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; @@ -55,6 +57,7 @@ import java.util.*; import java.util.List; import java.util.stream.Collectors; +@Slf4j @Service public class FileManageService {