Skip to content

打印流实战:日志输出

你写过这样的代码吗?

java
// ❌ 初级写法:System.out.println 满天飞
System.out.println("用户登录");
System.out.println("用户名: " + username);
System.out.println("登录成功");
// ... 1000 行后 ...
System.out.println("出错了");

这种写法的问题是:没有分级、没有格式、没有文件输出、一行代码改全局。

本节用 PrintWriter 实现一个实用的日志系统。

简单日志工具

java
public class SimpleLogger {
    private static PrintWriter out = new PrintWriter(
        new BufferedWriter(
            new OutputStreamWriter(
                new FileOutputStream("app.log", true), StandardCharsets.UTF_8)),
        true); // autoFlush

    public static void info(String msg) {
        out.println("[" + timestamp() + "] [INFO] " + msg);
    }

    public static void error(String msg) {
        out.println("[" + timestamp() + "] [ERROR] " + msg);
    }

    public static void error(String msg, Throwable t) {
        out.println("[" + timestamp() + "] [ERROR] " + msg);
        t.printStackTrace(out); // 异常栈输出到日志文件
    }

    private static String timestamp() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    }
}

// 使用
SimpleLogger.info("程序启动");
SimpleLogger.error("出错了", new RuntimeException("测试异常"));

日志分级

java
public enum LogLevel { DEBUG, INFO, WARN, ERROR }

public class Logger {
    private static LogLevel currentLevel = LogLevel.INFO;
    private static PrintWriter writer;

    static {
        try {
            writer = new PrintWriter(
                new BufferedWriter(
                    new OutputStreamWriter(
                        new FileOutputStream("app.log", true), StandardCharsets.UTF_8)),
                true);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void setLevel(LogLevel level) {
        currentLevel = level;
    }

    private static void log(LogLevel level, String msg) {
        if (level.ordinal() < currentLevel.ordinal()) return;
        String line = String.format("[%s] [%5s] %s",
            timestamp(), level.name(), msg);
        writer.println(line);
    }

    public static void debug(String msg) { log(LogLevel.DEBUG, msg); }
    public static void info(String msg)  { log(LogLevel.INFO, msg); }
    public static void warn(String msg)  { log(LogLevel.WARN, msg); }
    public static void error(String msg) { log(LogLevel.ERROR, msg); }

    private static String timestamp() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
    }
}

日志文件滚动

java
public class RollingLogger {
    private PrintWriter writer;
    private String basePath;
    private long maxFileSize = 10 * 1024 * 1024; // 10MB
    private int fileCount = 0;

    public RollingLogger(String basePath) {
        this.basePath = basePath;
        openNewFile();
    }

    public synchronized void log(String level, String msg) {
        if (writer == null) return;

        // 检查文件大小
        try {
            FileChannel ch = ((FileOutputStream)writer).getChannel();
            if (ch.size() >= maxFileSize) {
                rotate();
            }
        } catch (IOException e) {
            // 忽略
        }

        writer.printf("[%s] [%5s] %s%n",
            timestamp(), level, msg);
    }

    private void rotate() throws IOException {
        writer.close();
        fileCount++;
        openNewFile();
    }

    private void openNewFile() {
        try {
            String path = basePath + "." + fileCount + ".log";
            writer = new PrintWriter(
                new BufferedWriter(
                    new OutputStreamWriter(
                        new FileOutputStream(path), StandardCharsets.UTF_8)),
                true);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String timestamp() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}

格式化日志输出

java
public class FormattedLogger {
    private PrintWriter writer;

    public FormattedLogger(String path) throws IOException {
        writer = new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(
                    new FileOutputStream(path), StandardCharsets.UTF_8)),
            true);
    }

    // 表格形式日志
    public void logTable(String... columns) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < columns.length; i++) {
            sb.append(String.format("%-20s", columns[i]));
            if (i < columns.length - 1) sb.append(" | ");
        }
        writer.println(sb);
    }

    // 分割线
    public void logSeparator() {
        writer.println("=".repeat(80));
    }
}

// 使用
FormattedLogger logger = new FormattedLogger("table.log");
logger.logSeparator();
logger.logTable("ID", "姓名", "分数", "评级");
logger.logSeparator();
logger.logTable("001", "张三", "98", "A");
logger.logTable("002", "李四", "85", "B");
logger.logSeparator();

记住这些教训

不要用 System.out.println 做日志。 要分级、要格式化、要写文件。 生产环境用 SLF4J + Logback,这只是学习阶段的练习。

基于 VitePress 构建