[JAVA] Que faire si le journal utilisant JUL n'est plus sorti dans le journal de l'application après le déploiement de l'application Spring Boot sur Tomcat en tant que guerre

Pour diverses raisons, l'application Spring Boot qui s'exécutait sur le Tomcat intégré a été transformée en guerre et déployée sur le serveur Tomcat. Cependant, à ce moment-là, java.util.logging (ci-dessous Un problème est survenu en raison du fait que la sortie du journal à l'aide de l'enregistreur JUL) n'était plus sortie dans le journal de l'application, mais était à la place sortie dans le fichier journal Tomcat (catalina.log).

Une note sur la cause, comment y faire face et ce que vous avez appris.

environnement

OS

Windows 10 (64bit)

Java

openjdk 11.0.2

Tomcat Server

9.0.19

Spring Boot

2.1.4

État lors de l'exécution avec Tomcat intégré

Code source

Structure du projet


|-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!!");
    }
}

C'est juste pour vérification, et je ne pense pas que différents enregistreurs seront mélangés dans une classe dans une application réelle. En fait, il est supposé que la bibliothèque tierce utilisée génère des journaux à l'aide de JUL.

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>

Résultat d'exécution

Après avoir démarré avec bootRun, envoyez une requête GET à http: // localhost: 8080 / hello.

Sortie de la console


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

Les deux enregistreurs sortent sans problème.

Une fois converti en guerre et déployé sur Tomcat

Code source

Structure du projet


|-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!!");
    }
}

Hormis les changements requis pour la conversion War (sortie du journal, etc.), ils restent tels quels.

logback.xml est inchangé.

Résultat d'exécution

Déployez sur Tomcat et envoyez une requête GET à http: // localhost: 8080 / sample / hello.

Sortie de la console Tomcat


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

Bien qu'il soit sorti, le format de JUL est différent de celui spécifié dans logback.xml.

De plus, si vous regardez le journal Tomcat catalina.log,

catalina.log


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

Il est en cours de sortie.

Que se passe-t-il

Pont entre JUL et SLF4J

SLF4J a une fonction pour relier la sortie de contenu à l'enregistreur de JUL à l'enregistreur du côté SLF4J.

Un mécanisme appelé gestionnaire JUL est utilisé pour cela.

Les enregistreurs JUL ont un gestionnaire à l'intérieur (dans la classe, Handler /logging/Handler.html)). La sortie des informations du journal vers le journal est transmise à ce gestionnaire et le traitement de sortie du journal réel est effectué par le gestionnaire.

Par exemple, ConsoleHandler envoie les journaux à la console comme son nom l'indique. Faire. D'autre part, FileHandler renvoie le journal dans un fichier.

Le jul-to-slf4j.jar fourni par SLF4J fournit un" gestionnaire qui délègue la sortie à l'enregistreur SLF4J ". En enregistrant cela dans l'enregistreur racine de JUL, la sortie des informations du journal vers l'enregistreur de JUL sera également transmise à l'enregistreur de SLF4J.

En fait, la classe bridge dans jul-to-slf4j.jar ressemble à ceci:

SLF4JBridgeHandler.java


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

Si vous exécutez la méthode ʻinstall () , vous verrez que vous obtenez l'enregistreur racine de LogManager et que vous ajoutez SLF4JBridgeHandler`.

Conditions du pont

Lors de l'utilisation de SLF4J avec Spring Boot, cette méthode SLF4JBridgeHandler.install () est censée être exécutée au démarrage.

Plus précisément, cela se fait dans une classe appelée Slf4JLoggingSystem, et l'implémentation est la suivante.

Slf4JLoggingSystem.java


    private void configureJdkLoggingBridgeHandler() {
        try {
            if (isBridgeJulIntoSlf4j()) {
                removeJdkLoggingBridgeHandler();
                SLF4JBridgeHandler.install(); //★ Enregistrez le pont ici
            }
        }
        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);
    }

Il y a plusieurs conditions pour appeler ʻinstall () , mais si vous regardez le dernier ʻisJulUsingASingleConsoleHandlerAtMost (), vous pouvez voir que la ** une des ** conditions suivantes doit être remplie. ..

Gestionnaire que Tomcat enregistre par défaut

Tomcat utilise JUL pour la sortie du journal (à proprement parler, il contient sa propre extension, mais les détails seront décrits plus tard).

Le fichier de configuration JUL par défaut pour Tomcat se trouve dans $ {CATALINA_HOME} / conf / logging.properties. Et le gestionnaire de l'enregistreur racine est défini comme suit.

logging.properties


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

Deux gestionnaires, la sortie de fichier et la sortie de console, sont enregistrés. (Ce 1catalina.org.apache.juli.AsyncFileHandler est un gestionnaire qui génère catalina.log)

En d'autres termes, dans l'application déployée sur Tomcat, deux gestionnaires sont enregistrés par défaut dans le logger racine qui peut être obtenu par LogManager.getLogManager (). GetLogger (" ").

Ensuite, les «conditions d'enregistrement du pont de SLF4J avec Spring Boot» ci-dessus ne seront pas satisfaites.

En conséquence, le pont SLF4J n'a pas été enregistré et les paramètres par défaut de Tomcat (catalina.log et sortie vers la console) ont été appliqués à la sortie du journal via JUL.

L'atmosphère est comme ↓.

jul.jpg

Enregistrer de force le pont comme essai

Vous pouvez enregistrer le pont de force car il n'est pas enregistré car il ne répond pas aux conditions définies par Spring Boot.

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(); //★ Enregistrez le pont de force
        return builder.sources(WarApplication.class);
    }

    ...
}

Résultat d'exécution


24-Apr-2019 22:59:23.Informations 405[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!!

Il est maintenant sorti au format spécifié dans logback.xml. Cependant, les paramètres par défaut de Tomcat sont toujours valides.

Si vous montrez la même atmosphère qu'avant, cela ressemble à ↓.

jul.jpg

Depuis que je viens d'ajouter un pont, la sortie du journal est dupliquée entre la sortie côté Tomcat et la sortie côté application.

solution de contournement

Supprimer le gestionnaire de journalisation racine

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);
    }

    ...
}

Dans les conditions de Spring Boot, si l'enregistreur racine n'a pas de gestionnaire, le pont sera enregistré. En outre, s'il existe un gestionnaire que Tomcat utilise par défaut, la sortie du journal sera dupliquée.

Donc, si vous supprimez tous les gestionnaires de l'enregistreur racine avant le démarrage, vous devriez être dans le même état que lorsque vous exécutiez sur le Tomcat intégré.

Si vous déployez ceci et vérifiez le fonctionnement, ce sera comme suit.

Sortie de la console Tomcat


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

Il n'y a pas de sortie des paramètres par défaut de Tomcat, uniquement la sortie de journal par SLF4J définie dans l'application. Bien sûr, il n'est plus affiché dans catalina.log.

L'image ressemble à ↓.

jul.jpg

Est-il acceptable de réécrire les paramètres de l'enregistreur racine?

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

En regardant cette implémentation, LogManager obtient via la méthode static. Ainsi, l'instance de LogManager semble être singleton.

En regardant l'implémentation de LogManager comme un essai, c'est comme suit.

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;
    }
    ...

Une instance de LogManager ( manager) est stockée dans un champ static final et est un singleton. Et le journal racine est déclaré comme un champ d'instance de ce LogManager.

LogManager est inclus dans l'API standard et est chargé par le chargeur de classe bootstrap. Cela signifie qu'une instance de LogManager sera partagée sur la JVM.

Puisqu'il n'y a qu'une seule JVM exécutant Tomcat, tous les LogManagers accédés à partir de chaque application Web déployée dessus feront référence à la même instance.

Ensuite, si une application déployée change l'état de l'enregistreur racine, il semble que cela affectera d'autres applications déployées et même le Tomcat lui-même.

Je m'inquiète de savoir s'il est correct de supprimer tous les gestionnaires de l'enregistreur racine.

Tomcat sépare la gestion des enregistreurs par le chargeur de classe

Avec l'implémentation par défaut de JUL, un enregistreur racine est partagé au sein de la JVM, ce qui peut provoquer les problèmes mentionnés ci-dessus.

Par conséquent, Tomcat fournit son propre LogManager qui peut gérer les journaux séparément pour chaque chargeur de classe et remplace l'implémentation par défaut.

En d'autres termes, l'atmosphère est telle que vous pouvez faire quelque chose comme ↓.

jul.jpg

La manipulation de JUL dans Tomcat est expliquée dans la documentation officielle.

Apache Tomcat 9 (9.0.19) - Logging in Tomcat

Comment remplacer l'implémentation de LogManager

L'implémentation de LogManager elle-même existe dans la bibliothèque standard. Cependant, quand il s'agit de remplacer l'implémentation, LogManager lui-même dispose d'un mécanisme pour remplacer l'implémentation.

En regardant l'implémentation de la partie instanciation de LogManager, cela ressemble à ceci:

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;

            }
        });
    }

Initialisation d'une instance de manager avec l'initialiseur static.

À ce stade, il est confirmé si la propriété système java.util.logging.manager est spécifiée. Si spécifié, la classe qui y est spécifiée est chargée et l'instance créée est affectée à manager.

Pour le moment, dans [Javadoc] de LogManager (https://docs.oracle.com/javase/jp/11/docs/api/java.logging/java/util/logging/LogManager.html)," La classe LogManager est , Il est placé à l'aide de la propriété système java.util.logging.manager au démarrage." [^ 1].

[^ 1]: Je ne sais pas qui est cette explication

Si vous regardez le script de démarrage Tomcat, vous pouvez voir qu'il est certainement spécifié comme suit.

catalina.bat


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

Gestionnaire vide dans le fichier de configuration

D'ailleurs, Tomcat vous permet de placer un fichier de configuration JUL (logging.properties) pour chaque application Web que vous déployez.

Ainsi, vous n'avez même pas à vider le logger racine dans votre implémentation si vous procédez comme suit:

Structure du projet


|-build.gradle
`-src/main/
  |-java/
  | `-sample/jul/
  |   `-WarApplication.java
  `-resources/
    |-logging.propriétés ★ Ajouté
    `-logback.xml

logging.properties


.handlers=

Le contenu est de vider le gestionnaire de l'enregistreur racine.

Déployez et vérifiez le fonctionnement.

Sortie de la console Tomcat


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

Ambiance qui semble bien fonctionner.

référence

Recommended Posts

Que faire si le journal utilisant JUL n'est plus sorti dans le journal de l'application après le déploiement de l'application Spring Boot sur Tomcat en tant que guerre
Que faire si l'image d'arrière-plan n'est pas appliquée après le déploiement
WAR l'application WEB par Spring Boot et la déployer sur le serveur Tomcat
[Rails / Docker] Que faire si l'accès est refusé par le navigateur (localhost: 3000) après l'ajout d'un gem
Que faire si le serveur Tomcat meurt
Que faire si la mise à jour ne prend pas effet après le déploiement de Rails AWS
[Maven] Que faire si on vous demande d’incorporer dans la guerre un fichier jar qui n’est pas dans le référentiel distant
Sortez le journal d'accès Tomcat intégré à la sortie standard avec Spring Boot
De quoi avez-vous besoin à la fin pour créer une application Web en utilisant Java? Expliquer le mécanisme et ce qui est nécessaire pour apprendre
[Spring Boot] Que faire si une exception HttpMediaTypeNotAcceptableException se produit sur un point de terminaison pour lequel produit est défini
Que faire si l'application n'est pas créée avec la dernière version de Rails installée lorsque les rails sont neufs
[Ubuntu 20.04] Que faire si le moniteur externe n'est pas reconnu
Que faire lorsque Cloud 9 est plein dans le didacticiel Rails
Remarques sur la marche à suivre si le Jar de dépendance Eclipse Maven est incorrect
Que faire si l'image publiée par refile disparaît après avoir défini la page d'erreur 404 dans Rails
Que faire si l'opération non autorisée s'affiche lors de l'exécution d'une commande dans le terminal
Que faire si vous obtenez l'erreur Trop long sans sortie (dépassé 10m0s) dans CircleCI
Que faire si le processus Tomcat reste lorsque vous arrêtez Tomcat dans Eclipse
Étapes pour créer une application chameau simple avec les démarreurs Apache Camel Spring Boot
Après avoir utilisé Android Studio 3, les dépendances des applications Java ne peuvent plus être résolues, alors que faire à ce sujet
Qu'est-ce qu'un fichier .original Spring Boot?
Androd: Que faire à propos de "Le Royaume est déjà dans une transaction d'écriture dans"
[Solution] Que faire si vous obtenez une erreur Docker "ERREUR: Impossible de se connecter au démon Docker sous unix: ///var/run/docker.sock. Le démon docker est-il en cours d'exécution?"
Que faire si vous recevez l'avertissement «Le validateur d'unicité n'appliquera plus la comparaison sensible à la casse dans Rails 6.1.» Dans Rails 6.0
De la création d'un environnement cloud AWS au déploiement d'une application Spring Boot (pour les débutants)
Que faire si vous ne trouvez pas votre clé API après le déploiement sur Rails Heroku
Que faire si le point d'arrêt est grisé et ne s'arrête pas pendant le débogage