From 8ab0cba1848e9a1ec83b74a6dedfccf7047615e6 Mon Sep 17 00:00:00 2001 From: DPhngNam Date: Mon, 2 Nov 2026 00:19:38 +0700 Subject: [PATCH] fix(run): give the spring-boot:run fork a UTF-8 default charset on Windows On Windows, mvn spring-boot:run passes the stdout/stderr encoding properties to the forked application JVM through JAVA_TOOL_OPTIONS, but not file.encoding (it would change how Maven reads sources). On JDK 8-17, logback / log4j2 console appenders encode with the default charset (GBK on Chinese Windows) and ignore stdout.encoding, so Chinese log lines showed as U+FFFD in the UTF-8 Run console. For a Maven launch with a spring-boot:run goal and a UTF-8 console, add -Dfile.encoding=UTF-8 to -Dspring-boot.run.jvmArguments, merged into the user's VM options. It reaches only the forked JVM; Maven's own JVM is unchanged. A file.encoding the user set (in jvmArguments or JAVA_TOOL_OPTIONS) wins. Fixes #697 --- crates/ide-core/src/output_encoding.rs | 49 ++++++++++++++++++++++++++ crates/ide-run/src/process.rs | 4 ++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/ide-core/src/output_encoding.rs b/crates/ide-core/src/output_encoding.rs index de53b5409..9aee05cb3 100644 --- a/crates/ide-core/src/output_encoding.rs +++ b/crates/ide-core/src/output_encoding.rs @@ -75,6 +75,7 @@ fn encoding_for_code_page(cp: u32) -> Option<&'static encoding_rs::Encoding> { const STD_PROPERTIES: &[&str] = &["stdout.encoding", "stderr.encoding", "sun.stdout.encoding", "sun.stderr.encoding"]; const FILE_ENCODING: &str = "file.encoding"; const TOOL_OPTIONS: &str = "JAVA_TOOL_OPTIONS"; +const SPRING_BOOT_JVM_ARGS: &str = "-Dspring-boot.run.jvmArguments="; /// Build tools and launcher scripts that run on (and may fork) JVMs. const JVM_TOOLS: &[&str] = @@ -85,6 +86,7 @@ const JVM_TOOLS: &[&str] = #[derive(Debug, Default, PartialEq, Eq)] pub struct EncodingFix { pub java_args: Vec, + pub args: Option>, pub env: Vec<(String, String)>, /// "Picked up JAVA_TOOL_OPTIONS: …" for a variable set here alone, to drop from the output. pub notice: Option, @@ -194,6 +196,12 @@ fn fix_kind( } } (Kind::Java | Kind::JvmTool, _) => { + if let (Kind::JvmTool, Some(args)) = (kind, args) + && is_utf8(encoding) + && !sets(&tool_options, &[FILE_ENCODING]) + { + out.args = spring_boot_file_encoding(args); + } if !sets(&tool_options, STD_PROPERTIES) { let added = options(STD_PROPERTIES, encoding).join(" "); let value = match tool_options.trim() { @@ -214,6 +222,26 @@ fn fix_kind( out } +fn spring_boot_file_encoding(args: &[String]) -> Option> { + if !args.iter().any(|a| a == "spring-boot:run" || (a.contains(":spring-boot-maven-plugin:") && a.ends_with(":run"))) + { + return None; + } + let option = format!("-D{FILE_ENCODING}=UTF-8"); + let mut out = args.to_vec(); + match out.iter_mut().rfind(|a| a.starts_with(SPRING_BOOT_JVM_ARGS)) { + Some(own) if sets(own, &[FILE_ENCODING]) => return None, + Some(own) => { + if own.len() > SPRING_BOOT_JVM_ARGS.len() { + own.push(' '); + } + own.push_str(&option); + } + None => out.push(format!("{SPRING_BOOT_JVM_ARGS}{option}")), + } + Some(out) +} + /// `vmArgs` of a launch by java-debug, which decodes the JVM's output as `encoding` /// (its launch attribute, UTF-8 when unset). java-debug sets `file.encoding` only /// when that attribute is nonblank; use the same defaults and user overrides as [`fix`]. @@ -435,6 +463,27 @@ mod tests { assert_eq!(run("python3", &[], &[], "GBK", false).env, [("PYTHONIOENCODING".to_owned(), "GBK".to_owned())]); } + #[test] + fn spring_boot_run_forks_get_a_utf8_default_charset() { + let boot = |args: &[&str], env: &[(&str, &str)]| run("mvn.cmd", args, env, "UTF-8", true).args; + let utf8 = "-Dspring-boot.run.jvmArguments=-Dfile.encoding=UTF-8"; + assert_eq!(boot(&["spring-boot:run"], &[]).unwrap(), ["spring-boot:run", utf8]); + let plugin = "org.springframework.boot:spring-boot-maven-plugin:3.2.0:run"; + assert_eq!(boot(&["-pl", "app", plugin], &[]).unwrap(), ["-pl", "app", plugin, utf8]); + // The user's VM options are kept; their own `file.encoding` wins. + assert_eq!( + boot(&["spring-boot:run", "-Dspring-boot.run.jvmArguments=-Xmx1g \"-Da=b c\""], &[]).unwrap(), + ["spring-boot:run", "-Dspring-boot.run.jvmArguments=-Xmx1g \"-Da=b c\" -Dfile.encoding=UTF-8"] + ); + assert_eq!(boot(&["spring-boot:run", "-Dspring-boot.run.jvmArguments="], &[]).unwrap()[1], utf8); + assert!(boot(&["spring-boot:run", "-Dspring-boot.run.jvmArguments=-Dfile.encoding=GBK"], &[]).is_none()); + assert!(boot(&["spring-boot:run"], &[(TOOL_OPTIONS, "-Dfile.encoding=GBK")]).is_none()); + // Other goals and tools, other consoles, and other platforms are left alone. + assert!(boot(&["package"], &[]).is_none()); + assert!(run("mvn", &["spring-boot:run"], &[], "GBK", true).args.is_none()); + assert!(run("mvn", &["spring-boot:run"], &[], "UTF-8", false).args.is_none()); + } + #[test] fn shell_command_lines_get_variables_from_their_first_word() { let line = |l: &str| fix_command_line(l, &BTreeMap::new(), |_| None, "UTF-8", true); diff --git a/crates/ide-run/src/process.rs b/crates/ide-run/src/process.rs index 0808e9522..104c2d7d0 100644 --- a/crates/ide-run/src/process.rs +++ b/crates/ide-run/src/process.rs @@ -195,7 +195,9 @@ fn match_console_encoding( output_encoding::fix(&program, &inv.args, &env.vars, inherited, encoding, cfg!(windows)) }; env.vars.extend(fix.env); - if !fix.java_args.is_empty() && !inv.raw_args { + let changed = fix.args.is_some() || !fix.java_args.is_empty(); + if changed && !inv.raw_args { + inv.args = fix.args.unwrap_or_else(|| std::mem::take(&mut inv.args)); inv.args.splice(0..0, fix.java_args); let mut display = quote(config.command.trim()); for a in &inv.args {