Maison  >  Article  >  Java  >  Une introduction détaillée sur la façon d'utiliser les journaux Java pour écrire de la poésie

Une introduction détaillée sur la façon d'utiliser les journaux Java pour écrire de la poésie

黄舟
黄舟original
2017-03-25 10:53:281627parcourir

Si vous voulez bien faire votre travail, vous devez d'abord affiner vos outils

De nombreux programmeurs ont peut-être oublié à quel point il est important d'enregistrer le comportement de l'application en cas de bugs de concurrence. sont causés par une pression élevée dans un environnement de thread, vous pouvez réaliser l'importance de l'enregistrement des journaux.

Certaines personnes sont heureuses d'ajouter cette phrase au code :

log.info("Happy and carefree logging");

Il ne réalise peut-être pas que le journal des applications est important dans la maintenance, le réglage et l'identification des défauts.

Je pense que slf4j est la meilleure API de journalisation, principalement parce qu'elle prend en charge un excellent moyen d'injection de modèles :

log.debug("Found {} records matching filter: '{}'", records, filter);

log4j Pour , vous ne pouvez faire que ceci :

log.debug("Found " + records + " recordsmatching filter: '" + filter + "'");

Écrire de cette façon est non seulement plus verbeux et moins lisible, mais aussi l'épissage de chaîne affecte l'efficacité (lorsque la sortie n'est pas requise à ce niveau).

slf4j introduit la fonctionnalité d'injection {}, et comme l'épissage de chaînes est évité à chaque fois, la méthode toString ne sera pas appelée et isDebugEnabled n'est plus nécessaire.

slf4j est une application de mode apparence, ce n'est qu'une façade. Pour une implémentation spécifique, je recommande le framework de logback, qui a déjà été annoncé une fois, au lieu du log4j déjà complet. Il possède de nombreuses fonctionnalités intéressantes et, contrairement à log4j, il est toujours en cours de développement et d’amélioration.

Un autre outil à recommander est perf4j :

Perf4J est à System.currentTimeMillis() comme log4j est à System.out.println()

Tout comme log4j est un meilleur remplacement pour System.out.println, perf4j ressemble plus à un remplacement pour System.currentTimeMillis().

J'ai introduit perf4j dans un projet et observé ses performances sous charge élevée. Les administrateurs et les utilisateurs professionnels sont stupéfaits par les magnifiques graphiques fournis par ce gadget.

Nous pouvons examiner les problèmes de performances à tout moment. Il devrait y avoir un article dédié à perf4j. Pour l'instant, vous pouvez d'abord lire son guide du développeur.

Ceki Gülcü (créateur des projets log4j, slf4j et logback) nous fournit également une méthode simple pour supprimer la dépendance à la journalisation commune.

N'oubliez pas le niveau de journalisation

Chaque fois que vous souhaitez ajouter une ligne de journalisation, vous vous demanderez quel niveau de journalisation doit être utilisé ici ? Environ 90 % des programmeurs ne prêtent pas beaucoup d'attention à ce problème. Ils utilisent tous un seul niveau pour enregistrer les journaux, généralement INFO ou DEBUG. Pourquoi?

Le framework de journalisation présente deux avantages majeurs par rapport à System.out : la classification et le niveau. Les deux vous permettent de filtrer sélectivement les journaux, de manière permanente ou uniquement lors du dépannage.

  • ERREUR Une erreur grave s'est produite et doit être traitée immédiatement. Ce niveau d’erreur ne peut être toléré par aucun système. Par exemple : exception de pointeur nul, la base de données n'est pas disponible et les cas d'utilisation sur le chemin critique ne peuvent pas continuer à s'exécuter.

  • WARN continuera à exécuter le processus suivant, mais il doit être pris au sérieux. En fait, j'espère qu'il y a deux niveaux ici : l'un est le problème évident avec une solution (par exemple, "Les données actuelles ne sont pas disponibles, utilisez données en cache"), et l'autre est les problèmes potentiels et suggestions (telles que « Le programme s'exécute en mode développement » ou « Le mot de passe de la console de gestion n'est pas suffisamment sécurisé »). Les applications peuvent tolérer ces messages, mais ils doivent être vérifiés et réparés.

  • DEBUG est un sujet de préoccupation pour les développeurs. Je parlerai plus tard du genre de choses qui devraient être enregistrées à ce niveau.

  • TRACE Des informations plus détaillées ne sont utilisées que pendant la phase de développement. Vous devrez peut-être encore prêter attention à ces informations pendant une courte période après le lancement du produit, mais ces enregistrements de journaux ne sont que temporaires et doivent éventuellement être désactivés. La différence entre DEBUG et TRACE est difficile à distinguer, mais si vous ajoutez une ligne de journal puis la supprimez après développement et test, le journal doit être de niveau TRACE.

La liste ci-dessus n'est qu'une suggestion. Vous pouvez vous connecter selon vos propres règles, mais il est préférable d'avoir certaines règles. Mon expérience personnelle est la suivante : ne filtrez pas les journaux au niveau du code, mais utilisez le bon niveau de journalisation pour filtrer rapidement les informations souhaitées, ce qui peut vous faire gagner beaucoup de temps.

La dernière chose dont je veux parler est la fameuse instruction conditionnelle is*Enabled. Certaines personnes aiment ajouter ceci avant chaque journal :

if(log.isDebugEnabled())
    log.debug("Place for your commercial");

Personnellement, je pense que vous devriez éviter d'ajouter cette chose compliquée au code. Il ne semble y avoir aucune amélioration des performances (surtout après avoir utilisé slf4j), plutôt une optimisation prématurée. Aussi, ne réalisez-vous pas que c’est un peu redondant ? Il est rare que de telles déclarations de jugement aussi explicites soient clairement nécessaires, à moins que nous ne prouvions que la construction du message de journal lui-même est trop coûteuse. Sinon, souvenez-vous simplement de ce que vous voulez et laissez le framework de journalisation s'en occuper.

Savez-vous ce que vous enregistrez ?

Chaque fois que vous écrivez une ligne de journal, prenez un moment pour voir exactement ce que vous imprimez dans le fichier journal. Lisez vos journaux et recherchez toute anomalie. Tout d’abord, évitez au moins les exceptions de pointeur nul :

log.debug("Processing request with id: {}", request.getId());

你确认过request不是null了吗?

记录集合也是一个大坑。如果你用Hibernate从数据库里获取领域对象的集合的时候,不小心写成了这样:

log.debug("Returning users: {}", users);

slf4j只会在这条语句确实会打印的时候调用toString方法,当然这个很酷。不过如果内存溢出了,N+1选择问题,线程饿死,延迟初始化异常,日志存储空间用完了…这些都有可能发生。

最好的方式是只记录对象的ID(或者只记录集合的大小)。不过收集ID需要对每个对象调用getId方法,这个在Java里可真不是件简单的事。Groovy有个很棒的展开操作符(users*.id),在Java里我们可以用Commons Beanutils库来模拟下:

log.debug("Returning user ids: {}", collect(users, "id"));

collect方法大概是这么实现的:

public static Collection collect(Collection collection, String propertyName) {
    return CollectionUtils.collect(collection, new BeanToPropertyValueTransformer(propertyName));
}

最后要说的是,toString方法可能没有正确的实现或者使用。

首先,为了记录日志,为每个类创建一个toString的做法比比皆是,最好用 ToStringBuilder来生成(不过不是它的反射实现的那个版本)。

第二,注意数组和非典型的集合。数组和一些另类的集合的toString实现可能没有挨个调用每个元素的toString方法。可以使用JDK提供的Arrays#deepToString方法。经常检查一下你自己打印的日志,看有没有格式异常的一些信息。

避免副作用

日志打印一般对程序的性能没有太大影响。最近我一个朋友在一些特殊的平台上运行的一个系统抛出了Hibernate的LazyInitializationException异常。你可能从这已经猜到了,当会话连接进来的时候,一些日志打印导致延迟初始化的集合被加载。在这种情况下,把日志级别提高了,集合也就不再被初始化了。如果你不知道这些上下文信息,你得花多长时间来发现这个BUG。

另一个副作用就是影响程序的运行速度。快速回答一下这个问题:如果日志打印的过多或者没有正确的使用toString和字符串拼接,日志打印就会对性能产生负面影响。能有多大?好吧,我曾经见过一个程序每15分钟就重启一次,因为太多的日志导致的线程饿死。这就是副作用!从我的经验来看,一小时打印百来兆差不多就是上限了。

当然如果由于日志打印异常导致的业务进程中止,这个副作用就大了。我经常见到有人为了避免这个而这么写:

try {
    log.trace("Id=" + request.getUser().getId() + " accesses " + manager.getPage().getUrl().toString())
} catch(NullPointerException e) {}

这是段真实的代码,但是为了让世界清净点,请不要这么写。

描述要清晰

每个日志记录都会包含数据和描述。看下这个例子:

log.debug("Message processed");
log.debug(message.getJMSMessageID());
log.debug("Message with id '{}' processed", message.getJMSMessageID());

当在一个陌生的系统里排查错误的时候,你更希望看到哪种日志?相信我,上面这些例子都很常见。还有一个反面模式:

if(message instanceof TextMessage)
    //...
else
    log.warn("Unknown message type");

在这个警告日志里加上消息类型,消息ID等等这些难道很困难吗?我是知道发生错误了,不过到底是什么错误?上下文信息是什么?

第三个反面例子是“魔法日志”。一个真实的例子:团队里的很多程序员都知道,3个&号后面跟着!号再跟着一个#号,再跟着一个伪随机数的日志意味着”ID为XYZ的消息收到了”。没人愿意改这个日志,某人敲下键盘,选中某个唯一的”&&&!#”字符串,他就能很快找到想要的信息。

结果是,整个日志文件看起来像一大串随机字符。有人不禁会怀疑这是不是一个perl程序。

日志文件应当是可读性强的,清晰的,自描述的。不要用一些魔数,记录值,数字,ID还有它们的上下文。记录处理的数据以及它的含义。记录程序正在干些什么。好的日志应该是程序代码的一份好的文档。

我有提过不要打印密码还有个人信息吗?相信没有这么傻的程序员。

调整你的格式

日志格式是个很有用的工具,无形中在日志添加了很有价值的上下文信息。不过你应该想清楚,在你的格式中包含什么样的信息。比如说,在每小时循环写入的日志中记录日期是没有意义的,因为你的日志名就已经包含了这个信息。相反的,如果你没记录线程名的话当两个线程并行的工作的时候,你就无法通过日志跟踪线程了——日志已经重叠到一起了。在单线程的应用程序中,这样做没问题,不过那个已经是过去的事儿了。

从我的经验来看,理想的日志格式应当包括(当然除了日志信息本身了):当前时间(无日期,毫秒级精度),日志级别,线程名,简单的日志名称(不用全称)还有消息。在logback里会是这样的:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern>
    </encoder>
</appender>

文件名,类名,行号,都不用列进来,尽管它们看起来很有用。我还在代码里见过空的日志记录:

log.info(“”);
因为程序员认为行号会作为日志格式的一部分,并且他知道如果空日志消息出现在这个文件的67行的话,意味着这个用户是认证过的。不仅这样,记录类名方法名,或者行号对性能都有很大的影响。

日志框架的一个比较高级的特性是诊断上下文映射(Mapped Diagnostic Context)。MDC只是一个线程本地的一个map。你可以把任何键值对放到这个map里,这样的话这个线程的所有日志记录都能从这个map里取到相应的信息作为输出格式的一部分。

记录方法的参数和返回值

如果你在开发阶段发现了一个BUG,你通常会用调试器来跟踪具体的原因。现在假设不让你用调试器了,比如,因为这个BUG几天前在用户的环境里出现了,你能拿到的只有一些日志。你能从中发现些什么?

如果你遵循打印每个方法的入参和出参这个简单的原则,你根本不需要调试器。当然每个方法可能访问外部系统,阻塞,等待,等等,这些都应该考虑进来。就参考以下这个格式就好:

public String printDocument(Document doc, Mode mode) {
    log.debug("Entering printDocument(doc={}, mode={})", doc, mode);
    String id = ...; //Lengthy printing operation
    log.debug("Leaving printDocument(): {}", id);
    return id;
}

由于你在方法的开始和结束都记录了日志,所以你可以人工找出效率不高的代码,甚至还可以检测到可能会引起死锁和饥饿的诱因——你只需看一下“Entering”后面是不是没有”Leaving“就明白了。如果你的方法名的含义很清晰,清日志将是一件愉快的事情。同样的,分析异常也更得更简单了,因为你知道每一步都在干些什么。代码里要记录的方法很多的话,可以用AOP切面来完成。这样减少了重复的代码,不过使用它得特别小心,不注意的话可能会导致输出大量的日志。

这种日志最合适的级别就是DEBUG和TRACE了。如果你发现某个方法调用 的太频繁,记录它的日志可能会影响性能的话,只需要调低它的日志级别就可以了,或者把日志直接删了(或者整个方法调用只留一个?)不过日志多了总比少了要强。把日志记录当成单元测试来看,你的代码应该布满了日志就像它的单元测试到处都是一样。系统没有任何一部分是完全不需要日志的。记住,有时候要知道你的系统是不是正常工作,你只能查看不断刷屏的日志。

观察外部系统

这条建议和前面的有些不同:如果你和一个外部系统通信的话,记得记录下你的系统传出和读入的数据。系统集成是一件苦差事,而诊断两个应用间的问题(想像下不同的公司,环境,技术团队)尤其困难。最近我们发现记录完整的消息内容,包括Apache CXF的SOAP和HTTP头,在系统的集成和测试阶段非常有效。

这样做开销很大,如果影响到了性能的话,你只能把日志关了。不过这样你的系统可能跑的很快,挂的也很快,你还无能为力?当和外部系统进行集成的时候,你只能格外小心并做好牺牲一定开销的准备。如果你运气够好,系统集成由ESB处理了,那在总线把请求和响应给记录下来就最好不过了。可以参考下Mule的这个日志组件。

有时候和外部系统交换的数据量决定了你不可能什么都记下来。另一方面,在测试阶段和发布初期,最好把所有东西都记到日志里,做好牺牲性能的准备。可以通过调整日志级别来完成这个。看下下面这个小技巧:

Collection<Integer> requestIds = //...
if(log.isDebugEnabled())
    log.debug("Processing ids: {}", requestIds);
else
    log.info("Processing ids size: {}", requestIds.size());

如果这个logger是配置成DEBUG级别,它会打印完整的请求ID的集合。如果它配置成了打印INFO信息的话,就只会输出集合的大小。你可能会问我是不是忘了isInfoEnabled条件了,看下第二点建议吧。这里还有一个值得注意的是ID的集合不能为null。尽管在DEBUG下,它为NULL也能正常打印,但是当配置成INFO的时候一个大大的空指针。还记得第4点建议中提到的副作用吧?

正确的记录异常

首先,不要记录异常,让框架或者容器来干这个。当然有一个例外:如果你从远程服务中抛出了异常(RMI,EJB等),异常会被序列化,确保它们能返回给客户端 (API中的一部分)。不然的话,客户端会收到NoClassDefFoundError或者别的古怪的异常,而不是真正的错误信息

异常记录是日志记录的最重要的职责之一,不过很多程序员都倾向于把记录日志当作处理异常的方式。他们通常只是返回默认值(一般是null,0或者空字符串),装作什么也没发生一样。还有的时候,他们会先记录异常,然后把异常包装了下再抛出去:

log.error("IO exception", e);
throw new CustomException(e);

这样写通常会把栈信息打印两次,因为捕获了MyCustomException异常的地方也会再打印一次。日志记录,或者包装后再抛出去,不要同时使用,否则你的日志看起来会让人很迷惑。

如果我们真的想记录日志呢?由于某些原因(大概是不读API和文档?),大约有一半的日志记录我认为是错误的。做个小测试,下面哪个日志语句能够正确的打印空指针异常?

try {
    Integer x = null;
    ++x;
} catch (Exception e) {
    log.error(e); //A
    log.error(e, e); //B
    log.error("" + e); //C
    log.error(e.toString()); //D
    log.error(e.getMessage()); //E
    log.error(null, e); //F
    log.error("", e); //G
    log.error("{}", e); //H
    log.error("{}", e.getMessage()); //I
    log.error("Error reading configuration file: " + e); //J
    log.error("Error reading configuration file: " + e.getMessage()); //K
    log.error("Error reading configuration file", e); //L
}

很奇怪吧,只有G和L(这个更好)是对的!A和B在slf4j下面根本就编译不过,其它的会把栈跟踪信息给丢掉了或者打印了不正确的信息。比如,E什么也不打印,因为空指针异常本身没有提供任何异常信息而栈信息又没打印出来 .记住,第一个参数通常都是文本信息,关于这个错误本身的。不要把异常信息给写进来,打印日志后它会自动出来的,在栈信息的前面。不过想要打印这个,你当然还得把异常传到第二个参数里面才行。

日志应当可读性强且易于解析

现在有两组用户对你的日志感兴趣:我们人类(不管你同不同意,码农也是在这里边),还有计算机(通常就是系统管理员写的shell脚本)。日志应当适合这两种用户来理解。如果有人在你后边看你的程序的日志却看到了这个:

那你肯定没听从我的建议。日志应该像代码一样易于阅读和理解。

另一方面,如果你的程序每小时就生成了半GB的日志,没有谁或者任何图形化的文本编辑器能把它们看完。这时候我们的老家伙们,grep,sed和awk这些上场的时候就来了。如果有可能的话,你记录的日志最好能让人和计算机都能看明白 ,不要将数字格式化,用一些能让正则容易匹配的格式等等。如果不可能的,用两个格式来打印数据:

log.debug("Request TTL set to: {} ({})", new Date(ttl), ttl);
// Request TTL set to: Wed Apr 28 20:14:12 CEST 2010 (1272478452437)
final String duration = DurationFormatUtils.formatDurationWords(durationMillis, true, true);
log.info("Importing took: {}ms ({})", durationMillis, duration);
//Importing took: 123456789ms (1 day 10 hours 17 minutes 36 seconds)

计算机看到”ms after 1970 epoch“这样的的时间格式会感谢你的,而人们则乐于看到”1天10小时17分36秒“这样的东西。

总之,日志也可以写得像诗一样优雅,如果你愿意琢磨的话。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn