Undertow容器在Springboot中如何自定义修改文件名

Wesley13
• 阅读 796

背景

  • Springboot集成了众多容器(Tomcat、Jetty、Undertow)

  • Undertow是一款并发性能极高的容器,由于默认的容器是Tomcat,我们通常会把tomcat的jar包干掉并引入Undertow的jar包,由此开启Undertow容器

  • 项目需要记录AccessLog日志,来保存和查询接口调用情况

  • AccessLog日志文件默认会定时日志切割(每天凌晨,按照天维度拆分小文件),默认生成的文件名为:

    access_log.log access_log.2021-02-11.log

  • 默认AccessLog不会自动删除,时间久了可能导致硬盘空间不够

  • 公司有一款自动日志删除的功能代理服务(可以设置日志最大保留天数),但是日志文件名的格式需要设置统一标准。比如:必须符合".日期格式"(日期可以按照天和小时维度)如:

    access_log.log.2021-02-11

  • 由于access_log.2021-02-11.log不符合日志文件名标准,导致自动日志删除代理无法识别,日志会积压,只能手动去集群删除,比较耗费时间

  • 默认的Undertow无法修改和自定义文件名。虽然可以设置前缀、后缀,但是规则比较生硬、日期也无法调整在文件名中位置和日期格式、生成的日期结尾会自带"."开头不带"."、无法满足日志删除代理的匹配规则

    accesslog: dir: "logs" # 路径 enabled: true # 是否启用 pattern: 'common' # 一条条请求的匹配模式(可以匹配接口path,时间,响应码,ip等),用于生成请求日志内容 prefix: "access_log." # 前缀 suffix: "log" # 后缀

抓手

  • 为了解决AccessLog文件名不支持自定义的问题,需要从Undertow源码入手
  • 从源码找到生成日志文件名的地方,重写这部分的逻辑

解决过程

1.首先打开Undertow的源码包

发现server.handlers.accesslog下有相关的accesslog的处理的类

Undertow容器在Springboot中如何自定义修改文件名

2.接下来看接口

AccessLogReceiver接口有两个实现

DefaultAccessLogReceiver和JBossLoggingAccessLogReceiver

Undertow容器在Springboot中如何自定义修改文件名

package io.undertow.server.handlers.accesslog;

_/** _ * Interface that is used by the access log handler to send data to the log file manager. * * Implementations of this interface must be thread safe. * * @author Stuart Douglas */ public interface AccessLogReceiver {

void logMessage(final String message);

}

Undertow容器在Springboot中如何自定义修改文件名

看注释可以看到这个接口就是处理日志文件相关的,接下来进入到实现类一个一个看

_/** _ * Access log receiver that logs messages at INFO level. * * @author Stuart Douglas */ public class JBossLoggingAccessLogReceiver implements AccessLogReceiver {

public static final String _DEFAULT\_CATEGORY_ \= "io.undertow.accesslog";

private final Logger logger;

public JBossLoggingAccessLogReceiver(final String category) {
    this.logger \= Logger._getLogger_(category);
}

public JBossLoggingAccessLogReceiver() {
    this.logger \= Logger._getLogger_(_DEFAULT\_CATEGORY_);
}

@Override

public void logMessage(String message) { logger.info(message); } }

JBossLoggingAccessLogReceiver并没有对日志文件进行什么处理,只是单纯的进行了日志的打印,接下来看另外一个

Undertow容器在Springboot中如何自定义修改文件名

看DefaultAccessLogReceiver的类注释,大概就可以猜到是我们要找的地方了,接下来先找构造函数(变量的初始化的地方)

public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName) { this(logWriteExecutor, outputDirectory.toPath(), logBaseName, null); }

public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName, final String logNameSuffix) { this(logWriteExecutor, outputDirectory.toPath(), logBaseName, logNameSuffix, true); }

public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName, final String logNameSuffix, boolean rotate) { this(logWriteExecutor, outputDirectory.toPath(), logBaseName, logNameSuffix, rotate); }

public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName) { this(logWriteExecutor, outputDirectory, logBaseName, null); }

public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, final String logNameSuffix) { this(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, true); }

public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, final String logNameSuffix, boolean rotate) { this(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, rotate, null); }

private DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, final String logNameSuffix, boolean rotate, LogFileHeaderGenerator fileHeader) { this.logWriteExecutor = logWriteExecutor; this.outputDirectory = outputDirectory; this.logBaseName = logBaseName; this.rotate = rotate; this.fileHeaderGenerator = fileHeader; this.logNameSuffix = (logNameSuffix != null) ? logNameSuffix : DEFAULT_LOG_SUFFIX; this.pendingMessages = new ConcurrentLinkedDeque<>(); this.defaultLogFile = outputDirectory.resolve(logBaseName + this.logNameSuffix); calculateChangeOverPoint(); }

可以看到多个构造函数都调用了一个地方,在这个地方可以看到我们在配置文件中配置的前缀、后缀、路径等关键参数。calculateChangeOverPoint() 这个方法比较特别,我们继续往下看

private void calculateChangeOverPoint() { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.add(Calendar.DATE, 1); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.US); currentDateString = df.format(new Date()); _// 如果存在现有的默认日志文件,请使用上次修改的日期而不是当前日期 _ if (Files.exists(defaultLogFile)) { try { currentDateString = df.format(new Date(Files.getLastModifiedTime(defaultLogFile).toMillis())); } catch(IOException e){ _// 忽视。如果发生异常,请使用当前日期 _ } } changeOverPoint = calendar.getTimeInMillis(); }

可以看到这个类指定了时间的格式,只能是日期"yyyy-MM-dd"的模式并赋值currentDateString为当前时间或者同名文件(即access_log.log)的最后的修改时间。

并同时记录changeOverPoint为明天凌晨的毫秒数(如明天是2020-02-19 00:00:00),作为判断依据来判断当前时间是否已经第二天了。

观察发现这个类还继承了Runnable,实现了run()方法,可以知道AccessLog日志文件的写入默认是异步进行的

_/** _ * processes all queued log messages */ @Override public void run() { if (!stateUpdater.compareAndSet(this, 1, 2)) { return; } if (forceLogRotation) { doRotate(); } else if (initialRun && Files.exists(defaultLogFile)) { _// 如果有现有的日志文件,请检查是否应该切割 _ long lm = 0; try { lm = Files.getLastModifiedTime(defaultLogFile).toMillis(); } catch (IOException e) { UndertowLogger.ROOT_LOGGER.errorRotatingAccessLog(e); } Calendar c = Calendar.getInstance(); c.setTimeInMillis(changeOverPoint); c.add(Calendar.DATE, -1); if (lm <= c.getTimeInMillis()) { doRotate(); } } initialRun = false; List<String> messages = new ArrayList<>(); String msg; _// 一次最多只能抓取1000条消息 _ for (int i = 0; i < 1000; ++i) { msg = pendingMessages.poll(); if (msg == null) { break; } messages.add(msg); } try { if (!messages.isEmpty()) {            // 内容写入 writeMessage(messages); } } finally { // 忽略先不看 } }

我们可以先看writeMessage(messages);这个方法

private void writeMessage(final List<String> messages) { // 如果当前时间大于明天凌晨,说明发生了跨天,需要进行日志切割 if (System.currentTimeMillis() > changeOverPoint) { doRotate(); } try { if (writer == null) { boolean created = !Files.exists(defaultLogFile); writer = Files.newBufferedWriter(defaultLogFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE); if(Files.size(defaultLogFile) == 0 && fileHeaderGenerator != null) { String header = fileHeaderGenerator.generateHeader(); if(header != null) { writer.write(header); writer.newLine(); writer.flush(); } } } for (String message : messages) { writer.write(message); writer.newLine(); } writer.flush(); } catch (IOException e) { UndertowLogger.ROOT_LOGGER.errorWritingAccessLog(e); } }

这个方法调用了doRotate();进行日志的切割,我们接着看doRotate()方法

private void doRotate() { forceLogRotation = false; if (!rotate) { return; } try { if (writer != null) { writer.flush(); writer.close(); writer = null; } if (!Files.exists(defaultLogFile)) { return; }         // 找到了日志文件名的生成规则 (前缀+当前日期+"."+后缀) Path newFile = outputDirectory.resolve(logBaseName + currentDateString + "." + logNameSuffix); int count = 0;         // 如果新生成的文件已经存在,则进行命名变更(多加了个计数),防止文件覆盖更新 while (Files.exists(newFile)) { ++count; newFile = outputDirectory.resolve(logBaseName + currentDateString + "-" + count + "." + logNameSuffix); } Files.move(defaultLogFile, newFile); } catch (IOException e) { UndertowLogger.ROOT_LOGGER.errorRotatingAccessLog(e); } finally { calculateChangeOverPoint(); } }

可以看到newFile的文件名生成是写死了(实在是太坑了,太不灵活了)

Undertow容器在Springboot中如何自定义修改文件名

我们需要重写的地方就找到了,接下来得看如何重写这一块的逻辑

3.查找重写的链路

首先看下这个DefaultAccessLogReceiver对象是怎么来的,如果是spring自动装配的bean,那么我们只需要把这个bean想办法替换调就可以,如果是写死new出来的,那只能一层一层网上找,直到找到spring bean的创建的地方

接下来从构造函数出发,搜索对象生成的地方

Undertow容器在Springboot中如何自定义修改文件名

可以看到有两个地方

通过启动springboot,每个地方打个断点来看是否走到了这些地方来找到调用的流程(没错就是这么low),可以找到AccessLogHttpHandlerFactory这个类的getHandler方法生成了DefaultAccessLogReceiver对象

Undertow容器在Springboot中如何自定义修改文件名

Undertow容器在Springboot中如何自定义修改文件名

那只能继续往上层的调用来找(通过查询AccessLogHttpHandlerFactory的构造方法生成的地方)

Undertow容器在Springboot中如何自定义修改文件名

可以看到UndertowWebServerFactoryDelegate这个类生成了AccessLogHttpHandlerFactory对象

我们继续通过构造方法+断点来顺藤摸瓜找调用方

Undertow容器在Springboot中如何自定义修改文件名

竟然有这么多

Undertow容器在Springboot中如何自定义修改文件名

最后定位到UndertowServletWebServerFactory这个工厂类,竟然还是直接new的,并且还是私有的(人类是有极限了,我不做人了,JOJO!!!)

Undertow容器在Springboot中如何自定义修改文件名

继续找生成这个工厂的地方

Undertow容器在Springboot中如何自定义修改文件名

Undertow容器在Springboot中如何自定义修改文件名

最终终于找到了这个bean的创建的地方(只有能够到达那个地方(DIO))

可以看到这里还是有点良心的,设置了

@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)

如果我们没有默认提供ServletWebServerFactory则会走这里,换句话说,我们只要提供下自定义的ServletWebServerFactory Bean 就可以覆盖以上的逻辑了

4.重写文件名生成规则

找到bean创建的地方后,可以直接进行新bean的注册

@Configuration public class ServletWebServerFactoryConfig {

@Bean public CustomUndertowServletWebServerFactory customUndertowServletWebServerFactory( ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers, ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) { CustomUndertowServletWebServerFactory factory = new CustomUndertowServletWebServerFactory(); factory.getDeploymentInfoCustomizers() .addAll(deploymentInfoCustomizers.orderedStream().collect(Collectors.toList())); factory.getBuilderCustomizers() .addAll(builderCustomizers.orderedStream().collect(Collectors.toList())); return factory; }

}

针对UndertowWebServerFactoryDelegate的修改,需要结合反射进行,生成我们自定义的CustomAccessLogHttpHandlerFactory

public class CustomUndertowServletWebServerFactory extends UndertowServletWebServerFactory {

@Override protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) { Object delegate = ReflectUtil.getFieldValue(this, "delegate"); List<HttpHandlerFactory> httpHandlerFactories = createHttpHandlerFactories(delegate, this, new CustomDeploymentManagerHttpHandlerFactory(manager)); return new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0); }

List<HttpHandlerFactory> createHttpHandlerFactories(Object delegate, AbstractConfigurableWebServerFactory webServerFactory, HttpHandlerFactory... initialHttpHandlerFactories) { boolean useForwardHeaders = (Boolean) ReflectUtil.getFieldValue(delegate, "useForwardHeaders"); File accessLogDirectory = (File) ReflectUtil.getFieldValue(delegate, "accessLogDirectory"); String accessLogPattern = (String) ReflectUtil.getFieldValue(delegate, "accessLogPattern"); String accessLogPrefix = (String) ReflectUtil.getFieldValue(delegate, "accessLogPrefix"); String accessLogSuffix = (String) ReflectUtil.getFieldValue(delegate, "accessLogSuffix"); Boolean accessLogRotate = (Boolean) ReflectUtil.getFieldValue(delegate, "accessLogRotate");

List<HttpHandlerFactory\> factories \= _createHttpHandlerFactories_(
    webServerFactory.getCompression(),
    useForwardHeaders, webServerFactory.getServerHeader(), webServerFactory.getShutdown(),
    initialHttpHandlerFactories);
if (isAccessLogEnabled()) {
  factories

.add(new CustomAccessLogHttpHandlerFactory(accessLogDirectory, accessLogPattern, accessLogPrefix, accessLogSuffix, accessLogRotate)); } return factories; }

static List<HttpHandlerFactory> createHttpHandlerFactories(Compression compression, boolean useForwardHeaders, String serverHeader, Shutdown shutdown, HttpHandlerFactory... initialHttpHandlerFactories) { List<HttpHandlerFactory> factories = new ArrayList<>( Arrays.asList(initialHttpHandlerFactories)); if (compression != null && compression.getEnabled()) { factories.add(new CustomCompressionHttpHandlerFactory(compression)); } if (useForwardHeaders) { factories.add(Handlers::proxyPeerAddress); } if (StringUtils.hasText(serverHeader)) { factories.add((next) -> Handlers.header(next, "Server", serverHeader)); } if (shutdown == Shutdown.GRACEFUL) { factories.add(Handlers::gracefulShutdown); } return factories; } }

在CustomAccessLogHttpHandlerFactory中进行修改,改用我们自定义的CustomDefaultAccessLogReceiver

Undertow容器在Springboot中如何自定义修改文件名

通过新建的类CustomDefaultAccessLogReceiver(这个类其实就是DefaultAccessLogReceiver的源码复制过来,之后重新修改了下doRatate方法中的文件生成规则),重写doRatate方法,进而改变文件命名规则

Undertow容器在Springboot中如何自定义修改文件名

类似其他需要的类也需要一并复制过来

Undertow容器在Springboot中如何自定义修改文件名

总结

  • 本次项目编写中遇到了实际的问题并结合源码一步一步的进行了分析。
  • 通过构造函数和断点分析法,找到了调用链路。
  • 通过对上层链路Bean以及部分源码的复制及替换,实现了整体功能的切换(万事万物皆对象)。
  • 通过这次的源码的分析的分享,希望可以提供一个解决问题的思路。
  • 如果有帮助到大家,请一键三联,多多关注我,后续提供更多的分享。
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
SpringBoot使用Undertow
Undertow是一个Java开发的灵活的高性能Web服务器,提供包括阻塞和基于NIO的非阻塞机制。Undertow是红帽公司的开源产品,是Wildfly默认的Web服务器。SpringBoot2中可以将Web服务器切换到Undertow来提高应用性能。Undertow官网地址(https://www.oschina.net/action/GoToL
Stella981 Stella981
3年前
SpringBoot使用Undertow
Undertow是一个Java开发的灵活的高性能Web服务器,提供包括阻塞和基于NIO的非阻塞机制。Undertow是红帽公司的开源产品,是Wildfly默认的Web服务器。SpringBoot2中可以将Web服务器切换到Undertow来提高应用性能。Undertow官网地址(https://www.oschina.net/action/GoToL
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这