diff --git a/pom.xml b/pom.xml index eb8940d..878bfa5 100644 --- a/pom.xml +++ b/pom.xml @@ -269,7 +269,6 @@ ${basedir}/scripts start.bat - start-jar.bat diff --git a/scripts/build_release.bat b/scripts/build_release.bat index bbba2d3..f85edf1 100644 --- a/scripts/build_release.bat +++ b/scripts/build_release.bat @@ -1,6 +1,6 @@ @echo off chcp 65001 >nul -setlocal +setlocal enabledelayedexpansion :: ============================================ :: WorkUtils 生产打包脚本(免安装版) @@ -8,7 +8,7 @@ setlocal :: (含 exe + 内嵌JRE + jar + 启动脚本) :: ============================================ -::: 切换到项目根目录 +:: 切换到项目根目录 cd /d "%~dp0.." set "PROJECT_DIR=%CD%" set "TARGET=%PROJECT_DIR%\target" @@ -20,26 +20,55 @@ echo ========== WorkUtils 生产构建 ========== echo 项目目录: %PROJECT_DIR% echo. -::: ============================================================ -::: 步骤 1:清理残留进程 -::: ============================================================ -echo [1/5] Clean stale processes... +:: ============================================================ +:: 步骤 1:清理残留进程 & 解锁删除旧产物 +:: ============================================================ +echo [1/5] Clean stale processes and files... taskkill /f /im java.exe 2>nul taskkill /f /im workutils.exe 2>nul +:: 等进程完全退出,文件锁释放 +echo 等待进程退出... +ping 127.0.0.1 -n 4 >nul -::: ============================================================ -::: 步骤 2:Maven 打包 -::: ============================================================ -echo. -echo [2/5] Maven build... +:: --- 清理 target/dist --- if exist "%TARGET%" ( - echo Cleaning old target... + :: 策略1:直接 rd(文件锁已释放时应成功) rd /s /q "%TARGET%" 2>nul + :: 策略2:如果 rd 失败,改名后重建 if exist "%TARGET%" ( - echo [WARN] Cannot delete target, retrying with Maven clean... - call "%PROJECT_DIR%\mvnw.cmd" clean -q 2>nul + echo [WARN] 直接删除失败,尝试改名清理... + :: 先删 dist/ 子目录的文件(规避 JRE 文件锁) + if exist "%DIST%" ( + del /f /s /q "%DIST%\*" 2>nul + rd /s /q "%DIST%" 2>nul + ) + :: 再删 jre/ + if exist "%TARGET%\jre" ( + del /f /s /q "%TARGET%\jre\*" 2>nul + rd /s /q "%TARGET%\jre" 2>nul + ) + :: 删 target/ 下其余文件 + del /f /s /q "%TARGET%\*" 2>nul + for /d %%d in ("%TARGET%\*") do rd /s /q "%%d" 2>nul + rd /s /q "%TARGET%" 2>nul + ) + :: 策略3:改名旧目录,让后续步骤创建新的 + if exist "%TARGET%" ( + for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value') do set "TS=%%I" + set "OLD_TARGET=%PROJECT_DIR%\target_old_!TS:~0,14!" + echo [WARN] 无法删除,重命名为 !OLD_TARGET! ... + move "%TARGET%" "!OLD_TARGET!" >nul 2>&1 + if exist "!OLD_TARGET!" ( + echo [INFO] 旧产物已移至 !OLD_TARGET!,稍后请手动删除 + ) ) ) + +:: ============================================================ +:: 步骤 2:Maven 打包 +:: ============================================================ +echo. +echo [2/5] Maven build... call "%PROJECT_DIR%\mvnw.cmd" package -DskipTests -q if %errorlevel% neq 0 ( echo [ERROR] Maven build failed! @@ -47,22 +76,22 @@ if %errorlevel% neq 0 ( exit /b 1 ) -::: ============================================================ -::: 步骤 3:提取版本号(PowerShell 解析 pom.xml) -::: ============================================================ +:: ============================================================ +:: 步骤 3:提取版本号 +:: ============================================================ echo. echo [3/5] Read version from pom.xml... for /f "usebackq delims=" %%a in (`powershell -NoProfile -NonInteractive -Command "([xml](Get-Content -Encoding UTF8 '%PROJECT_DIR%\pom.xml')).project.version"`) do set "VERSION=%%a" if "%VERSION%"=="" set "VERSION=0.0.1-SNAPSHOT" echo 版本: %VERSION% -::: ============================================================ -::: 步骤 4:生成精简 JRE(jlink 内嵌 Java 17 + JavaFX) -::: ============================================================ +:: ============================================================ +:: 步骤 4:生成精简 JRE(jlink 内嵌 Java 17 + JavaFX) +:: ============================================================ echo. echo [4/5] Generate bundled JRE... -::: 4.1 查找 JDK 17(环境变量 JDK17_HOME > 默认路径) +:: 4.1 查找 JDK 17 if not defined JDK17_HOME ( for %%d in ( "C:\Program Files\Java\jdk-17" @@ -81,7 +110,7 @@ if not defined JDK17_HOME ( ) :jdk17_found -::: 4.2 查找 JavaFX SDK(环境变量 JFX_SDK_HOME > 默认路径) +:: 4.2 查找 JavaFX SDK if not defined JFX_SDK_HOME ( for %%d in ( "C:\Program Files\Java\javafx-sdk-17.0.15" @@ -102,10 +131,10 @@ if not defined JFX_SDK_HOME ( echo JDK : %JDK17_HOME% echo JavaFX: %JFX_SDK_HOME% -::: 4.3 清理旧 JRE -if exist "%TARGET%\jre" rd /s /q "%TARGET%\jre" +:: 4.3 清理旧 JRE +if exist "%TARGET%\jre" rd /s /q "%TARGET%\jre" 2>nul -::: 4.4 执行 jlink +:: 4.4 执行 jlink echo Running jlink, please wait... "%JDK17_HOME%\bin\jlink.exe" ^ --module-path "%JFX_SDK_HOME%\lib;%JDK17_HOME%\jmods" ^ @@ -119,7 +148,7 @@ if %errorlevel% neq 0 ( exit /b 1 ) -::: 4.5 补充 JavaFX 原生 DLL(jlink 不会自动从 SDK bin/ 复制) +:: 4.5 补充 JavaFX 原生 DLL echo Copy JavaFX native DLLs... if exist "%JFX_SDK_HOME%\bin\glass.dll" ( xcopy /y /q "%JFX_SDK_HOME%\bin\*.dll" "%TARGET%\jre\bin\" >nul @@ -130,17 +159,21 @@ if exist "%JFX_SDK_HOME%\bin\glass.dll" ( echo [OK] JRE generated : %TARGET%\jre -::: ============================================================ -::: 步骤 5:组装发布目录 & 打包 ZIP -::: ============================================================ +:: ============================================================ +:: 步骤 5:组装发布目录 & 打包 ZIP +:: ============================================================ echo. echo [5/5] Assemble distribution... -::: 清理旧发布目录 -if exist "%DIST%" rd /s /q "%DIST%" +:: 清理旧发布目录 +if exist "%DIST%" ( + del /f /s /q "%DIST%\*" 2>nul + for /d %%d in ("%DIST%\*") do rd /s /q "%%d" 2>nul + rd /s /q "%DIST%" 2>nul +) mkdir "%DIST%\workutils" 2>nul -::: 复制 EXE +:: 复制 EXE if exist "%TARGET%\workutils.exe" ( copy /y "%TARGET%\workutils.exe" "%DIST%\workutils\" >nul echo [OK] workutils.exe @@ -148,44 +181,61 @@ if exist "%TARGET%\workutils.exe" ( echo [WARN] workutils.exe not found! ) -::: 复制 JAR +:: 复制 JAR for %%j in ("%TARGET%\workutils-*.jar") do ( copy /y "%%j" "%DIST%\workutils\" >nul echo [OK] %%~nxj ) -::: 复制内嵌 JRE +:: 复制内嵌 JRE if exist "%TARGET%\jre\bin\java.exe" ( + echo 复制内嵌 JRE... xcopy /e /i /q "%TARGET%\jre" "%DIST%\workutils\jre\" >nul echo [OK] jre/ ^(bundled Java 17 + JavaFX^) ) else ( echo [WARN] jre not found in target! ) -::: ============================================================ -::: 生成 start.bat(备选启动方式) -::: ============================================================ +:: ============================================================ +:: 生成 start.bat +:: ============================================================ ( echo @echo off echo chcp 65001 ^>nul echo title WorkUtils - echo java -jar "%%~dp0workutils-%VERSION%.jar" %%* - echo pause + echo. + echo :: 优先用内嵌 JRE,否则查系统 Java 17+ + echo set "JAVA_BIN=%%~dp0jre\bin\java.exe" + echo if exist "%%JAVA_BIN%%" goto :run + echo. + echo set "JAVA_BIN=java" + echo for /f "tokens=3" %%%%v in ^('"%%JAVA_BIN%%" -version 2^>^&1 ^| findstr /i "version"'^) do set "JAVA_VER=%%%%~v" + echo for /f "tokens=1 delims=." %%%%m in ^("%%JAVA_VER%%"^) do set "MAJOR=%%%%m" + echo if "%%MAJOR%%"=="1" for /f "tokens=2 delims=." %%%%m in ^("%%JAVA_VER%%"^) do set "MAJOR=%%%%m" + echo if %%MAJOR%% LSS 17 ^( + echo echo [ERROR] Java %%JAVA_VER%% found. WorkUtils requires Java 17+. + echo echo Download JDK 17+ from https://adoptium.net/ + echo pause ^& exit /b 1 + echo ^) + echo. + echo :run + echo "%%JAVA_BIN%%" -jar "%%~dp0workutils-%VERSION%.jar" %%* + echo if %%errorlevel%% neq 0 pause ) > "%DIST%\workutils\start.bat" echo [OK] start.bat -::: ============================================================ -::: 打包 ZIP -::: ============================================================ +:: ============================================================ +:: 打包 ZIP +:: ============================================================ set "ZIPNAME=workutils-v%VERSION%" echo 正在压缩 %ZIPNAME%.zip ... powershell -Command ^ "Compress-Archive -Path '%DIST%\workutils' -DestinationPath '%DIST%\%ZIPNAME%.zip' -Force" echo [OK] %ZIPNAME%.zip -::: ============================================================ -::: 完成 -::: ============================================================ +:: ============================================================ +:: 完成 +:: ============================================================ echo. echo ============================================ echo Build Complete ^! v%VERSION% @@ -196,3 +246,4 @@ echo %DIST%\%ZIPNAME%.zip echo ============================================ echo. pause +exit /b 0 diff --git a/scripts/start-jar.bat b/scripts/start-jar.bat deleted file mode 100644 index c372576..0000000 --- a/scripts/start-jar.bat +++ /dev/null @@ -1,7 +0,0 @@ -@echo off -chcp 65001 >nul -title WorkUtils - -java -Dfile.encoding=UTF-8 -jar "%~dp0target\workutils-0.0.2-SNAPSHOT.jar" %* - -pause diff --git a/scripts/start.bat b/scripts/start.bat index a2220da..ca31d95 100644 --- a/scripts/start.bat +++ b/scripts/start.bat @@ -1,10 +1,56 @@ @echo off -chcp 65001 > nul -title workutils +chcp 65001 >nul +title WorkUtils + +:: 切到脚本所在目录的父目录(即项目根目录) +:: 当脚本在 scripts/ 时 → 项目根;在 target/ 时 → 项目根 +cd /d "%~dp0.." + +:: 从 pom.xml 读取版本号 +set "VERSION=" +for /f "usebackq delims=" %%a in (`powershell -NoProfile -NonInteractive -Command "([xml](Get-Content -Encoding UTF8 '%CD%\pom.xml')).project.version"`) do set "VERSION=%%a" +if "%VERSION%"=="" set "VERSION=0.0.1-SNAPSHOT" + +:: 优先匹配版本号精确的 JAR,找不到回退通配符 +set "JAR=" +if exist "target\workutils-%VERSION%.jar" set "JAR=target\workutils-%VERSION%.jar" +if "%JAR%"=="" for %%j in ("target\workutils-*.jar") do set "JAR=%%j" +if "%JAR%"=="" ( + echo [ERROR] JAR not found in target\ Run build_release.bat first. + pause & exit /b 1 +) + +:: ======== 查找可用的 Java 17+ ======== +set "JAVA_EXE=" + +:: 1) 优先用 jlink 生成的内嵌 JRE(运行过 build_release.bat 后产生) +if exist "target\jre\bin\java.exe" set "JAVA_EXE=target\jre\bin\java.exe" + +:: 2) 检查 JDK17_HOME 环境变量 +if "%JAVA_EXE%"=="" if defined JDK17_HOME if exist "%JDK17_HOME%\bin\java.exe" set "JAVA_EXE=%JDK17_HOME%\bin\java.exe" + +:: 3) 检查 JAVA_HOME(可能是 JDK 17) +if "%JAVA_EXE%"=="" if defined JAVA_HOME if exist "%JAVA_HOME%\bin\java.exe" set "JAVA_EXE=%JAVA_HOME%\bin\java.exe" + +:: 4) 回退到 PATH 上的 java +if "%JAVA_EXE%"=="" set "JAVA_EXE=java" + +:: 检查 Java 版本是否 ≥ 17 +for /f "tokens=3" %%v in ('"%JAVA_EXE%" -version 2^>^&1 ^| findstr /i "version"') do set "JAVA_VER=%%~v" +for /f "tokens=1 delims=." %%m in ("%JAVA_VER%") do set "MAJOR=%%m" +if "%MAJOR%"=="1" for /f "tokens=2 delims=." %%m in ("%JAVA_VER%") do set "MAJOR=%%m" +if %MAJOR% LSS 17 ( + echo [ERROR] Java %JAVA_VER% found. WorkUtils requires Java 17+. + echo Path: %JAVA_EXE% + echo Download JDK 17+ from https://adoptium.net/ + pause & exit /b 1 +) + echo ==================================== -echo workutils 系统工具集 -echo 控制台编码: UTF-8 (65001) +echo WorkUtils v%VERSION% +echo Java: %JAVA_VER% ^( %JAVA_EXE% ^) echo ==================================== echo. -cd /d "%~dp0" -java -jar target\workutils-0.0.2-SNAPSHOT.jar + +"%JAVA_EXE%" -Dfile.encoding=UTF-8 -jar "%JAR%" %* +if %errorlevel% neq 0 pause diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index 5e330e9..2bafa16 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -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); diff --git a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java index 7180850..1863408 100644 --- a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java +++ b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java @@ -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); } diff --git a/src/main/java/com/example/workutils/system/ui/Theme.java b/src/main/java/com/example/workutils/system/ui/Theme.java index 0acc9c5..859f576 100644 --- a/src/main/java/com/example/workutils/system/ui/Theme.java +++ b/src/main/java/com/example/workutils/system/ui/Theme.java @@ -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 *

@@ -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 cssFileCache = new ConcurrentHashMap<>(); + public static Mode getCurrentMode() { return currentMode; } /** @@ -44,17 +53,36 @@ public final class Theme { } /** - * 将自定义 CSS 样式表添加到 Scene + * 将自定义 CSS 样式表添加到 Scene。 + *

+ * 兼容 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; + } } /**