diff --git a/build.gradle b/build.gradle index cee8bd9..f76f2c4 100644 --- a/build.gradle +++ b/build.gradle @@ -147,6 +147,9 @@ dependencies { implementation 'com.google.zxing:core:3.5.3' implementation 'com.google.zxing:javase:3.5.3' + // 视频转码(内置FFmpeg native,无需系统安装ffmpeg) + implementation 'org.bytedeco:javacv-platform:1.5.10' + } test { diff --git a/src/main/java/com/xujie/sys/config/ShiroConfig.java b/src/main/java/com/xujie/sys/config/ShiroConfig.java index b727b8e..420265f 100644 --- a/src/main/java/com/xujie/sys/config/ShiroConfig.java +++ b/src/main/java/com/xujie/sys/config/ShiroConfig.java @@ -48,6 +48,7 @@ public class ShiroConfig { filterMap.put("/tcp", "anon"); filterMap.put("/ckp-file/**", "anon"); filterMap.put("/oss/2/**", "anon"); + filterMap.put("/oss/video/**", "anon"); filterMap.put("/oss/previewOssFile", "anon"); // filterMap.put("/**", "authc"); filterMap.put("/webjars/**", "anon"); diff --git a/src/main/java/com/xujie/sys/modules/longchuang/service/impl/ProductionPlanServiceImpl.java b/src/main/java/com/xujie/sys/modules/longchuang/service/impl/ProductionPlanServiceImpl.java index 672718b..5413c0f 100644 --- a/src/main/java/com/xujie/sys/modules/longchuang/service/impl/ProductionPlanServiceImpl.java +++ b/src/main/java/com/xujie/sys/modules/longchuang/service/impl/ProductionPlanServiceImpl.java @@ -27,6 +27,11 @@ import com.xujie.sys.modules.oss.entity.SysOssEntity; import com.xujie.sys.modules.oss.service.SysOssService; import com.xujie.sys.modules.sys.entity.SysUserEntity; import lombok.extern.slf4j.Slf4j; +import org.bytedeco.ffmpeg.global.avcodec; +import org.bytedeco.ffmpeg.global.avutil; +import org.bytedeco.javacv.FFmpegFrameGrabber; +import org.bytedeco.javacv.FFmpegFrameRecorder; +import org.bytedeco.javacv.Frame; import org.apache.shiro.SecurityUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -41,6 +46,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; @@ -69,6 +75,8 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { private static final String NODE_REPORT_MODE_PARALLEL = "PARALLEL"; private static final String NODE_REPORT_MODE_SEQUENTIAL = "SEQUENTIAL"; private static final String NODE_MEDIA_REF_TYPE = "LONGCHUANG_NODE_REPORT_MEDIA"; + private static final String MEDIA_INFO_VIDEO_STANDARD = "VIDEO_STD_MP4_H264_AAC"; + private static final String MEDIA_INFO_VIDEO_RAW = "VIDEO_RAW"; @Autowired private ProductionPlanMapper productionPlanMapper; @@ -79,6 +87,9 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { @Value("${longchuang.production-report.media-root:D:\\longchuang}") private String workReportMediaRootPath; + @Value("${longchuang.production-report.enable-video-transcode:true}") + private boolean enableVideoTranscode; + @Override public PageUtils queryHomeLiftOrderPage(ProductionPlanOrderQueryData data) { return queryOrderPage(data, ORDER_TYPE_HOME_LIFT); @@ -856,18 +867,42 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { if (file == null || file.isEmpty()) { continue; } - String suffix = resolveMediaSuffix(file.getOriginalFilename(), file.getContentType()); - String newFileName = buildMediaFileName(suffix); + String originalFileName = file.getOriginalFilename(); + String suffix = resolveMediaSuffix(originalFileName, file.getContentType()); + boolean videoFile = isVideoMedia(file.getContentType(), suffix); + boolean transcodeVideo = videoFile && enableVideoTranscode; + String targetSuffix = transcodeVideo ? "mp4" : (StringUtils.hasText(suffix) ? suffix : (videoFile ? "mp4" : "bin")); + String newFileName = buildMediaFileName(targetSuffix); File targetFile = new File(nodeDateFolder, newFileName); - file.transferTo(targetFile); + if (transcodeVideo) { + String sourceSuffix = StringUtils.hasText(suffix) ? suffix : "bin"; + File sourceFile = File.createTempFile("lc_media_src_", "." + sourceSuffix); + try { + file.transferTo(sourceFile); + transcodeVideoToMp4(sourceFile, targetFile); + } finally { + if (sourceFile.exists()) { + sourceFile.delete(); + } + } + } else { + file.transferTo(targetFile); + } + if (!targetFile.exists() || targetFile.length() <= 0) { + throw new XJException("报工影像上传失败,请重试"); + } savedFileList.add(targetFile); SysOssEntity ossEntity = new SysOssEntity(); ossEntity.setUrl(targetFile.getAbsolutePath()); ossEntity.setCreateDate(new Date()); - ossEntity.setFileName(StringUtils.hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : newFileName); + String displayFileName = StringUtils.hasText(originalFileName) ? originalFileName : newFileName; + if (transcodeVideo) { + displayFileName = normalizeVideoDisplayName(displayFileName); + } + ossEntity.setFileName(displayFileName); ossEntity.setNewFileName(newFileName); - ossEntity.setFileType(suffix); + ossEntity.setFileType(targetSuffix); ossEntity.setCreatedBy(userName); ossEntity.setOrderRef1(data.getOrderNo()); ossEntity.setOrderRef2(node.getNodeCode()); @@ -876,7 +911,11 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { ossEntity.setOrderRef5(String.valueOf(userId)); ossEntity.setOrderRef6(node.getNodeName()); ossEntity.setOrderReftype(NODE_MEDIA_REF_TYPE); - ossEntity.setCAdditionalInfo(resolveMediaCategory(file.getContentType())); + String mediaAdditionalInfo = resolveMediaCategory(file.getContentType()); + if (videoFile) { + mediaAdditionalInfo = transcodeVideo ? MEDIA_INFO_VIDEO_STANDARD : MEDIA_INFO_VIDEO_RAW; + } + ossEntity.setCAdditionalInfo(mediaAdditionalInfo); ossEntity.setConclusion(userName); sysOssService.save(ossEntity); uploadCount++; @@ -887,6 +926,11 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { savedFile.delete(); } } + log.error("报工影像保存失败,orderNo={}, nodeCode={}, logNo={}, error={}", + data.getOrderNo(), data.getNodeCode(), logNo, e.getMessage(), e); + if (e instanceof XJException) { + throw (XJException) e; + } throw new XJException("报工影像上传失败,请重试"); } if (uploadCount <= 0) { @@ -901,6 +945,28 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { return folderName.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); } + private boolean isVideoMedia(String contentType, String suffix) { + if (StringUtils.hasText(contentType) && contentType.toLowerCase().startsWith("video/")) { + return true; + } + if (!StringUtils.hasText(suffix)) { + return false; + } + String lowerSuffix = suffix.toLowerCase(); + return Arrays.asList("mp4", "webm", "mov", "avi", "m4v", "3gp", "mkv").contains(lowerSuffix); + } + + private String normalizeVideoDisplayName(String originalFileName) { + if (!StringUtils.hasText(originalFileName)) { + return "video.mp4"; + } + int dotIndex = originalFileName.lastIndexOf("."); + if (dotIndex >= 0) { + return originalFileName.substring(0, dotIndex) + ".mp4"; + } + return originalFileName + ".mp4"; + } + private String resolveMediaSuffix(String originalFileName, String contentType) { if (StringUtils.hasText(originalFileName)) { int dotIndex = originalFileName.lastIndexOf("."); @@ -924,6 +990,15 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { if (lowerType.contains("mp4")) { return "mp4"; } + if (lowerType.contains("quicktime")) { + return "mov"; + } + if (lowerType.contains("avi")) { + return "avi"; + } + if (lowerType.contains("3gpp")) { + return "3gp"; + } return "bin"; } @@ -933,6 +1008,82 @@ public class ProductionPlanServiceImpl implements ProductionPlanService { return "LCM_" + timeText + "_" + randomText + "." + suffix; } + private void transcodeVideoToMp4(File sourceFile, File targetFile) { + if (sourceFile == null || !sourceFile.exists()) { + throw new XJException("视频源文件不存在,无法转码"); + } + if (targetFile.exists() && !targetFile.delete()) { + throw new XJException("视频转码目标文件清理失败"); + } + FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(sourceFile); + FFmpegFrameRecorder recorder = null; + try { + grabber.start(); + int width = grabber.getImageWidth(); + int height = grabber.getImageHeight(); + if (width <= 0 || height <= 0) { + throw new XJException("视频转码失败:无法识别视频分辨率"); + } + + int audioChannels = Math.max(grabber.getAudioChannels(), 0); + recorder = new FFmpegFrameRecorder(targetFile, width, height, audioChannels); + recorder.setInterleaved(true); + recorder.setFormat("mp4"); + recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); + recorder.setVideoOption("preset", "veryfast"); + recorder.setVideoOption("crf", "23"); + recorder.setVideoOption("movflags", "+faststart"); + recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); + + double frameRate = grabber.getVideoFrameRate(); + if (frameRate > 0) { + recorder.setFrameRate(frameRate); + } else { + recorder.setFrameRate(25); + } + int videoBitrate = grabber.getVideoBitrate(); + recorder.setVideoBitrate(videoBitrate > 0 ? videoBitrate : 1500_000); + + if (audioChannels > 0) { + recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); + int sampleRate = grabber.getSampleRate(); + recorder.setSampleRate(sampleRate > 0 ? sampleRate : 44100); + recorder.setAudioChannels(audioChannels); + int audioBitrate = grabber.getAudioBitrate(); + recorder.setAudioBitrate(audioBitrate > 0 ? audioBitrate : 128000); + } + + recorder.start(); + Frame frame; + while ((frame = grabber.grabFrame()) != null) { + recorder.record(frame); + } + + recorder.stop(); + grabber.stop(); + if (!targetFile.exists() || targetFile.length() <= 0) { + throw new XJException("视频转码失败:目标文件为空"); + } + } catch (XJException e) { + throw e; + } catch (Exception e) { + log.error("视频转码失败,source={}, target={}, error={}", + sourceFile.getAbsolutePath(), targetFile.getAbsolutePath(), e.getMessage(), e); + throw new XJException("视频转码失败,请重试"); + } finally { + try { + if (recorder != null) { + recorder.release(); + } + } catch (Exception ignored) { + } + try { + grabber.release(); + } catch (Exception ignored) { + } + } + } + private String resolveMediaCategory(String contentType) { if (!StringUtils.hasText(contentType)) { return "OTHER"; diff --git a/src/main/java/com/xujie/sys/modules/oss/controller/OssController.java b/src/main/java/com/xujie/sys/modules/oss/controller/OssController.java index 5c7094e..93dd58c 100644 --- a/src/main/java/com/xujie/sys/modules/oss/controller/OssController.java +++ b/src/main/java/com/xujie/sys/modules/oss/controller/OssController.java @@ -5,13 +5,20 @@ import com.xujie.sys.common.utils.OfficeConverter; import com.xujie.sys.common.utils.R; import com.xujie.sys.modules.oss.entity.SysOssEntity; import com.xujie.sys.modules.oss.service.SysOssService; +import org.bytedeco.ffmpeg.global.avcodec; +import org.bytedeco.ffmpeg.global.avutil; +import org.bytedeco.javacv.FFmpegFrameGrabber; +import org.bytedeco.javacv.FFmpegFrameRecorder; +import org.bytedeco.javacv.Frame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.*; import java.net.HttpURLConnection; @@ -20,14 +27,25 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; @RestController @RequestMapping("/oss") public class OssController { private static final Logger logger = LoggerFactory.getLogger(OssController.class); + private static final String MEDIA_INFO_VIDEO_STANDARD = "VIDEO_STD_MP4_H264_AAC"; + @Value("${sys-file.mes-url}") private String mesUrl; + + @Value("${longchuang.production-report.enable-playback-transcode:false}") + private boolean enablePlaybackTranscode; + + @Value("${longchuang.production-report.playback-transcode-force-mp4:false}") + private boolean playbackTranscodeForceMp4; + + private final ConcurrentHashMap playbackTranscodeLockMap = new ConcurrentHashMap<>(); @Autowired private SysOssService sysOssService; @@ -75,6 +93,74 @@ public class OssController { sysOssService.previewOssFileById2(id,response); } + @GetMapping("/video/{id}") + public void previewVideoStream(@PathVariable("id") Long id, HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + SysOssEntity attachment = sysOssService.getById(id); + if (attachment == null || !StringUtils.hasText(attachment.getUrl())) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "视频不存在"); + return; + } + File sourceFile = new File(attachment.getUrl()); + if (!sourceFile.exists() || !sourceFile.isFile()) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "视频文件不存在"); + return; + } + File videoFile = sourceFile; + boolean playbackTranscoded = false; + if (shouldPlaybackTranscode(attachment, sourceFile)) { + videoFile = ensurePlaybackVideoFile(sourceFile); + playbackTranscoded = !sourceFile.getAbsolutePath().equals(videoFile.getAbsolutePath()); + } + long fileLength = videoFile.length(); + if (fileLength <= 0) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "视频文件为空"); + return; + } + + String contentType = playbackTranscoded ? "video/mp4" : resolveVideoContentType(attachment, videoFile); + response.setContentType(contentType); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + response.setHeader("Content-Disposition", "inline"); + + String rangeHeader = request.getHeader("Range"); + if (!StringUtils.hasText(rangeHeader) || !rangeHeader.startsWith("bytes=")) { + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader("Content-Length", String.valueOf(fileLength)); + writeVideoRange(videoFile, 0, fileLength - 1, response); + return; + } + + long[] range = resolveVideoRange(rangeHeader, fileLength); + if (range == null) { + response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + response.setHeader("Content-Range", "bytes */" + fileLength); + return; + } + + long start = range[0]; + long end = range[1]; + long contentLength = end - start + 1; + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength); + response.setHeader("Content-Length", String.valueOf(contentLength)); + writeVideoRange(videoFile, start, end, response); + } catch (Exception e) { + if (isClientAbortException(e)) { + logger.debug("视频流已由客户端主动断开,id={}, msg={}", id, e.getMessage()); + return; + } + if (!response.isCommitted()) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "视频预览失败"); + return; + } + throw e; + } + } + /** * 通过OSS服务代理预览或下载附件 * OSS接口:POST http://172.26.68.17:8091//oss/2/{id} @@ -285,9 +371,243 @@ public class OssController { case "rar": return "application/x-rar-compressed"; case "7z": return "application/x-7z-compressed"; case "mp4": return "video/mp4"; + case "m4v": return "video/mp4"; + case "webm": return "video/webm"; + case "mov": return "video/quicktime"; + case "3gp": return "video/3gpp"; case "avi": return "video/x-msvideo"; + case "mkv": return "video/x-matroska"; case "mp3": return "audio/mpeg"; default: return "application/octet-stream"; } } + + private String resolveVideoContentType(SysOssEntity attachment, File videoFile) { + String ext = ""; + if (attachment != null && StringUtils.hasText(attachment.getFileType())) { + ext = attachment.getFileType(); + } + if (!StringUtils.hasText(ext) && attachment != null && StringUtils.hasText(attachment.getFileName())) { + ext = resolveExtByName(attachment.getFileName()); + } + if (!StringUtils.hasText(ext) && videoFile != null) { + ext = resolveExtByName(videoFile.getName()); + } + String resolved = getContentType(ext); + if (StringUtils.hasText(resolved) && !"application/octet-stream".equalsIgnoreCase(resolved)) { + return resolved; + } + return "video/mp4"; + } + + private String resolveExtByName(String fileName) { + String raw = fileName == null ? "" : fileName.trim(); + int index = raw.lastIndexOf("."); + if (index < 0 || index >= raw.length() - 1) { + return ""; + } + return raw.substring(index + 1); + } + + private boolean shouldPlaybackTranscode(SysOssEntity attachment, File sourceFile) { + if (!enablePlaybackTranscode || sourceFile == null) { + return false; + } + if (attachment != null && StringUtils.hasText(attachment.getCAdditionalInfo())) { + String additionalInfo = attachment.getCAdditionalInfo().trim().toUpperCase(); + if (additionalInfo.contains(MEDIA_INFO_VIDEO_STANDARD)) { + return false; + } + } + String sourceName = sourceFile.getName(); + if (sourceName.endsWith("_stream.mp4")) { + return false; + } + String ext = resolveExtByName(sourceName); + if (!StringUtils.hasText(ext)) { + return true; + } + String lowerExt = ext.toLowerCase(); + if ("mp4".equals(lowerExt)) { + return playbackTranscodeForceMp4; + } + return true; + } + + private File ensurePlaybackVideoFile(File sourceFile) { + if (sourceFile == null || !sourceFile.exists() || !sourceFile.isFile()) { + return sourceFile; + } + String sourceName = sourceFile.getName(); + int dotIndex = sourceName.lastIndexOf("."); + String baseName = dotIndex > 0 ? sourceName.substring(0, dotIndex) : sourceName; + File cachedFile = new File(sourceFile.getParentFile(), baseName + "_stream.mp4"); + if (cachedFile.exists() && cachedFile.length() > 0 && cachedFile.lastModified() >= sourceFile.lastModified()) { + return cachedFile; + } + + String lockKey = sourceFile.getAbsolutePath(); + Object lock = playbackTranscodeLockMap.computeIfAbsent(lockKey, key -> new Object()); + synchronized (lock) { + try { + if (cachedFile.exists() && cachedFile.length() > 0 && cachedFile.lastModified() >= sourceFile.lastModified()) { + return cachedFile; + } + transcodeVideoForPlayback(sourceFile, cachedFile); + if (cachedFile.exists() && cachedFile.length() > 0) { + return cachedFile; + } + return sourceFile; + } catch (Exception e) { + logger.error("播放转码失败,source={}, cache={}, error={}", + sourceFile.getAbsolutePath(), cachedFile.getAbsolutePath(), e.getMessage(), e); + return sourceFile; + } finally { + playbackTranscodeLockMap.remove(lockKey); + } + } + } + + private void transcodeVideoForPlayback(File sourceFile, File targetFile) throws Exception { + if (targetFile.exists() && !targetFile.delete()) { + throw new IOException("播放转码目标文件清理失败"); + } + FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(sourceFile); + FFmpegFrameRecorder recorder = null; + try { + grabber.start(); + int width = grabber.getImageWidth(); + int height = grabber.getImageHeight(); + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("视频分辨率无效"); + } + + int audioChannels = Math.max(grabber.getAudioChannels(), 0); + recorder = new FFmpegFrameRecorder(targetFile, width, height, audioChannels); + recorder.setInterleaved(true); + recorder.setFormat("mp4"); + recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); + recorder.setVideoOption("preset", "veryfast"); + recorder.setVideoOption("crf", "23"); + recorder.setVideoOption("movflags", "+faststart"); + recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); + + double frameRate = grabber.getVideoFrameRate(); + recorder.setFrameRate(frameRate > 0 ? frameRate : 25); + int videoBitrate = grabber.getVideoBitrate(); + recorder.setVideoBitrate(videoBitrate > 0 ? videoBitrate : 1500_000); + + if (audioChannels > 0) { + recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); + int sampleRate = grabber.getSampleRate(); + recorder.setSampleRate(sampleRate > 0 ? sampleRate : 44100); + recorder.setAudioChannels(audioChannels); + int audioBitrate = grabber.getAudioBitrate(); + recorder.setAudioBitrate(audioBitrate > 0 ? audioBitrate : 128000); + } + + recorder.start(); + Frame frame; + while ((frame = grabber.grabFrame()) != null) { + recorder.record(frame); + } + recorder.stop(); + grabber.stop(); + if (!targetFile.exists() || targetFile.length() <= 0) { + throw new IOException("播放转码结果为空"); + } + } finally { + try { + if (recorder != null) { + recorder.release(); + } + } catch (Exception ignored) { + } + try { + grabber.release(); + } catch (Exception ignored) { + } + } + } + + private long[] resolveVideoRange(String rangeHeader, long fileLength) { + try { + String rangeValue = rangeHeader.substring("bytes=".length()).trim(); + int commaIndex = rangeValue.indexOf(","); + if (commaIndex > -1) { + rangeValue = rangeValue.substring(0, commaIndex).trim(); + } + int dashIndex = rangeValue.indexOf("-"); + if (dashIndex < 0) { + return null; + } + String startText = rangeValue.substring(0, dashIndex).trim(); + String endText = rangeValue.substring(dashIndex + 1).trim(); + long start; + long end; + if (!StringUtils.hasText(startText)) { + long suffixLength = Long.parseLong(endText); + if (suffixLength <= 0) { + return null; + } + start = suffixLength >= fileLength ? 0 : fileLength - suffixLength; + end = fileLength - 1; + } else { + start = Long.parseLong(startText); + end = StringUtils.hasText(endText) ? Long.parseLong(endText) : fileLength - 1; + } + if (start < 0 || end < 0 || start >= fileLength) { + return null; + } + if (end >= fileLength) { + end = fileLength - 1; + } + if (end < start) { + return null; + } + return new long[]{start, end}; + } catch (Exception e) { + return null; + } + } + + private void writeVideoRange(File file, long start, long end, HttpServletResponse response) throws IOException { + OutputStream outputStream = response.getOutputStream(); + try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) { + randomAccessFile.seek(start); + byte[] buffer = new byte[8192]; + long remain = end - start + 1; + while (remain > 0) { + int readLength = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, remain)); + if (readLength < 0) { + break; + } + outputStream.write(buffer, 0, readLength); + remain -= readLength; + } + outputStream.flush(); + } + } + + private boolean isClientAbortException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + String className = current.getClass().getName(); + String message = current.getMessage(); + if (className.contains("ClientAbortException") || className.contains("AsyncRequestNotUsableException")) { + return true; + } + if (message != null) { + String lowerMessage = message.toLowerCase(); + if (lowerMessage.contains("broken pipe") + || lowerMessage.contains("connection reset") + || lowerMessage.contains("connection reset by peer") + || lowerMessage.contains("forcibly closed")) { + return true; + } + } + current = current.getCause(); + } + return false; + } }