The background,

In business development, there are often several people in charge of different business modules in the same project. For example, in a mall system, Mr. Wang is responsible for the member and Rebate modules. Lao Li is responsible for item and promotion modules. Lao Lu was in charge of the campaign module.

The business was just starting, the team was small, there was no budget for microservices, and everyone just shared an application (the same set of code) and wrote code together. The problem is that they all have a need to export logs, and they don’t want their logs to be undisturbed by other people’s logs. What to do?

The solution is to divide logs by service modules. Each service module outputs logs to an independent file or folder, which does not affect each other.

Logback and Log4j2 are supported. Java version >=8 is required. Logback and Log4j2 are supported.

2. Tool introduction

2.1 Configuring Access

Introducing dependencies into your project:

<dependency>
   <groupId>io.github.lvyahui8</groupId>
   <artifactId>feego-common-logging-starter</artifactId>
   <version>1.0.0</version>
</dependency>
Copy the code

In any kind of adding annotation @ ModuleLoggerAutoGeneration. It is also possible to annotate multiple modules on different classes.

@Configuration
@ModuleLoggerAutoGeneration({"member"."rebate"."item"."promotion"."campaign"})
public class LoggingConfiguration {}Copy the code

2.2 usage

The SystemLogger enumerator is generated at compile time. This enumerator can be used to log logs. It can be found in target\generated-sources.

public enum SystemLogger implements ModuleLogger { 
  member,
  rebate,
  item,
  promotion,
  campaign,
  ;

  @Override
  public Logger getInnerLogger(a) {
    return ModuleLoggerRepository.getModuleLogger(this.name()); }}Copy the code

It supports all methods of the SLF4J standard interface (org.slf4j.logger), in addition to extending the following methods:

void trace(LogSchema schema) 
void debug(LogSchema schema) 
void info(LogSchema schema) 
void warn(LogSchema schema) 
void error(LogSchema schema) 
Copy the code

The usage method is very simple, take the campaign module to log an info-level log as an example, the code is as follows:

SystemLogger.campaign.info(
   LogSchema.empty().of("id".1).of("begin".20200307).of("end".20200310).of("status".1));Copy the code

After the program runs, the corresponding log file will be generated in the log directory according to the service module:

The Campaign module will record a log, which is the one we printed above

Journal # separator | | and the output directory can be configuration changes

Three, the implementation principle

The implementation of the tool involves the following points of knowledge

  • Java compile-time annotation processor
  • Enumeration classes implement interfaces
  • Spring ApplicationReadyEvent Event handling
  • Find all classes that implement an interface
  • Programmatically dynamically configure logback or log4j2
  • Factory method pattern
  • Spring – the boot starter

Here is the realization of one disassembly tool, and respectively introduced the above knowledge points

3.1 Generation principle of enumeration classes

Enumeration classes are generated at compile time, and in this case it is the compile-time annotation handler that is used.

Compile-time annotation processor is a mechanism provided by the JDK to scan code annotations and process them during compilation of a Java program before the program is ready to run. Java code can be generated or modified through this mechanism, and the compiled class files of generated Java code are usually packaged into JAR packages by the IDE tool. The famous Lombok framework is based on this mechanism.

Specific how to write compile-time annotation processor will not expand, interested students please consult information.

What does our compiler, ModuleLoggerProcessor, do

  1. Traverses the current code module, all of which have@ModuleLoggerAutoGenerationThe package name of the annotated class, finding a common prefix for the package name of the enumerated class to be generated
  2. Add one to the public package namefeego.commonPrefixes (function described below)
  3. Create a Java file
  4. traverse@ModuleLoggerAutoGenerationAnnotated value, with value as an enumeration value, outputs enumeration class code

Specific code: github.com/lvyahui8/fe…

3.2 How can ENUMeration Classes have logging capability?

As you can see, the tool generates the SystemLogger enumeration class with very simple code that implements only a ModuleLogger interface and overwrites the getInnerLogger method

public enum SystemLogger implements ModuleLogger { 
  campaign,
  ;

  @Override
  public Logger getInnerLogger(a) {
    return ModuleLoggerRepository.getModuleLogger(this.name()); }}Copy the code

How did a few simple lines of code insert powerful logging capabilities into enumerations?

Take a look at ModuleLogger’s statement:

Github.com/lvyahui8/fe…

public interface ModuleLogger extends org.slf4j.Logger {

    default void info(LogSchema schema) {
        ((ModuleLogger) getInnerLogger()).info(schema);
    }
    // omit the other four methods that take in LogSchema

    @Override
    default void debug(String msg) {
        getInnerLogger().debug(msg);
    }
    
    @Override
    default void info(String msg) {
        getInnerLogger().info(msg);
    }

    // Omit dozens of other methods for org.slf4j.Logger
    
    /**
     * get actual logger
     * @return actual logger
     */
    Logger getInnerLogger(a) ;
}
Copy the code

First, enumerations are actually classes, and classes that inherit from Java.lang.Enum. We know that Java is polymorphic, and classes only support single inheritance, but allow multiple implementations of interfaces, so we can give enumerations wings through interfaces

Second, after Java 8, the interface supports the default implementation, we can write the default method in the interface, subclass can not implement, so our enumeration class can be written very succinctly.

The default method in the interface calls getInnerLogger and forwards the call to the innerLogger. The getInnerLogger method itself is not a default method and must be implemented by the implementation class. Our generated enumeration class implements this method

@Override
public Logger getInnerLogger(a) {
    return ModuleLoggerRepository.getModuleLogger(this.name());
}
Copy the code

It fetches a Logger instance from the ModuleLoggerRepository using a static method. Is this instance an enumeration of SystemLogger? Doesn’t that call go into dead recursion?

As you might have guessed, there must be another class that implements the ModuleLogger interface and ModuleLoggerRepository

That’s an instance of that class.

Yes, that class is DefaultModuleLoggerImpl

public class DefaultModuleLoggerImpl implements ModuleLogger {
    private org.slf4j.Logger logger;

    private String separator;


    public DefaultModuleLoggerImpl(org.slf4j.Logger logger, String separator) {
        this.logger = logger;
        this.separator = separator;
    }

    @Override
    public Logger getInnerLogger(a) {
        return logger;
    }

    @Override
    public void info(LogSchema schema) { LogSchema.Detail detail = schema.build(separator); getInnerLogger().info(detail.getPattern(),detail.getArgs()); }}Copy the code

To this link is clear, to sum up:

  1. Info, DEBUG, error, and so on on enumeration classes, switching to the default implementation method,
  2. The default method goes further to innerLogger
  3. The innerLogger of the enumeration class is again forwarded to the DefaultModuleLoggerImpl instance via the static ModuleLoggerRepository
  4. Finally DefaultModuleLoggerImpl uses the org.slf4j.logger instance in the input to log

3.3 How to initialize ModuleLoggerRepository?

From the above flow, we can see one key thing: ModuleLoggerRepository, the enumeration class we generate at compile time, is a very, very critical bridge to the moduleLogger instance. How is it initialized? How does the moduleLogger instance get generated and put into this?

Here, we are going to see a very important configuration class ModuleLoggerAutoConfiguration

Github.com/lvyahui8/fe…

It is an auto Configuration class for the Spring-Boot starter module. It also implements the ApplicationListener

interface, so that when the Spring process is initialized, ModuleLoggerAutoConfiguration# onApplicationEvent is invoked.

@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
    String storagePath = (loggingProperties.getStoragePath() == null ? System.getProperty("user.home") : loggingProperties.getStoragePath())
            + File.separator + "logs";

    Reflections reflections = new Reflections("feego.common.");
    Set<Class<? extends ModuleLogger>> allModuleLoggers = reflections.getSubTypesOf(ModuleLogger.class);

    String pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} : %m%n";

    for (Class<? extends ModuleLogger> moduleEnumClass : allModuleLoggers) {
        for(Object enumInstance : moduleEnumClass.getEnumConstants()) { Enum<? > em = (Enum<? >) enumInstance; String loggerName = em.name(); ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); String fileName = storagePath + File.separator + loggerName +".log";

            File file = new File(fileName);
            if(! file.getParentFile().exists() && ! file.getParentFile().mkdirs()) {throw new RuntimeException("No permission to create log path!");
            }
            String fileNamePattern = fileName + ".%d{yyyy-MM-dd}.%i";
            ModuleLoggerFactory factory ;
            if ("ch.qos.logback.classic.LoggerContext".equals(loggerFactory.getClass().getName())) {
                factory = new LogbackModuleLoggerFactory(loggingProperties);
            } else if ("org.apache.logging.slf4j.Log4jLoggerFactory".equals(loggerFactory.getClass().getName())){
                factory = new Log4j2ModuleLoggerFactory(loggingProperties);
            } else {
                throw new UnsupportedOperationException("Only logback and log4j2 are supported");
            }
            /* Replace the proxy enumeration implementation with a proxy class */
            ModuleLogger moduleLogger = newDefaultModuleLoggerImpl(factory.getLogger(pattern, loggerName, loggerFactory, fileName, fileNamePattern), loggingProperties.getFieldSeparator()); ModuleLoggerRepository.put(loggerName,moduleLogger); }}}Copy the code

In this method, the reflection tool scans all the SystemLogger classes in the feego.mon package, remember? Our annotation handler generates code under this package prefix, using a package prefix to reduce scanning classes. In fact, you can write your own SystemLogger enumeration class without annotations, as long as you make sure to put it in the feego.com common package or its subpackages.

We iterate through the enumeration class, using the name of the enumeration instance as logger’s name, by checkingLoggerFactory.getILoggerFactory()The implementation classes logback or Log4j2 create different factory classes that generate real ModuleLogger instances using the factory method pattern and add the instances to the ModuleLoggerRepository

3.4 Programmatically Configuring LogBack and Log4j2

Logback and Log4J2 both support the dynamic creation of loggers and appenders, where the factory method pattern is used to generate concrete logger instances

Factory interface:

package io.github.lvyahui8.core.logging.factory;

import org.slf4j.ILoggerFactory;

public interface ModuleLoggerFactory {
    org.slf4j.Logger getLogger(String pattern, String loggerName, ILoggerFactory loggerFactory, String fileName, String fileNamePattern);
}
Copy the code

Log4j2 factory implementation class

public class Log4j2ModuleLoggerFactory implements ModuleLoggerFactory {
    private ModuleLoggerProperties loggingProperties;

    public Log4j2ModuleLoggerFactory(ModuleLoggerProperties loggingProperties) {
        this.loggingProperties = loggingProperties;
    }

    @Override
    public Logger getLogger(String pattern, String loggerName, ILoggerFactory loggerFactory, String fileName, String fileNamePattern) {
        /* omit the programmatic configuration log4j2*/}}Copy the code

Logback factory implementation class

public class LogbackModuleLoggerFactory implements ModuleLoggerFactory {

    private ModuleLoggerProperties loggingProperties;

    public LogbackModuleLoggerFactory(ModuleLoggerProperties loggingProperties) {
        this.loggingProperties = loggingProperties;
    }

    @Override
    public Logger getLogger(String pattern, String loggerName, ILoggerFactory loggerFactory, String fileName, String fileNamePattern) {
        /* omit the programmatic configuration logback*/}}Copy the code

Attached here is the complete code and logback and log4j2 official documentation, interested students can go to understand the details

  • Full code: github.com/lvyahui8/fe…
  • Log4j2 (Programmatically Modifying the Current Configuration after Initialization) : logging.apache.org/log4j/2.x/m…
  • Logback: logback. Qos. Ch/manual/conf…

Four,

Writing this tool is also a temporary idea, looking for similar open source software on the Internet, but did not find, so I realized one. Maybe the function is not perfect enough, there are many improvements, if free, I will continue to optimize and improve. Of course, this is dependent on user feedback and suggestions for improvement.

Finally, the github connection of the tool is attached. Welcome star, suggestions, co-construction and use. Thank you very much.

Github.com/lvyahui8/fe…