How to Build a Custom Java Logging Framework from Scratch

This article walks through the complete process of designing and implementing a lightweight Java logging framework—covering motivation, core components, annotation processing, configuration handling, message queuing, and a full example with code snippets and test results.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Build a Custom Java Logging Framework from Scratch

Introduction

In June 2019 the author joined a company with a three‑month probation period. While optimizing a slow module that performed massive database operations (over a billion rows), they created a custom logging utility that reduced execution time and earned an early promotion.

Motivation for a Custom "Wheel"

Reasons to build a bespoke solution include: specific business needs not met by existing frameworks, complexity of third‑party libraries, company policies prohibiting external dependencies, lack of suitable existing tools, and personal learning goals.

Design Overview

The framework consists of several core parts:

Constants : defines configuration keys and default values.

ConfigurationUtil : loads configuration from a properties file.

DateUtil : provides date‑time formatting utilities.

SystemUtil : detects the operating system.

FileUtil : handles file creation and appending.

LogUtil : writes log messages to files based on configuration.

LogMsg : represents a log message and is automatically enqueued.

MsgQueue : a thread‑safe queue for message objects.

YzLogWrite : a custom annotation used on fields to declare log metadata.

DealAnnotation : scans configured packages, injects values into annotated fields, and creates LogMsg objects.

DealMsg : a thread that processes messages from MsgQueue and delegates to LogUtil.

StartWork : entry point that triggers annotation processing and message handling.

Key Code Snippets

public interface Constants {
    String CONFIG_FILE_NAME = "yzlogconfig";
    String CONFIG_LOG_PATH = "logpath";
    String CONFIG_SACN_PATH = "scanpath";
    String DEFAULT_CONTENT_PREFIX = "注入值:";
    String DEFAULT_FILE_NAME = "log";
    String MSG_TYPE_LOG = "log";
    String LINUX_LOG_PATH = "/home/data/";
    String WIN_LOG_PATH = "D:/winLog/data/";
}
public class ConfigurationUtil {
    private static Object lock = new Object();
    private static ConfigurationUtil config = null;
    private static ResourceBundle rb = null;
    private ConfigurationUtil(String filename) { rb = ResourceBundle.getBundle(filename); }
    public static ConfigurationUtil getInstance(String filename) { /* singleton logic */ }
    public String getValue(String key) { return rb.containsKey(key) ? rb.getString(key) : ""; }
}
public class DateUtil {
    public static final String DATE_A = "yyyy-MM-dd";
    public static String getDate() { return new SimpleDateFormat(DATE_A).format(new Date()); }
    public static String getDateTime() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); }
}
public class SystemUtil {
    public static boolean isLinux() { return !System.getProperty("os.name").toLowerCase().startsWith("win"); }
}
public class FileUtil {
    public static boolean write(String path, String str) { /* create directories, write with UTF‑8 */ }
}
public class LogUtil {
    public static void write2file(String path, String fileName, String content) {
        String date = DateUtil.getDate() + "/";
        if (path != null && !path.isEmpty()) {
            FileUtil.write(path + date + fileName + ".txt", DateUtil.getDateTime() + ":" + content + "
");
        } else {
            String base = SystemUtil.isLinux() ? Constants.LINUX_LOG_PATH : Constants.WIN_LOG_PATH;
            FileUtil.write(base + date + fileName + ".txt", DateUtil.getDateTime() + ":" + content + "
");
        }
    }
}
public class LogMsg {
    private String path;
    private String content;
    private String fileName;
    private String msgType = "logmsg";
    public LogMsg(String path, String content, String fileName) {
        this.path = path; this.content = content; this.fileName = fileName;
        MsgQueue.push(this);
    }
    // getters and setters omitted for brevity
}
public class MsgQueue {
    private static Queue<LogMsg> queue = new ConcurrentLinkedDeque<>();
    public static boolean push(LogMsg logMsg) { return queue.offer(logMsg); }
    public static LogMsg poll() { return queue.poll(); }
    public static boolean isFinash() { return !queue.isEmpty(); }
}
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface YzLogWrite {
    int value() default -1;
    String path() default "";
    String fileName() default "";
    String msgPrefix() default "";
}
public class DealAnnotation {
    private static String LOG_PATH = ConfigurationUtil.getInstance(Constants.CONFIG_FILE_NAME).getValue(Constants.CONFIG_LOG_PATH);
    private List<String> registyClasses = new ArrayList<>();
    public void injectAndMakeMsg() {
        String scanPath = ConfigurationUtil.getInstance(Constants.CONFIG_FILE_NAME).getValue(Constants.CONFIG_SACN_PATH);
        doScanner(scanPath);
        for (String className : registyClasses) {
            try {
                Class<?> clazz = Class.forName(className);
                for (Field field : clazz.getDeclaredFields()) {
                    if (!field.isAnnotationPresent(YzLogWrite.class)) continue;
                    YzLogWrite ann = field.getAnnotation(YzLogWrite.class);
                    String path = ann.path().isEmpty() ? LOG_PATH : ann.path();
                    String content = ann.msgPrefix().isEmpty() ? Constants.DEFAULT_CONTENT_PREFIX : ann.msgPrefix();
                    String fileName = ann.fileName().isEmpty() ? Constants.DEFAULT_FILE_NAME : ann.fileName();
                    int value = ann.value();
                    new LogMsg(path, content + ":" + value, fileName);
                    field.setAccessible(true);
                    field.setInt(null, value);
                }
            } catch (Exception e) { e.printStackTrace(); }
        }
    }
    private void doScanner(String scanPath) { /* recursive classpath scanning */ }
}
public class DealMsg extends Thread {
    @Override
    public void run() {
        while (MsgQueue.isFinash()) {
            LogMsg logMsg = MsgQueue.poll();
            switch (logMsg.getMsgType()) {
                case "logmsg":
                    LogUtil.write2file(logMsg.getPath(), logMsg.getFileName(), logMsg.getContent());
                    break;
                default:
                    System.out.println("no msg");
            }
        }
        this.interrupt();
    }
    @Override
    public synchronized void start() { this.run(); }
}
public class StartWork {
    public static void doWork() {
        new DealAnnotation().injectAndMakeMsg();
        new DealMsg().start();
    }
}

Testing the Framework

A configuration file yzlogconfig.properties defines logpath=/opt/ and scanpath=cn/yzstu/tt. A demo class in that package uses the @YzLogWrite(value=18, msgPrefix="记录Baldwin的年龄:") annotation on a static field. Running StartWork.doWork() injects the value, creates a log message, and writes 2020-04-06 00:17:30:记录Baldwin的年龄::18 to /opt/2020-04-06/log.txt. The console prints the injected age (18) confirming successful injection.

Conclusion

The tutorial demonstrates how to build a simple, configurable logging component in Java, covering annotation design, reflection‑based injection, configuration management, asynchronous message processing, and practical testing, providing a foundation that can be packaged as a JAR for reuse in other projects.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavamavenannotationFramework
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.