From 5a2fc48e3ecb0170a027af8dd6f224ced1918376 Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:15:22 +0900 Subject: [PATCH] ConsoleAppender: probe JLine's org.jline.jansi.AnsiConsole for Jansi was migrated from FuseSource (org.fusesource.jansi) to JLine (org.jline.jansi), which changed the package of AnsiConsole. Because ConsoleAppender loaded the FuseSource class name by reflection, silently fell back to the plain stream ("Failed to create AnsiPrintStream", ClassNotFoundException: org.fusesource.jansi.AnsiConsole) for users who now have only the JLine Jansi artifact on the classpath. Probe the JLine coordinates first and fall back to the legacy FuseSource ones, so keeps working with either artifact. Fixes #1043 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../ch/qos/logback/core/ConsoleAppender.java | 28 ++++++- ...nsoleAppenderJansiClassResolutionTest.java | 76 +++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 logback-core/src/test/java/ch/qos/logback/core/ConsoleAppenderJansiClassResolutionTest.java diff --git a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java index 4844752ec9..8451da05e9 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java +++ b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java @@ -48,7 +48,11 @@ public class ConsoleAppender extends OutputStreamAppender { protected ConsoleTarget target = ConsoleTarget.SystemOut; protected boolean withJansi = false; - private final static String AnsiConsole_CLASS_NAME = "org.fusesource.jansi.AnsiConsole"; + // Jansi was migrated from FuseSource (org.fusesource.jansi) to JLine (org.jline.jansi), which + // changed the package of AnsiConsole. Probe the JLine coordinates first, then fall back to the + // legacy FuseSource ones so that keeps working with both artifacts. See LOGBACK issue 1043. + private final static String[] ANSI_CONSOLE_CLASS_NAMES = { "org.jline.jansi.AnsiConsole", + "org.fusesource.jansi.AnsiConsole" }; private final static String JANSI2_OUT_METHOD_NAME = "out"; private final static String JANSI2_ERR_METHOD_NAME = "err"; private final static String WRAP_SYSTEM_OUT_METHOD_NAME = "wrapSystemOut"; @@ -114,7 +118,7 @@ private OutputStream wrapWithJansi(OutputStream targetStream) { try { addInfo("Enabling JANSI AnsiPrintStream for the console."); ClassLoader classLoader = Loader.getClassLoaderOfObject(context); - Class classObj = classLoader.loadClass(AnsiConsole_CLASS_NAME); + Class classObj = loadAnsiConsoleClass(classLoader); Method systemInstallMethod = classObj.getMethod(SYSTEM_INSTALL_METHOD_NAME); if(systemInstallMethod != null) { @@ -157,6 +161,26 @@ private OutputStream wrapWithJansi(OutputStream targetStream) { return targetStream; } + /** + * Loads the Jansi {@code AnsiConsole} class, probing the candidate class names in + * {@link #ANSI_CONSOLE_CLASS_NAMES} order (JLine's {@code org.jline.jansi} first, then the legacy + * FuseSource {@code org.fusesource.jansi}). This keeps {@code } working across the Jansi + * migration from FuseSource to JLine. + * + * @throws ClassNotFoundException if none of the candidate classes is available. + */ + Class loadAnsiConsoleClass(ClassLoader classLoader) throws ClassNotFoundException { + ClassNotFoundException lastException = null; + for (String className : ANSI_CONSOLE_CLASS_NAMES) { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + lastException = e; + } + } + throw lastException; + } + /** * @return whether to use JANSI or not. */ diff --git a/logback-core/src/test/java/ch/qos/logback/core/ConsoleAppenderJansiClassResolutionTest.java b/logback-core/src/test/java/ch/qos/logback/core/ConsoleAppenderJansiClassResolutionTest.java new file mode 100644 index 0000000000..625b6ece83 --- /dev/null +++ b/logback-core/src/test/java/ch/qos/logback/core/ConsoleAppenderJansiClassResolutionTest.java @@ -0,0 +1,76 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2026, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v2.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ +package ch.qos.logback.core; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verifies that {@link ConsoleAppender} probes the JLine Jansi {@code AnsiConsole} class name before + * the legacy FuseSource one, so that {@code } keeps working after the Jansi migration from + * FuseSource to JLine. See LOGBACK issue 1043. + */ +public class ConsoleAppenderJansiClassResolutionTest { + + static final String JLINE = "org.jline.jansi.AnsiConsole"; + static final String FUSESOURCE = "org.fusesource.jansi.AnsiConsole"; + + final ConsoleAppender ca = new ConsoleAppender<>(); + + /** + * ClassLoader that resolves only the AnsiConsole class names present in {@code known} (mapping each + * to a distinct stand-in class) and reports the others as absent. + */ + private ClassLoader loaderResolving(Map> known) { + return new ClassLoader(getClass().getClassLoader()) { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + Class mapped = known.get(name); + if (mapped != null) { + return mapped; + } + if (JLINE.equals(name) || FUSESOURCE.equals(name)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name); + } + }; + } + + @Test + public void prefersJLineWhenBothArePresent() throws ClassNotFoundException { + Map> known = new HashMap<>(); + known.put(JLINE, String.class); + known.put(FUSESOURCE, Integer.class); + assertEquals(String.class, ca.loadAnsiConsoleClass(loaderResolving(known))); + } + + @Test + public void fallsBackToFuseSourceWhenJLineIsAbsent() throws ClassNotFoundException { + Map> known = new HashMap<>(); + known.put(FUSESOURCE, Integer.class); + assertEquals(Integer.class, ca.loadAnsiConsoleClass(loaderResolving(known))); + } + + @Test + public void throwsWhenNoJansiIsAvailable() { + Map> none = new HashMap<>(); + assertThrows(ClassNotFoundException.class, () -> ca.loadAnsiConsoleClass(loaderResolving(none))); + } +}