我正在尝试了解有关 Spring 框架的更多信息(不使用 SpringBoot),目前我已经有了一个带有嵌入式 Tomcat 服务器的基本
RestController
设置(版本 10
)。
一切正常,但我想自定义 Catalina 的
Log
对象,以便我可以执行诸如控制台日志的 JSON 序列化之类的操作。
我有 Nest.JS 背景,这非常简单。只需向 Nest 注册一个新的 Logger 即可。我希望我能做类似的事情。
显然,Catalina 在
java.util.logging
周围使用了一种包装器,它是 LogFactory
。当为此类创建单例时,它会验证是否有任何服务实现并使用 Log
接口。如果没有,那么它使用默认实现(这是我试图覆盖的)
private LogFactory() {
FileSystems.getDefault();
// Look via a ServiceLoader for a Log implementation that has a
// constructor taking the String name.
ServiceLoader<Log> logLoader = ServiceLoader.load(Log.class);
Constructor<? extends Log> m=null;
for (Log log: logLoader) {
Class<? extends Log> c=log.getClass();
try {
m=c.getConstructor(String.class);
break;
}
catch (NoSuchMethodException | SecurityException e) {
throw new Error(e);
}
}
discoveredLogConstructor=m;
}
public Log getInstance(String name) throws LogConfigurationException {
if (discoveredLogConstructor == null) {
return DirectJDKLog.getInstance(name);
}
try {
return discoveredLogConstructor.newInstance(name);
} catch (ReflectiveOperationException | IllegalArgumentException e) {
throw new LogConfigurationException(e);
}
}
我已经实现了这个 Log 接口(它只是一个测试类),但我不知道如何让这个
ServiceLoader
使用它。
class CatalinaLogger implements Log {
private Logger logger;
private String name;
public CatalinaLogger(String name) {
this.name = name;
this.logger = Logger.getLogger(name);
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public boolean isFatalEnabled() {
return true;
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public void trace(Object message) {
logger.log(Level.FINER, String.valueOf(message));
}
@Override
public void trace(Object message, Throwable t) {
logger.log(Level.FINER, String.valueOf(message));
}
@Override
public void debug(Object message) {
logger.log(Level.FINE, String.valueOf(message));
}
@Override
public void debug(Object message, Throwable t) {
logger.log(Level.FINE, String.valueOf(message));
}
@Override
public void info(Object message) {
logger.log(Level.INFO, String.valueOf(message));
}
@Override
public void info(Object message, Throwable t) {
logger.log(Level.INFO, String.valueOf(message));
}
@Override
public void warn(Object message) {
logger.log(Level.WARNING, String.valueOf(message));
}
@Override
public void warn(Object message, Throwable t) {
logger.log(Level.WARNING, String.valueOf(message));
}
@Override
public void error(Object message) {
logger.log(Level.SEVERE, String.valueOf(message));
}
@Override
public void error(Object message, Throwable t) {
logger.log(Level.SEVERE, String.valueOf(message));
}
@Override
public void fatal(Object message) {
logger.log(Level.SEVERE, String.valueOf(message));
}
@Override
public void fatal(Object message, Throwable t) {
logger.log(Level.SEVERE, String.valueOf(message));
}
}
我对这个问题的理解正确吗?不管怎样,我该如何解决这个问题?
我找到了一种方法来完成我正在尝试的事情。基本上,Catalina 的默认
Log
实现使用 java.util.logging
提供的相同记录器。这包括为此类所做的配置,例如格式化程序。
如果Catalina检测到该包有配置文件,它不会自动自定义该类,这让我们可以自己自定义它:
class DirectJDKLog implements Log {
// no reason to hide this - but good reasons to not hide
public final Logger logger;
// Alternate config reader and console format
private static final String SIMPLE_FMT="java.util.logging.SimpleFormatter";
private static final String FORMATTER="org.apache.juli.formatter";
static {
if (System.getProperty("java.util.logging.config.class") == null &&
System.getProperty("java.util.logging.config.file") == null) {
...default configurations
我无法使用自己的
Log
实现,但我可以使用 logging.properties
目录中的 ${CATALINA_HOME}
文件配置自定义格式化程序:
handlers=java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.ConsoleHandler.formatter=com.springtutorial.httpconfig.LoggerFormatter
然后,这是定制东西的问题:
package com.springtutorial.httpconfig;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class LoggerFormatter extends Formatter {
private ObjectMapper JSON = new ObjectMapper();
private static String kubernetesEnvironment = "false";
public LoggerFormatter() {
String kubeEnvEnabled = System.getProperty("com.springtutorial.httpconfig.kubernetesEnvironment");
if (kubeEnvEnabled != null) {
kubernetesEnvironment = kubeEnvEnabled;
}
}
@Override
public String format(LogRecord record) {
if (LoggerFormatter.kubernetesEnvironment == "false") {
return formatForDevelopment(record);
}
return formatForKubernetes(record);
}
public String formatForDevelopment(LogRecord record) {
// [{Level} | {DateTime} | {Class} -> {Method}] - {Message}
Date eventDate = new Date(record.getMillis());
String[] fullClassName = record.getSourceClassName().split("\\.");
String shortClassName = fullClassName[fullClassName.length - 1];
return "[" + record.getLevel() + " | " +
new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(eventDate) + " | " +
shortClassName + " -> " +
record.getSourceMethodName() + "] - " +
record.getMessage() + '\n';
}
public String formatForKubernetes(LogRecord record) {
// {level: $level$, date: $date$, sourceClass: $class$, sourceMethod: $method$, businessData: $businessDataObj$}
Date eventDate = new Date(record.getMillis());
String[] fullClassName = record.getSourceClassName().split("\\.");
String shortClassName = fullClassName[fullClassName.length - 1];
BusinessData<?> receivedBusinessData;
if (fullClassName[1].equals("springframework") || fullClassName[1].equals("apache")) {
// Server is logging so we must format the businessData object by hand
receivedBusinessData = new BusinessData<Object>(new Object() {
@JsonProperty
String serverInfo = record.getMessage();
});
} else {
// Message was logged by the application so i will assume it was made from the application i wrote
receivedBusinessData = (BusinessData<?>) record.getParameters()[0];
}
var log = new Object() {
@JsonProperty
String level = record.getLevel().toString();
@JsonProperty
String date = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(eventDate);
@JsonProperty
String sourceClass = shortClassName;
@JsonProperty
String sourceMethod = record.getSourceMethodName();
@JsonProperty
BusinessData<?> businessData = receivedBusinessData;
};
try {
return JSON.writeValueAsString(log) + '\n';
} catch (JsonProcessingException exp) {
return exp.getMessage();
}
}
}
仍有很大的改进空间(我很想听到一些诚实的反馈,因为这里的一些事情感觉像是在编写战争罪行),但按预期工作:
从服务器(Catalina、Tomcat、Coyote 等)进行的日志记录根据所选模式(开发或集群部署模式)进行格式化:
// Development
[INFO | 11/05/2024 21:11:12 | AbstractProtocol -> init] - Initializing ProtocolHandler ["http-nio-8080"]
[INFO | 11/05/2024 21:11:12 | StandardService -> startInternal] - Starting service [Tomcat]
[INFO | 11/05/2024 21:11:12 | StandardEngine -> startInternal] - Starting Servlet engine: [Apache Tomcat/10.1.23]
// Cluster deployed
{"level":"INFO","date":"11/05/2024 21:11:51","sourceClass":"AbstractProtocol","sourceMethod":"init","businessData":{"data":{"serverInfo":"Initializing ProtocolHandler [\"http-nio-8080\"]"}}}
{"level":"INFO","date":"11/05/2024 21:11:51","sourceClass":"StandardService","sourceMethod":"startInternal","businessData":{"data":{"serverInfo":"Starting service [Tomcat]"}}}
可以使用默认的
java.util.logging.Logger
实现从应用程序进行日志记录:
// This could be wrapped in the custom Log implementation maybe...?
Logger.getLogger("com.springtutorial").logp(Level.INFO, this.getClass().getPackageName(), "onStartup", "anymsg", new BusinessData<Object>(new Object(){
@JsonProperty
String myCustomObjectData = "this is some custom data";
}));
结果是一样的:
{"level":"INFO","date":"11/05/2024 21:15:29","sourceClass":"springtutorial","sourceMethod":"onStartup","businessData":{"data":{"myCustomObjectData":"this is some custom data"}}}