兼容性优化

This commit is contained in:
张家训
2026-06-23 20:25:40 +08:00
parent c89fcba998
commit 443a5e9cd9
7 changed files with 277 additions and 72 deletions
@@ -17,6 +17,8 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import java.util.*;
import java.util.concurrent.CountDownLatch;
@@ -48,6 +50,37 @@ public class WorkUtilsApplication implements CommandLineRunner {
this.settingsService = settingsService;
}
// ==================== 启动诊断 ====================
/**
* 应用就绪后验证所有 data/ 配置文件状态
*/
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
String[] files = {
"project_paths.txt", "datasources.json",
"damaged_repos.cache", "mybatis-generator.properties", "workutils.config"
};
java.nio.file.Path dataDir = java.nio.file.Paths.get(
System.getProperty("user.dir"), "data");
StringBuilder sb = new StringBuilder("data 目录文件状态:\n");
for (String f : files) {
java.nio.file.Path p = dataDir.resolve(f);
try {
if (java.nio.file.Files.exists(p)) {
long size = java.nio.file.Files.size(p);
sb.append(String.format(" %-35s %s%n", f,
size > 0 ? size + " bytes ✓" : "0 bytes ✗ 空文件!"));
} else {
sb.append(String.format(" %-35s 不存在 ✗%n", f));
}
} catch (Exception e) {
sb.append(String.format(" %-35s 读取失败: %s%n", f, e.getMessage()));
}
}
log.info(sb.toString());
}
public static void main(String[] args) {
WinConsole.ensureUtf8();
SpringApplication.run(WorkUtilsApplication.class, args);
@@ -3,12 +3,15 @@ package com.example.workutils.system.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -20,6 +23,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.JarFile;
/**
* 工具集配置属性
@@ -301,23 +305,74 @@ public class WorkUtilsProperties {
private void ensureDataFile(Path target, String classpathResource, String label) {
try {
if (Files.exists(target)) return;
// 文件存在且非空 → 跳过;不存在或为空(0字节)→ 重新复制模板
if (Files.exists(target) && Files.size(target) > 0) {
log.debug("{} 已存在且非空,跳过复制: {}", label, target.toAbsolutePath());
return;
}
Files.createDirectories(target.getParent());
ClassPathResource cpr = new ClassPathResource(classpathResource);
if (cpr.exists()) {
try (InputStream in = cpr.getInputStream()) {
// 多 ClassLoader 回退,兼容 fat JAR / thin JAR / IDE 环境
InputStream in = openClasspathStream(classpathResource);
if (in != null) {
try (in) {
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
log.info("已从模板复制{}: {} ({} bytes)", label, target.toAbsolutePath(), Files.size(target));
}
log.info("已从模板复制{}: {}", label, target.toAbsolutePath());
} else {
Files.createFile(target);
log.info("模板未找到,已创建空{}: {}", label, target.toAbsolutePath());
// 所有 ClassLoader 都未找到模板,保留空文件防下次重试
if (!Files.exists(target)) Files.createFile(target);
log.warn("模板未找到[classpath:{}],已创建空{}: {}", classpathResource, label, target.toAbsolutePath());
}
} catch (java.io.IOException e) {
log.warn("创建{}失败: {}", label, e.getMessage());
}
}
/**
* 多 ClassLoader 回退打开 classpath 资源流。
* 优先级:ContextClassLoader → 当前类的 ClassLoader → 系统 ClassLoader → 直读 JAR
*/
private static InputStream openClasspathStream(String path) {
InputStream in;
// 1) Thread ContextClassLoader(Spring Boot fat JAR 标准)
ClassLoader ctxCl = Thread.currentThread().getContextClassLoader();
if (ctxCl != null) {
in = ctxCl.getResourceAsStream(path);
if (in != null) return in;
}
// 2) 当前类的 ClassLoader
in = WorkUtilsProperties.class.getClassLoader().getResourceAsStream(path);
if (in != null) return in;
// 3) 系统 ClassLoader(兜底)
in = ClassLoader.getSystemResourceAsStream(path);
if (in != null) return in;
// 4) 直接从 JAR 文件读取(终极回退,绕过所有 ClassLoader 问题)
try {
URL jarUrl = WorkUtilsProperties.class.getProtectionDomain().getCodeSource().getLocation();
if (jarUrl != null) {
String jarPath = jarUrl.getPath();
String entryName = "BOOT-INF/classes/" + path;
try (JarFile jar = new JarFile(jarPath)) {
java.util.jar.JarEntry entry = jar.getJarEntry(entryName);
if (entry != null) {
byte[] bytes = jar.getInputStream(entry).readAllBytes();
log.info("从 JAR 直接读取模板: {} ({} bytes)", entryName, bytes.length);
return new ByteArrayInputStream(bytes);
}
}
}
} catch (Exception ignored) {
// 不是 JAR 环境或读取失败,忽略,前面 ClassLoader 方式已尽力
}
return null;
}
public Path getExportBasePath() {
return Paths.get(exportDirectory);
}
@@ -8,6 +8,12 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 工集 UI 设计系统 —— AtlantaFX 主题管理 + 字体/间距 Token
* <p>
@@ -25,6 +31,9 @@ public final class Theme {
private static volatile Mode currentMode = Mode.LIGHT;
private static volatile String currentCustomCss = "css/light-theme.css";
/** 缓存已加载的 CSS data: URI,避免重复读取 */
private static final Map<String, String> cssFileCache = new ConcurrentHashMap<>();
public static Mode getCurrentMode() { return currentMode; }
/**
@@ -44,17 +53,36 @@ public final class Theme {
}
/**
* 将自定义 CSS 样式表添加到 Scene
* 将自定义 CSS 样式表添加到 Scene。
* <p>
* 兼容 Spring Boot fat JAR:使用 data: URI 内联 CSS,不依赖文件系统 URL。
*/
public static void applyCustomCss(Scene scene) {
var cssUrl = Theme.class.getClassLoader().getResource(currentCustomCss);
if (cssUrl == null) {
String dataUri = cssFileCache.computeIfAbsent(currentCustomCss, Theme::loadCssAsDataUri);
if (dataUri == null) {
System.err.println("[Theme] 警告: 找不到 CSS 资源 " + currentCustomCss + ",将使用默认样式");
return;
}
String css = cssUrl.toExternalForm();
scene.getStylesheets().removeIf(s -> s.contains("light-theme.css") || s.contains("dark-theme.css"));
scene.getStylesheets().add(css);
scene.getStylesheets().add(dataUri);
}
/**
* 从 classpath 读取 CSS 文件,转成 data: URI(Base64),JavaFX 原生支持。
*/
private static String loadCssAsDataUri(String classpath) {
try (InputStream in = Theme.class.getClassLoader().getResourceAsStream(classpath)) {
if (in == null) {
System.err.println("[Theme] 错误: classpath 中找不到 " + classpath);
return null;
}
String css = new String(in.readAllBytes(), StandardCharsets.UTF_8);
String b64 = Base64.getEncoder().encodeToString(css.getBytes(StandardCharsets.UTF_8));
return "data:text/css;base64," + b64;
} catch (Exception e) {
System.err.println("[Theme] 错误: 读取 CSS 失败 " + classpath + " - " + e.getMessage());
return null;
}
}
/**