SCX Logging 是一个轻量日志库。
它提供一个很小的日志核心:ScxLogging、ScxLogger、ScxLoggerConfig、ScxLogRecord 和 ScxLogRecorder。同时它也通过 SPI 接入了 JDK System.Logger、SLF4J 和 Log4j2,让不同日志入口最终都可以输出到同一套 SCX Logging 记录器中。
SCX Logging 本身不是复杂日志框架。它不提供异步队列、复杂 layout 配置、滚动文件策略、MDC 完整语义或分布式日志能力。它的定位是一个简单、可嵌入、可配置、可桥接的日志核心。
<dependency>
<groupId>dev.scx</groupId>
<artifactId>scx-logging</artifactId>
<version>0.10.0</version>
</dependency>
scx-logging 对下面两个依赖是 optional:
slf4j-api
log4j-api
也就是说:
ScxLogging 自己的 API,不需要额外关心 SLF4J 或 Log4j2。slf4j-api。log4j-api。System.Logger 调用 SCX Logging,不需要额外依赖。SCX Logging 中最核心的概念包括:
ScxLogging 全局日志入口和配置入口
ScxLogger 具体 logger
ScxLoggerConfig logger 配置
ScxLogRecord 一条日志记录
ScxLogRecorder 日志记录器接口
ConsoleRecorder 控制台记录器
FileRecorder 文件记录器
它们之间的关系可以简单理解为:
日志 API 调用
↓
ScxLogger
↓
判断 level 是否允许
↓
创建 ScxLogRecord
↓
交给 ScxLoggerConfig 中的 recorders
↓
ConsoleRecorder / FileRecorder / 自定义 Recorder
SCX Logging 同时支持三类外部入口:
JDK System.Logger
SLF4J
Log4j2
这些入口最终都会转到同一个核心对象:
ScxLogger
最直接的方式是使用 ScxLogging 自己的 API。
import dev.scx.logging.ScxLogging;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.INFO;
var logger = ScxLogging.getLogger("demo");
logger.log(INFO, "这条默认不会输出", null);
logger.log(ERROR, "这条默认会输出", null);
默认配置是:
level ERROR
stackTrace false
recorders ConsoleRecorder
所以默认情况下,只有 ERROR 及以上级别会输出。
ScxLoggerConfig 是不可变配置对象。修改根配置时,需要创建新的配置并传给 ScxLogging.rootConfig(...)。
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import dev.scx.logging.recorder.ConsoleRecorder;
import java.util.List;
import static java.lang.System.Logger.Level.DEBUG;
ScxLogging.rootConfig(new ScxLoggerConfig(
DEBUG,
true,
List.of(new ConsoleRecorder())
));
var logger = ScxLogging.getLogger("demo");
logger.log(DEBUG, "debug message", null);
这里把根日志级别改成了 DEBUG,并开启了调用栈记录,所以 DEBUG、INFO、WARNING、ERROR 都可以输出。
如果项目中使用 SLF4J,可以直接通过 SLF4J API 获取 logger。
import org.slf4j.LoggerFactory;
var logger = LoggerFactory.getLogger("demo");
logger.debug("debug {}", 1);
logger.error("error {}", 2);
logger.error("error with throwable", new RuntimeException("test"));
只要 classpath 中存在 scx-logging,它会通过 SLF4JServiceProvider 接入 SLF4J 2.x。
SLF4J 的日志级别会映射为 JDK 日志级别:
SLF4J ERROR -> System.Logger.Level.ERROR
SLF4J WARN -> System.Logger.Level.WARNING
SLF4J INFO -> System.Logger.Level.INFO
SLF4J DEBUG -> System.Logger.Level.DEBUG
SLF4J TRACE -> System.Logger.Level.TRACE
SLF4J 的 {} 占位符会通过 SLF4J 的 MessageFormatter 格式化。
logger.info("hello {}", "scx");
最终消息类似:
hello scx
SCX Logging 也提供了 JDK System.LoggerFinder 实现。
可以直接使用 JDK API:
import java.lang.System.Logger;
var logger = System.getLogger("demo");
logger.log(Logger.Level.ERROR, "error message");
如果使用格式化参数:
logger.log(Logger.Level.ERROR, "hello {0}", "scx");
JDK 入口会使用 MessageFormat 风格进行格式化。
结果类似:
hello scx
如果参数最后一个是 Throwable,会被当作异常对象处理。
logger.log(
Logger.Level.ERROR,
"error {0}",
"message",
new RuntimeException("test")
);
如果项目中使用 Log4j2 API,也可以直接调用:
import org.apache.logging.log4j.LogManager;
var logger = LogManager.getLogger("demo");
logger.info("info message");
logger.error("error message", new RuntimeException("test"));
Log4j2 的日志级别会映射为 JDK 日志级别:
Log4j2 FATAL -> System.Logger.Level.ERROR
Log4j2 ERROR -> System.Logger.Level.ERROR
Log4j2 WARN -> System.Logger.Level.WARNING
Log4j2 INFO -> System.Logger.Level.INFO
Log4j2 DEBUG -> System.Logger.Level.DEBUG
Log4j2 TRACE -> System.Logger.Level.TRACE
Log4j2 ALL -> System.Logger.Level.ALL
Log4j2 OFF -> System.Logger.Level.OFF
Log4j2 的 Message 会先转换为格式化后的字符串:
message.getFormattedMessage()
然后交给 ScxLogger 输出。
ScxLogging 是全局入口类。
它负责:
获取 logger
维护根配置
维护精确名称配置和正则配置
为 logger 解析当前配置
在配置变化后刷新 logger 的配置缓存
常用方法包括:
public static ScxLogger getLogger(String name)
public static ScxLogger getLogger(Class<?> clazz)
public static ScxLoggerConfig rootConfig()
public static void rootConfig(ScxLoggerConfig rootConfig)
public static ScxLoggerConfig config(String name)
public static void config(String name, ScxLoggerConfig newConfig)
public static void removeConfig(String name)
public static ScxLoggerConfig config(Pattern regex)
public static void config(Pattern regex, ScxLoggerConfig newConfig)
public static void removeConfig(Pattern regex)
getLogger(String name) 根据名称获取 logger。
var logger = ScxLogging.getLogger("dev.scx.demo.App");
同名 logger 会被缓存。
var a = ScxLogging.getLogger("demo");
var b = ScxLogging.getLogger("demo");
a 和 b 指向同一个缓存 logger。
也可以通过 class 获取:
var logger = ScxLogging.getLogger(App.class);
等价于:
var logger = ScxLogging.getLogger(App.class.getName());
SCX Logging 内部有一个默认根配置。
level ERROR
stackTrace false
recorders ConsoleRecorder
因此即使你不做任何配置,也可以直接输出 ERROR 级别日志。
var logger = ScxLogging.getLogger("demo");
logger.log(System.Logger.Level.ERROR, "error", null);
默认会输出到控制台。
rootConfig() 返回当前根配置。
var config = ScxLogging.rootConfig();
设置根配置需要传入一个新的 ScxLoggerConfig:
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.DEBUG,
true,
List.of(new ConsoleRecorder())
));
根配置是所有 logger 配置解析的起点,并且三个字段都必须为非 null:
level
stackTrace
recorders
普通名称配置和正则配置可以只设置部分字段,未设置的字段会保留之前解析出的值。
ScxLoggerConfig 表示 logger 配置。
它是一个 record:
public record ScxLoggerConfig(
Level level,
Boolean stackTrace,
List<ScxLogRecorder> recorders
) {
}
三个字段分别表示:
level 最小日志级别
stackTrace 是否记录调用栈
recorders 日志记录器列表
根配置中的三个字段都必须有值。普通配置规则允许使用 null 表示“不覆盖这个字段”。
例如下面的配置只覆盖日志级别:
var config = new ScxLoggerConfig(
System.Logger.Level.INFO,
null,
null
);
recorders 不为 null 时,构造方法会使用 List.copyOf(...) 保存不可变副本。
var root = ScxLogging.rootConfig();
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.DEBUG,
root.stackTrace(),
root.recorders()
));
ScxLogger#isLoggable(...) 的判断逻辑是:
当前日志 level 的 severity >= 配置 level 的 severity
也就是说,配置为 INFO 时:
ERROR 输出
WARNING 输出
INFO 输出
DEBUG 不输出
TRACE 不输出
配置为 DEBUG 时:
ERROR 输出
WARNING 输出
INFO 输出
DEBUG 输出
TRACE 不输出
配置为 TRACE 时,通常可以输出最详细日志。
var root = ScxLogging.rootConfig();
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.TRACE,
root.stackTrace(),
root.recorders()
));
如果传入的日志 level 是 null,会直接认为不可输出。
logger.isLoggable(null);
结果是:
false
stackTrace 用来控制是否记录当前日志调用上下文。
var root = ScxLogging.rootConfig();
ScxLogging.rootConfig(new ScxLoggerConfig(
root.level(),
true,
root.recorders()
));
当 stackTrace 为 true 时,SCX Logging 会创建一个临时异常,并从中提取过滤后的调用栈。
输出时会追加这些调用栈信息。
如果当前日志已经带有 Throwable,则优先输出 Throwable 的堆栈,而不会再同时输出调用上下文栈。
也就是说:
throwable != null 输出 throwable 堆栈
throwable == null
且 stackTrace != null 输出调用上下文栈
SCX Logging 会过滤掉日志框架内部调用栈。
过滤规则大致是排除下面这些类名前缀:
dev.scx.logging
org.slf4j.helpers
org.apache.logging.log4j
java.lang.System$Logger
这样输出的调用栈会更接近业务调用位置。
recorders 是真正处理日志记录的组件。
默认是:
List.of(new ConsoleRecorder())
可以设置多个记录器:
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import dev.scx.logging.recorder.ConsoleRecorder;
import dev.scx.logging.recorder.FileRecorder;
import java.nio.file.Path;
import java.util.List;
var root = ScxLogging.rootConfig();
ScxLogging.rootConfig(new ScxLoggerConfig(
root.level(),
root.stackTrace(),
List.of(
new ConsoleRecorder(),
new FileRecorder(Path.of("./logs"))
)
));
设置为空列表表示日志通过级别检查后,不交给任何 recorder:
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of()
));
普通名称配置和正则配置允许使用 null 表示“不覆盖这个字段”。
例如只为某个 logger 设置 DEBUG 级别:
ScxLogging.config(
"demo.UserService",
new ScxLoggerConfig(
System.Logger.Level.DEBUG,
null,
null
)
);
这里的 stackTrace 和 recorders 会继续使用前面已经解析出的值。
需要区分 null 和空 recorder 列表:
recorders == null 不覆盖 recorder 配置
recorders == List.of() 明确覆盖为空列表
如果要取消整条名称配置,可以使用:
ScxLogging.removeConfig("demo.UserService");
ScxLogging.config(...) 可以按精确 logger 名称或正则表达式设置配置。
字符串重载使用精确名称匹配:
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import static java.lang.System.Logger.Level.DEBUG;
ScxLogging.config(
"dev.scx.demo.UserService",
new ScxLoggerConfig(DEBUG, true, null)
);
正则匹配需要显式传入 Pattern:
import java.util.regex.Pattern;
ScxLogging.config(
Pattern.compile("dev\\.scx\\.demo\\..*"),
new ScxLoggerConfig(DEBUG, true, null)
);
之后名称匹配这个正则的 logger 会应用该配置。
var logger = ScxLogging.getLogger("dev.scx.demo.UserService");
正则配置内部使用:
pattern.matcher(loggerName).matches()
所以它要求整个 loggerName 匹配正则。
例如:
pattern: test.*
name: test1
可以匹配。
pattern: test
name: test1
不能匹配。
如果只想匹配前缀,应写成:
test.*
如果想匹配包名,. 需要转义:
dev\.scx\.demo\..*
配置解析从根配置开始,然后按声明顺序应用所有匹配的精确名称配置和正则配置。
后声明的规则会覆盖前面规则中相同且非 null 的字段。
var allDemo = Pattern.compile("dev\\.scx\\.demo\\..*");
ScxLogging.config(
allDemo,
new ScxLoggerConfig(System.Logger.Level.INFO, null, null)
);
ScxLogging.config(
"dev.scx.demo.UserService",
new ScxLoggerConfig(System.Logger.Level.DEBUG, true, null)
);
对于:
dev.scx.demo.UserService
最终配置会得到:
level DEBUG
stackTrace true
recorders 继续使用根配置中的值
精确名称规则和正则规则之间没有固定优先级,覆盖关系只取决于声明顺序。
对同一个名称或同一个 Pattern 再次调用 config(...),新的声明会获得最新顺序。
config(...) 同时影响未来创建的 logger 和已经存在的 logger。
var logger = ScxLogging.getLogger("test1");
ScxLogging.config(
Pattern.compile("test.*"),
new ScxLoggerConfig(
System.Logger.Level.ERROR,
null,
null
)
);
配置变化时,SCX Logging 会更新全局配置版本。已有 logger 在之后调用 config()、isLoggable(...) 或 log(...) 时会重新解析配置。
这个刷新是按需完成的,不会在 config(...) 调用中遍历并逐个修改所有 logger。
removeConfig(...) 可以删除精确名称配置或正则配置。
ScxLogging.removeConfig("demo.UserService");
ScxLogging.removeConfig(Pattern.compile("demo\\..*"));
配置删除后,全局配置版本会发生变化。已经存在的 logger 在下一次解析配置时,会基于剩余规则和根配置重新计算结果。
如果对应配置不存在,removeConfig(...) 不执行其它操作。
ScxLogger 是实际日志对象。
常用方法包括:
public boolean isLoggable(Level level)
public void log(Level level, String message, Throwable t)
public String name()
public ScxLoggerConfig config()
示例:
var logger = ScxLogging.getLogger("demo");
if (logger.isLoggable(System.Logger.Level.DEBUG)) {
logger.log(System.Logger.Level.DEBUG, "debug message", null);
}
log(...) 中的 message 和 Throwable 都允许为 null。
logger.log(System.Logger.Level.ERROR, null, null);
logger.log(System.Logger.Level.ERROR, "error", null);
logger.log(System.Logger.Level.ERROR, "error", new RuntimeException("test"));
ScxLogRecord 表示一条日志记录。
它是一个 record:
public record ScxLogRecord(
LocalDateTime timeStamp,
Level level,
String loggerName,
String message,
String threadName,
Throwable throwable,
StackTraceElement[] stackTrace
) {
}
字段含义:
timeStamp 日志时间
level 日志级别
loggerName logger 名称
message 日志消息
threadName 当前线程名称
throwable 异常对象
stackTrace 调用上下文栈
ScxLogger 在真正输出前会创建 ScxLogRecord,然后交给所有 recorder。
ScxLogRecorder 是日志记录器接口。
public interface ScxLogRecorder {
void record(ScxLogRecord logRecord);
}
所有输出目标都可以实现这个接口。
例如:
控制台
文件
内存
数据库
HTTP
消息队列
测试断言收集器
只要实现 record(...),就可以加入 ScxLoggerConfig。
ConsoleRecorder 是控制台记录器。
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import dev.scx.logging.recorder.ConsoleRecorder;
import java.util.List;
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(new ConsoleRecorder())
));
它的输出规则是:
level >= ERROR 输出到 System.err
其它级别 输出到 System.out
也就是说,ERROR 级别日志走标准错误流。
logger.log(System.Logger.Level.ERROR, "error", null);
会使用:
System.err.print(...)
INFO、DEBUG 等级别会使用:
System.out.print(...)
FileRecorder 是文件记录器。
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import dev.scx.logging.recorder.FileRecorder;
import java.nio.file.Path;
import java.util.List;
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(new FileRecorder(Path.of("./logs")))
));
它会按日期写入日志文件。
文件名格式是:
yyyy-MM-dd.log
例如:
2026-05-08.log
日志目录由构造方法传入:
new FileRecorder(Path.of("./logs"))
最终写入路径类似:
./logs/2026-05-08.log
FileRecorder 写文件时使用:
APPEND
CREATE
SYNC
WRITE
也就是说:
如果写入失败,当前实现会吞掉异常,不向调用方抛出。
因此 FileRecorder 更偏向“尽力记录”,不会因为日志写入失败影响业务流程。
如果 FileRecorder 的目录是 null,会直接返回,不记录日志。
new FileRecorder(null)
这种情况下不会抛异常,也不会写文件。
默认格式由 ScxLogRecordHelper.formatLogRecord(...) 生成。
格式大致是:
yyyy-MM-dd HH:mm:ss.SSS [threadName] LEVEL loggerName - message
示例:
2026-05-08 12:30:15.123 [main] ERROR demo.App - something wrong
日志级别会格式化为固定宽度:
ALL -> ALL
TRACE -> TRACE
DEBUG -> DEBUG
INFO -> INFO
WARNING -> WARN
ERROR -> ERROR
OFF -> OFF
需要注意,WARNING 会显示为:
WARN
如果日志记录中包含 Throwable,默认格式化器会在日志行后追加异常堆栈。
示例:
logger.log(
System.Logger.Level.ERROR,
"failed",
new RuntimeException("test")
);
输出类似:
2026-05-08 12:30:15.123 [main] ERROR demo.App - failed
java.lang.RuntimeException: test
at ...
如果有 Throwable,就不会再输出调用上下文栈。
可以实现自己的 ScxLogRecorder。
import dev.scx.logging.ScxLogRecord;
import dev.scx.logging.ScxLogRecorder;
import java.util.ArrayList;
import java.util.List;
public final class MemoryRecorder implements ScxLogRecorder {
private final List<ScxLogRecord> records = new ArrayList<>();
@Override
public void record(ScxLogRecord logRecord) {
records.add(logRecord);
}
public List<ScxLogRecord> records() {
return records;
}
}
使用:
var memoryRecorder = new MemoryRecorder();
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(memoryRecorder)
));
var logger = ScxLogging.getLogger("demo");
logger.log(System.Logger.Level.ERROR, "error", null);
System.out.println(memoryRecorder.records().size());
这适合:
ConsoleRecorder 提供了 format(...) 方法,可以通过继承覆盖。
import dev.scx.logging.ScxLogRecord;
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.recorder.ConsoleRecorder;
import java.util.List;
var recorder = new ConsoleRecorder() {
@Override
public String format(ScxLogRecord record) {
return record.loggerName()
+ " : "
+ record.message()
+ System.lineSeparator();
}
};
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(recorder)
));
这样可以快速改变控制台输出格式。
FileRecorder 也提供了 format(...) 方法。
import dev.scx.logging.ScxLogRecord;
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.recorder.FileRecorder;
import java.nio.file.Path;
import java.util.List;
var recorder = new FileRecorder(Path.of("./logs")) {
@Override
public String format(ScxLogRecord record) {
return record.timeStamp()
+ " "
+ record.level()
+ " "
+ record.message()
+ System.lineSeparator();
}
};
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(recorder)
));
FileRecorder 提供 getLogFileName(...) 方法。
默认是:
yyyy-MM-dd.log
可以覆盖:
import dev.scx.logging.recorder.FileRecorder;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
var recorder = new FileRecorder(Path.of("./logs")) {
private final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd-HH");
@Override
public String getLogFileName(TemporalAccessor temporal) {
return formatter.format(temporal) + ".log";
}
};
这样可以按小时拆分:
2026-05-08-12.log
需要注意,FileRecorder 本身不提供日志滚动、压缩、保留天数清理等能力。
SCX Logging 通过 META-INF/services 注册了三类 SPI。
META-INF/services/java.lang.System$LoggerFinder
META-INF/services/org.slf4j.spi.SLF4JServiceProvider
META-INF/services/org.apache.logging.log4j.spi.Provider
对应实现是:
dev.scx.logging.spi.jdk.ScxJDKLoggerFinder
dev.scx.logging.spi.slf4j.ScxSLF4JServiceProvider
dev.scx.logging.spi.log4j.ScxLog4jProvider
因此,在 classpath 中加入 scx-logging 后:
JDK System.Logger 可以找到 ScxJDKLoggerFinder
SLF4J 2.x 可以找到 ScxSLF4JServiceProvider
Log4j2 可以找到 ScxLog4jProvider
JDK 适配层包括:
ScxJDKLogger
ScxJDKLoggerFinder
ScxJDKLoggerFinder 会把 JDK logger 名称转交给:
ScxLogging.getLogger(name)
ScxJDKLogger 实现了:
java.lang.System.Logger
核心行为包括:
getName() 返回底层 ScxLogger 的名称。isLoggable(level) 使用 SCX Logging 的级别判断。log(level, bundle, msg, throwable) 支持 ResourceBundle 文本查找。log(level, bundle, format, params) 支持 MessageFormat 格式化。Throwable,会作为异常处理。SLF4J 适配层包括:
ScxSLF4JLogger
ScxSLF4JLoggerFactory
ScxSLF4JServiceProvider
ScxSLF4JServiceProvider 提供:
ILoggerFactory ScxSLF4JLoggerFactory
IMarkerFactory BasicMarkerFactory
MDCAdapter BasicMDCAdapter
ScxSLF4JLoggerFactory 会把 SLF4J logger 名称转交给:
ScxLogging.getLogger(name)
ScxSLF4JLogger 继承:
LegacyAbstractLogger
并在 handleNormalizedLoggingCall(...) 中把日志转发给 ScxLogger。
当前 SLF4J 适配中,Marker 参数不会写入 ScxLogRecord。
也就是说,Marker 不参与默认输出格式,也不参与过滤。
Log4j2 适配层包括:
ScxLog4jLogger
ScxLog4jLoggerContext
ScxLog4jLoggerContextFactory
ScxLog4jProvider
ScxLog4jProvider 注册 ScxLog4jLoggerContextFactory。
ScxLog4jLoggerContextFactory 返回一个单例 ScxLog4jLoggerContext。
ScxLog4jLoggerContext 会把 logger 名称转交给:
ScxLogging.getLogger(name)
并创建:
new ScxLog4jLogger(...)
当前 Log4j2 适配中,Marker 参数不会写入 ScxLogRecord。
也就是说,Marker 不参与默认输出格式,也不参与过滤。
当前 ScxLog4jLoggerContext#hasLogger(...) 系列方法返回 false。
因此不要依赖它判断 logger 是否已经存在。
SCX Logging 对全局 logger 缓存和配置表做了基础并发处理。
logger 缓存、精确名称配置和正则配置都使用:
ConcurrentHashMap
配置版本和声明顺序使用:
AtomicLong
根配置通过 volatile 引用发布。logger 内部缓存当前配置和对应的配置版本,版本变化后会在后续调用中重新解析。
配置解析不保证并发修改期间的严格快照一致性。一次解析可能看到修改前后的混合状态,但之后的调用会根据新版本再次解析并最终使用新配置。
需要注意:
recorders 本身是否线程安全,取决于具体 recorder 实现
例如自定义 recorder 如果内部使用普通 ArrayList 收集日志,在多线程日志输出时需要自己保证同步。
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import dev.scx.logging.recorder.ConsoleRecorder;
import java.util.List;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.ERROR;
public class LoggingDemo {
public static void main(String[] args) {
ScxLogging.rootConfig(new ScxLoggerConfig(
DEBUG,
false,
List.of(new ConsoleRecorder())
));
var logger = ScxLogging.getLogger(LoggingDemo.class);
logger.log(DEBUG, "debug message", null);
logger.log(ERROR, "error message", new RuntimeException("test"));
}
}
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import dev.scx.logging.recorder.ConsoleRecorder;
import org.slf4j.LoggerFactory;
import java.util.List;
import static java.lang.System.Logger.Level.DEBUG;
public class Slf4jDemo {
public static void main(String[] args) {
ScxLogging.rootConfig(new ScxLoggerConfig(
DEBUG,
false,
List.of(new ConsoleRecorder())
));
var logger = LoggerFactory.getLogger(Slf4jDemo.class);
logger.debug("debug {}", 1);
logger.info("info {}", 2);
logger.error("error {}", 3, new RuntimeException("test"));
}
}
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.ERROR;
public class ConfigDemo {
public static void main(String[] args) {
ScxLogging.config(
Pattern.compile("demo\\..*"),
new ScxLoggerConfig(DEBUG, true, null)
);
ScxLogging.config(
Pattern.compile("demo\\.quiet\\..*"),
new ScxLoggerConfig(ERROR, null, null)
);
var logger1 = LoggerFactory.getLogger("demo.UserService");
var logger2 = LoggerFactory.getLogger("demo.quiet.Job");
logger1.debug("会输出");
logger2.debug("不会输出");
logger2.error("会输出");
}
}
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import dev.scx.logging.recorder.ConsoleRecorder;
import dev.scx.logging.recorder.FileRecorder;
import java.nio.file.Path;
import java.util.List;
import static java.lang.System.Logger.Level.INFO;
public class RecorderDemo {
public static void main(String[] args) {
ScxLogging.rootConfig(new ScxLoggerConfig(
INFO,
false,
List.of(
new ConsoleRecorder(),
new FileRecorder(Path.of("./logs"))
)
));
var logger = ScxLogging.getLogger("demo");
logger.log(INFO, "hello file and console", null);
}
}
如果当前日期是:
2026-05-08
文件输出路径类似:
./logs/2026-05-08.log
import dev.scx.logging.ScxLogRecord;
import dev.scx.logging.ScxLogRecorder;
import dev.scx.logging.ScxLoggerConfig;
import dev.scx.logging.ScxLogging;
import java.util.ArrayList;
import java.util.List;
import static java.lang.System.Logger.Level.DEBUG;
public class MemoryRecorderDemo {
public static void main(String[] args) {
var recorder = new MemoryRecorder();
ScxLogging.rootConfig(new ScxLoggerConfig(
DEBUG,
false,
List.of(recorder)
));
var logger = ScxLogging.getLogger("demo");
logger.log(DEBUG, "hello", null);
System.out.println(recorder.records().size());
}
public static class MemoryRecorder implements ScxLogRecorder {
private final List<ScxLogRecord> records = new ArrayList<>();
@Override
public void record(ScxLogRecord logRecord) {
records.add(logRecord);
}
public List<ScxLogRecord> records() {
return records;
}
}
}
public static ScxLogger getLogger(String name)
public static ScxLogger getLogger(Class<?> clazz)
public static ScxLoggerConfig rootConfig()
public static void rootConfig(ScxLoggerConfig rootConfig)
public static ScxLoggerConfig config(String name)
public static void config(String name, ScxLoggerConfig newConfig)
public static void removeConfig(String name)
public static ScxLoggerConfig config(Pattern regex)
public static void config(Pattern regex, ScxLoggerConfig newConfig)
public static void removeConfig(Pattern regex)
public boolean isLoggable(Level level)
public void log(Level level, String message, Throwable t)
public String name()
public ScxLoggerConfig config()
public ScxLoggerConfig(
Level level,
Boolean stackTrace,
List<ScxLogRecorder> recorders
)
public Level level()
public Boolean stackTrace()
public List<ScxLogRecorder> recorders()
void record(ScxLogRecord logRecord)
public void record(ScxLogRecord logRecord)
public String format(ScxLogRecord logRecord)
public FileRecorder(Path storedDirectory)
public static void writeToFile(Path path, String data)
public String getLogFileName(TemporalAccessor temporal)
public void record(ScxLogRecord logRecord)
public String format(ScxLogRecord logRecord)
核心对象只有几类:
ScxLogging
ScxLogger
ScxLoggerConfig
ScxLogRecord
ScxLogRecorder
其它 JDK、SLF4J、Log4j2 相关类都是适配层。
这意味着你可以只用 SCX Logging 自己的 API,也可以通过已有日志门面接入它。
SCX Logging 内部统一使用:
java.lang.System.Logger.Level
SLF4J 和 Log4j2 入口都会先映射到 JDK level,再交给 ScxLogger。
这样可以减少内部级别体系数量。
字符串重载用于精确 logger 名称:
ScxLogging.config("dev.scx.demo.App", config);
Pattern 重载用于正则匹配:
ScxLogging.config(Pattern.compile("dev\\.scx\\..*"), config);
正则使用 matches(),要求匹配完整 logger 名称。
配置解析从根配置开始,然后按声明顺序合并全部匹配规则。
后声明的规则只覆盖自己非 null 的字段。
这让不同规则可以分别设置 level、stackTrace 和 recorders。
ScxLogger 创建 ScxLogRecord 后,会直接遍历 recorders 并调用:
r.record(logRecord);
当前核心没有异步队列。
因此:
Exception 会被忽略,不会传播到用户代码。需要注意,内置 FileRecorder 写入失败时也会吞掉异常。
FileRecorder 只负责按日期写入文件。
它不负责:
日志滚动
日志压缩
日志清理
文件大小切分
异步写入
并发写入优化
复杂 layout
如果需要这些能力,应扩展 recorder 或接入更完整的日志系统。
如果一条日志带有 Throwable,默认格式会输出异常堆栈。
如果没有 Throwable,但开启了 stackTrace,才会输出调用上下文栈。
这样可以避免同一条日志同时输出两套堆栈。
根配置、名称配置或正则配置发生变化后,全局配置版本会更新。
已经存在的 logger 会在之后的 config()、isLoggable(...) 或 log(...) 调用中重新解析配置。
SCX Logging 默认只输出到控制台。
如果需要文件输出,需要显式设置 FileRecorder。
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(new FileRecorder(Path.of("./logs")))
));
slf4j-api 和 log4j-api 是 optional。
这表示 SCX Logging 可以作为很小的直接日志库使用。
只有当项目本身需要 SLF4J 或 Log4j2 API 时,才需要对应 API 出现在 classpath 中。
它可以作为 SLF4J 2.x 的 provider 使用,但它本身不仅仅是 SLF4J 实现。
它还有自己的核心 API,并且同时接入 JDK System.Logger 和 Log4j2 API。
默认 level 是:
ERROR
所以默认只输出 ERROR 及以上级别。
var root = ScxLogging.rootConfig();
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.DEBUG,
root.stackTrace(),
root.recorders()
));
var root = ScxLogging.rootConfig();
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.TRACE,
root.stackTrace(),
root.recorders()
));
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(new FileRecorder(Path.of("./logs")))
));
如果需要同时输出到控制台和文件,可以把 ConsoleRecorder 和 FileRecorder 一起放入列表。
默认按日期命名:
yyyy-MM-dd.log
例如:
2026-05-08.log
如果写入时发现父目录不存在,会尝试创建目录。
当前实现会吞掉写入异常。
也就是说,文件写入失败不会影响业务流程。
可以继承 ConsoleRecorder 或 FileRecorder,覆盖 format(...)。
new ConsoleRecorder() {
@Override
public String format(ScxLogRecord record) {
return record.message() + System.lineSeparator();
}
}
也可以直接实现 ScxLogRecorder。
实现 ScxLogRecorder。
public final class MyRecorder implements ScxLogRecorder {
@Override
public void record(ScxLogRecord logRecord) {
// 自己处理
}
}
然后放入配置:
ScxLogging.rootConfig(new ScxLoggerConfig(
System.Logger.Level.ERROR,
false,
List.of(new MyRecorder())
));
不是。
ScxLogging.config("demo.UserService", config);
这里使用精确名称匹配。正则匹配需要显式传入 Pattern:
ScxLogging.config(Pattern.compile("demo\\..*"), config);
dev.scx.* 没匹配到?如果传给 config(String, ...),它会被当作完整 logger 名称,不会作为正则解析。
正则匹配应写成:
Pattern.compile("dev\\.scx\\..*")
会。
配置变化后,已有 logger 会在下一次解析配置时使用新规则。
会重新计算。
配置删除后,已有 logger 在下一次解析配置时会基于剩余规则和根配置得到新的结果。
logger.config() 返回当前解析出的不可变配置,不能直接修改。
可以使用精确名称配置:
ScxLogging.config(
"demo",
new ScxLoggerConfig(System.Logger.Level.DEBUG, null, null)
);
根配置变化后,已有 logger 会在后续调用中重新解析。
如果某个字段被名称配置或正则配置覆盖,则该字段仍使用后声明规则中的值。
它表示是否在日志中附加当前调用上下文栈。
var root = ScxLogging.rootConfig();
ScxLogging.rootConfig(new ScxLoggerConfig(
root.level(),
true,
root.recorders()
));
不会。
默认格式中,Throwable 优先。
如果 Throwable 不为 null,会输出 Throwable 堆栈;否则才可能输出调用上下文栈。
当前不会。
Marker 参数不会写入 ScxLogRecord。
当前不会。
Marker 参数不会写入 ScxLogRecord。
当前不会。
ScxSLF4JServiceProvider 提供的是 BasicMDCAdapter,但 ScxLogRecord 中没有 MDC 字段,默认格式也不会输出 MDC。
当前 SCX Logging 的 Log4j2 适配不会读取 Log4j2 配置文件。
它只是把 Log4j2 API 调用桥接到 ScxLogger。
ConsoleRecorder 中规定:
level >= ERROR System.err
其它级别 System.out
这样错误日志会走标准错误流。
不是。
recorders 是同步调用。
如果需要异步输出,可以自己实现异步 ScxLogRecorder。
logger 缓存和配置表使用并发容器,配置变化通过版本号通知已有 logger 重新解析。
配置解析在并发更新期间不保证严格快照一致性,但会最终使用新配置。
具体 recorder 是否线程安全,取决于 recorder 自身实现。
适合下面这些场景:
System.Logger、SLF4J 和 Log4j2。