[JAVA]

Aus verschiedenen Gründen wurde die Spring Boot-Anwendung, die auf dem integrierten Tomcat ausgeführt wurde, in einen Krieg verwandelt und auf dem Tomcat-Server bereitgestellt. Zu diesem Zeitpunkt jedoch java.util.logging (unten) Es gab ein Problem, dass die Protokollausgabe mit dem JUL-Logger nicht mehr im App-Protokoll, sondern in der Tomcat-Protokolldatei (Catalina.log) ausgegeben wurde. Eine Notiz über die Ursache, wie man damit umgeht und was Sie gelernt haben. Umgebung

OS

Windows 10 (64bit)

Java

openjdk 11.0.2

Tomcat Server

9.0.19

Spring Boot

2.1.4

Status beim Ausführen mit integriertem Tomcat

Quellcode

Projektstruktur


|-build.gradle
`-src/main/
  |-java/
  | `-sample/jul/
  |   `-ExecutableJarApplication.java
  `-resources/
    `-logback.xml

build.gradle


plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

sourceCompatibility = '11'
targetCompatibility = '11'
compileJava.options.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

ExecutableJarApplication.java


package sample.jul;

import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class ExecutableJarApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExecutableJarApplication.class, args);
    }
    
    private static final java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(ExecutableJarApplication.class.getName());
    private static final org.slf4j.Logger slf4jLogger = LoggerFactory.getLogger(ExecutableJarApplication.class);

    @GetMapping("/hello")
    public void hello() {
        julLogger.info("Hello JUL Logger!!");
        slf4jLogger.info("Hello SLF4J Logger!!");
    }
}

Dies dient nur zur Überprüfung, und ich glaube nicht, dass verschiedene Logger in einer Klasse in einer tatsächlichen Anwendung gemischt werden. Tatsächlich nehmen wir einen Fall an, in dem die von Ihnen verwendete Drittanbieter-Bibliothek JUL zur Ausgabe von Protokollen verwendet.

logback.xml


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%-5level] %-15logger{15} - %m%n</pattern>
        </encoder>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

Ausführungsergebnis

Senden Sie nach dem Booten mit bootRun eine GET-Anfrage an http: // localhost: 8080 / hello.

Konsolenausgabe


[INFO ] s.j.ExecutableJarApplication - Hello JUL Logger!!
[INFO ] s.j.ExecutableJarApplication - Hello SLF4J Logger!!

Beide Logger geben problemlos aus.

Bei Konvertierung in War und Bereitstellung in Tomcat

Quellcode

Projektstruktur


|-build.gradle
`-src/main/
  |-java/
  | `-sample/jul/
  |   `-WarApplication.java
  `-resources/
    `-logback.xml

build.gradle


plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'war'
}

apply plugin: 'io.spring.dependency-management'

sourceCompatibility = '11'
targetCompatibility = '11'
compileJava.options.encoding = "UTF-8"

war {
    enabled = true
    baseName = "sample"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
}

WarApplication.java


package sample.jul;

import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class WarApplication extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(WarApplication.class);
    }

    private static final java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(WarApplication.class.getName());
    private static final org.slf4j.Logger slf4jLogger = LoggerFactory.getLogger(WarApplication.class);

    @GetMapping("/hello")
    public void hello() {
        julLogger.info("Hello JUL Logger!!");
        slf4jLogger.info("Hello SLF4J Logger!!");
    }
}

Abgesehen von den für die Kriegskonvertierung erforderlichen Änderungen (Protokollausgabe usw.) bleiben sie unverändert.

logback.xml bleibt unverändert.

Ausführungsergebnis

Stellen Sie es in Tomcat bereit und senden Sie eine GET-Anforderung an "http: // localhost: 8080 / sample / hello".

Tomcat-Konsolenausgabe


24-Apr-2019 22:15:57.041 Informationen[http-nio-8080-exec-3] sample.jul.WarApplication.hello Hello JUL Logger!!
[INFO ] s.j.WarApplication - Hello SLF4J Logger!!

Obwohl es ausgegeben wird, unterscheidet sich das Format von JUL von dem in logback.xml angegebenen.

Wenn Sie sich außerdem das Tomcat-Protokoll "Catalina.log" ansehen,

catalina.log


24-Apr-2019 22:15:57.041 Informationen[http-nio-8080-exec-3] sample.jul.WarApplication.hello Hello JUL Logger!!

Es wird ausgegeben.

Was ist los

Brücke zwischen JUL und SLF4J

SLF4J hat eine Funktion zum Überbrücken der an den Logger von JUL ausgegebenen Inhalte mit dem Logger auf der SLF4J-Seite.

Hierfür wird ein Mechanismus verwendet, der als JUL-Handler bezeichnet wird.

In JULs Logger befindet sich ein Handler (in der Klasse Handler. /logging/Handler.html)). Die an den Logger ausgegebene Protokollinformation wird an diesen Handler übergeben, und die eigentliche Verarbeitung der Protokollausgabe wird vom Handler ausgeführt.

Beispielsweise gibt ConsoleHandler Protokolle an die Konsole aus, wie der Name schon sagt. Machen. Andererseits gibt FileHandler das Protokoll in eine Datei aus.

Das von SLF4J bereitgestellte "jul-to-slf4j.jar" bietet einen "Handler, der die Ausgabe an den SLF4J-Logger delegiert". Wenn Sie dies im JUL-Root-Logger registrieren, werden die an den JUL-Logger ausgegebenen Protokollinformationen auch an den SLF4J-Logger übergeben.

Tatsächlich sieht die Bridge-Klasse in jul-to-slf4j.jar folgendermaßen aus:

SLF4JBridgeHandler.java


...
public class SLF4JBridgeHandler extends Handler {
    ...
    public static void install() {
        LogManager.getLogManager().getLogger("").addHandler(new SLF4JBridgeHandler());
    }
    ...

Wenn Sie die Methode install () ausführen, werden Sie feststellen, dass Sie den Root-Logger von LogManager erhalten und SLF4JBridgeHandler hinzufügen.

Brückenbedingungen

Wenn Sie SLF4J mit Spring Boot verwenden, sollte diese SLF4JBridgeHandler.install () -Methode beim Start ausgeführt werden.

Insbesondere wird dies in einer Klasse namens "Slf4JLoggingSystem" durchgeführt, und die Implementierung ist wie folgt.

Slf4JLoggingSystem.java


    private void configureJdkLoggingBridgeHandler() {
        try {
            if (isBridgeJulIntoSlf4j()) {
                removeJdkLoggingBridgeHandler();
                SLF4JBridgeHandler.install(); //★ Registrieren Sie die Brücke hier
            }
        }
        catch (Throwable ex) {
            // Ignore. No java.util.logging bridge is installed.
        }
    }

    protected final boolean isBridgeJulIntoSlf4j() {
        return isBridgeHandlerAvailable() && isJulUsingASingleConsoleHandlerAtMost();
    }

    protected final boolean isBridgeHandlerAvailable() {
        return ClassUtils.isPresent(BRIDGE_HANDLER, getClassLoader());
    }

    private boolean isJulUsingASingleConsoleHandlerAtMost() {
        Logger rootLogger = LogManager.getLogManager().getLogger("");
        Handler[] handlers = rootLogger.getHandlers();
        return handlers.length == 0
                || (handlers.length == 1 && handlers[0] instanceof ConsoleHandler);
    }

Es gibt verschiedene Bedingungen für den Aufruf von "install ()". Wenn Sie sich jedoch die letzte "isJulUsingASingleConsoleHandlerAtMost ()" ansehen, können Sie sehen, dass die folgende ** eine ** Bedingung erfüllt sein muss. ..

Handler, den Tomcat standardmäßig registriert

Tomcat verwendet JUL für die Protokollausgabe (genau genommen enthält es eine eigene Erweiterung, Details werden jedoch später beschrieben).

Die Standard-JUL-Konfigurationsdatei für Tomcat befindet sich in "$ {CATALINA_HOME} / conf / logging.properties". Der Handler des Root-Loggers ist wie folgt eingestellt.

logging.properties


.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler

Zwei Handler, Dateiausgabe und Konsolenausgabe, werden registriert. (Dieser 1catalina.org.apache.juli.AsyncFileHandler ist ein Handler, der Catalina.log ausgibt.)

Mit anderen Worten, in der auf Tomcat bereitgestellten Anwendung sind standardmäßig zwei Handler im Root-Logger registriert, die von "LogManager.getLogManager (). GetLogger (" ")" abgerufen werden können.

Dann sind die obigen "Bedingungen zum Registrieren der Brücke von SLF4J bei Spring Boot" nicht erfüllt.

Infolgedessen wurde die SLF4J-Bridge nicht registriert und die Tomcat-Standardeinstellungen (Catalina.log und Ausgabe an die Konsole) wurden auf die Protokollausgabe über JUL angewendet.

Die Atmosphäre ist wie ↓.

jul.jpg

Registrieren Sie die Brücke zwangsweise als Test

Sie können die Bridge zwangsweise registrieren, da sie nicht registriert ist, weil sie die von Spring Boot festgelegten Bedingungen nicht erfüllt.

WarApplication.java


package sample.jul;

import org.slf4j.bridge.SLF4JBridgeHandler;
...

@SpringBootApplication
@RestController
public class WarApplication extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        SLF4JBridgeHandler.install(); //★ Registrieren Sie die Brücke zwangsweise
        return builder.sources(WarApplication.class);
    }

    ...
}

Ausführungsergebnis


24-Apr-2019 22:59:23.405 Informationen[http-nio-8080-exec-18] sample.jul.WarApplication.hello Hello JUL Logger!!
[INFO ] s.j.WarApplication - Hello JUL Logger!!
[INFO ] s.j.WarApplication - Hello SLF4J Logger!!

Es wird jetzt in dem in logback.xml angegebenen Format ausgegeben. Die Tomcat-Standardeinstellungen sind jedoch weiterhin gültig.

Wenn Sie die gleiche Atmosphäre wie zuvor zeigen, sieht es wie ↓ aus.

jul.jpg

Da ich gerade eine Bridge hinzugefügt habe, wird die Protokollausgabe zwischen der Ausgabe auf der Tomcat-Seite und der Ausgabe auf der Anwendungsseite dupliziert.

Problemumgehung

Löschen Sie den Root-Logger-Handler

python


package sample.jul;

...

import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.Logger;

@SpringBootApplication
@RestController
public class WarApplication extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        Logger rootLogger = LogManager.getLogManager().getLogger("");
        for (Handler handler : rootLogger.getHandlers()) {
            rootLogger.removeHandler(handler);
        }

        return builder.sources(WarApplication.class);
    }

    ...
}

Wenn der Root-Logger unter den Bedingungen von Spring Boot keinen Handler hat, wird die Bridge registriert. Wenn es einen Handler gibt, den Tomcat standardmäßig verwendet, wird die Protokollausgabe dupliziert.

Wenn Sie also vor dem Booten alle Handler aus dem Root-Logger entfernen, sollten Sie sich in demselben Zustand befinden, in dem Sie den integrierten Tomcat ausgeführt haben.

Wenn Sie dies bereitstellen und den Vorgang überprüfen, sieht es wie folgt aus.

Tomcat-Konsolenausgabe


[INFO ] s.j.WarApplication - Hello JUL Logger!!
[INFO ] s.j.WarApplication - Hello SLF4J Logger!!

Es gibt keine Ausgabe der Tomcat-Standardeinstellungen, nur die in der Anwendung festgelegte Protokollausgabe von SLF4J. Natürlich wird es nicht mehr in "catalogina.log" ausgegeben.

Das Bild sieht aus wie ↓.

jul.jpg

Ist es in Ordnung, die Root-Logger-Einstellungen neu zu schreiben?

        Logger rootLogger = LogManager.getLogManager().getLogger("");
        for (Handler handler : rootLogger.getHandlers()) {
            rootLogger.removeHandler(handler);
        }

Bei dieser Implementierung wird "LogManager" über die "statische" Methode abgerufen. Dies bedeutet, dass die Instanz von "LogManager" Singleton zu sein scheint.

Wenn man die Implementierung von "LogManager" als Test betrachtet, sieht es wie folgt aus.

LogManager.java


...
public class LogManager {
    // The global LogManager object
    private static final LogManager manager;
    ...
    private volatile Logger rootLogger;
    ...

    public static LogManager getLogManager() {
        if (manager != null) {
            manager.ensureLogManagerInitialized();
        }
        return manager;
    }
    ...

Eine Instanz von "LogManager" ("Manager") wird in einem "statischen Endfeld" gespeichert und ist ein Singleton. Und der Root-Logger wird als Instanzfeld dieses "LogManager" deklariert.

LogManager ist in der Standard-API enthalten und wird vom Bootstrap-Klassenladeprogramm geladen. Dies bedeutet, dass eine Instanz von "LogManager" auf der JVM freigegeben wird.

Da nur eine JVM mit Tomcat ausgeführt wird, beziehen sich alle LogManager, auf die von jeder darauf bereitgestellten Webanwendung zugegriffen wird, auf dieselbe Instanz.

Wenn dann eine bereitgestellte App den Status des Root-Loggers ändert, scheint dies Auswirkungen auf andere bereitgestellte Apps und sogar auf den Tomcat selbst zu haben.

Ich mache mir Sorgen, ob es in Ordnung ist, alle Handler des Root-Loggers zu löschen.

Tomcat trennt die Logger-Verwaltung nach Klassenladeprogramm

Bei der Standardimplementierung von JUL wird ein Root-Logger in der JVM gemeinsam genutzt, was zu den oben genannten Problemen führen kann.

Daher bietet Tomcat einen eigenen "LogManager" an, der Logger für jeden Klassenlader separat verwalten kann und die Standardimplementierung ersetzt.

Mit anderen Worten, die Atmosphäre ist so, dass Sie so etwas wie ↓ tun können.

jul.jpg

Der Umgang mit JUL in Tomcat wird in der offiziellen Dokumentation erläutert.

Apache Tomcat 9 (9.0.19) - Logging in Tomcat

So ersetzen Sie die LogManager-Implementierung

Die Implementierung von "LogManager" selbst ist in der Standardbibliothek vorhanden. Wenn es jedoch darum geht, die Implementierung zu ersetzen, verfügt LogManager selbst über einen Mechanismus zum Ersetzen der Implementierung.

Bei der Implementierung des Instanziierungsteils von "LogManager" sieht es folgendermaßen aus:

LogManager.java



    static {
        manager = AccessController.doPrivileged(new PrivilegedAction<LogManager>() {
            @Override
            public LogManager run() {
                LogManager mgr = null;
                String cname = null;
                try {
                    cname = System.getProperty("java.util.logging.manager");
                    if (cname != null) {
                        try {
                            @SuppressWarnings("deprecation")
                            Object tmp = ClassLoader.getSystemClassLoader()
                                .loadClass(cname).newInstance();
                            mgr = (LogManager) tmp;
                        } catch (ClassNotFoundException ex) {
                            @SuppressWarnings("deprecation")
                            Object tmp = Thread.currentThread()
                                .getContextClassLoader().loadClass(cname).newInstance();
                            mgr = (LogManager) tmp;
                        }
                    }
                } catch (Exception ex) {
                    System.err.println("Could not load Logmanager \"" + cname + "\"");
                    ex.printStackTrace();
                }
                if (mgr == null) {
                    mgr = new LogManager();
                }
                return mgr;

            }
        });
    }

Initialisieren einer Instanz von "Manager" mit dem "statischen" Initialisierer.

Zu diesem Zeitpunkt wird bestätigt, ob die Systemeigenschaft "java.util.logging.manager" angegeben ist. Wenn angegeben, wird die dort angegebene Klasse geladen und die erstellte Instanz wird "manager" zugewiesen.

Derzeit gibt das [Javadoc] von "LogManager" (https://docs.oracle.com/javase/jp/11/docs/api/java.logging/java/util/logging/LogManager.html) an, dass die LogManager-Klasse ist , Es wird beim Start mit der Systemeigenschaft java.util.logging.manager platziert." [^ 1].

[^ 1]: Ich weiß nicht, wer diese Erklärung ist

Wenn Sie sich das Tomcat-Startskript ansehen, sehen Sie, dass es mit Sicherheit wie folgt angegeben ist.

catalina.bat


if not "%LOGGING_MANAGER%" == "" goto noJuliManager
set LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager

Leerer Handler in Konfigurationsdatei

Mit Tomcat können Sie übrigens für jede von Ihnen bereitgestellte Webanwendung eine JUL-Konfigurationsdatei (logging.properties) platzieren.

Sie müssen also nicht einmal den Root-Logger in Ihrer Implementierung leeren, wenn Sie Folgendes tun:

Projektstruktur


|-build.gradle
`-src/main/
  |-java/
  | `-sample/jul/
  |   `-WarApplication.java
  `-resources/
    |-logging.Eigenschaften ★ Hinzugefügt
    `-logback.xml

logging.properties


.handlers=

Der Inhalt besteht darin, den Handler des Root-Loggers zu leeren.

Stellen Sie den Vorgang bereit und überprüfen Sie ihn.

Tomcat-Konsolenausgabe


[INFO ] s.j.WarApplication - Hello JUL Logger!!
[INFO ] s.j.WarApplication - Hello SLF4J Logger!!

Atmosphäre, die gut zu funktionieren scheint.

Referenz

Recommended Posts