6.appender

news2025/2/14 5:34:40

文章目录

  • 一、前言
  • 二、源码解析
    • Appender
    • UnsynchronizedAppenderBase
    • OutputStreamAppender
    • ConsoleAppender
    • FileAppender
    • RollingFileAppender
    • FileNamePattern
  • 三、总结

一、前言

前一篇文章介绍了appender、conversionRule、root和logger节点的解析, 为的是为本篇详细介绍它们的原理做铺垫, 日志打印也是主要围绕这几个对象开展的

二、源码解析

Appender

在这里插入图片描述

在 Logback 框架中,Appender 是用来将日志事件输出到目标(如文件、控制台、数据库等)的组件。而 UnsynchronizedAppenderBase 和 AppenderBase 是两种核心的抽象类,提供了实现日志输出的基础功能。

下面是UnsynchronizedAppenderBase 和 AppenderBase 的对比; 我们常用的ConsoleAppender和RollingFileAppender都是UnsynchronizedAppenderBase 的子类

特性UnsynchronizedAppenderBaseAppenderBase
线程安全性不线程安全线程安全
同步机制无需同步(开发者需手动处理)内置同步机制
性能性能更高,因为没有同步开销性能稍低,因为引入了同步逻辑
适用场景单线程或已外部同步的高性能需求场景多线程环境下无需手动同步的场景

appender接口定义

实现了FilterAttachable接口哦

public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {

    /**
     * 设置名称
     */
    void setName(String name);
    
    /**
     * 获取appender的名称
     */
    String getName();

    /**
     * 添加日志
     */
    void doAppend(E event) throws LogbackException;
}

public interface FilterAttachable<E> {
    /**
     * 添加过滤器
     */
    void addFilter(Filter<E> newFilter);
	/*
	 * 清空过滤器
	 */
    void clearAllFilters();

    /*
     * 获取复制所有的过滤器
     */
    List<Filter<E>> getCopyOfAttachedFiltersList();

    /*
     * 循环遍历链中的过滤器。一旦过滤器决定ACCEPT或DENY,则返回该值。如果所有过滤器都返回NEUTRAL,则返回NEUTRAL。
     */
    FilterReply getFilterChainDecision(E event);
}

接口比较简单, 核心方法就是这个doAppend, 用于添加我们的日志。

appender继承了FilterAttachable接口, 添加了对过滤器的支持, 允许我们根据过滤器判断是否需要打印日志

这里可以看出appender是支持filter标签的(因为addFilter方法)

UnsynchronizedAppenderBase

非同步Appender

abstract public class UnsynchronizedAppenderBase<E> extends ContextAwareBase implements Appender<E> {
    /**
     * 用来阻止当前线程递归调用doAppend方法
     */
    private ThreadLocal<Boolean> guard = new ThreadLocal<Boolean>();
    
    /**
     * 它是FilterAttachable接口的实现类, 使用静态代理模式
     */
    private FilterAttachableImpl<E> fai = new FilterAttachableImpl<E>();
    
    /** 模板方法 */
    public void doAppend(E eventObject) {
        // 阻止当前线程递归调用doAppend方法
        if (Boolean.TRUE.equals(guard.get())) {
            return;
        }
        try {
            // 设置当前线程已经进来的标识
            guard.set(Boolean.TRUE);
            // appender还未启动
            if (!this.started) {
                // 还未到允许重试此时, 输出警告信息
                if (statusRepeatCount++ < ALLOWED_REPEATS) {
                    addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
                }
                return;
            }
            // 过滤器处理, 如果返回DENY则不处理
            if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
                return;
            }
            // 子类的append方法, 去添加日志吧
            this.append(eventObject);

        } catch (Exception e) {
            // 异常次数不达最大重试次数, 输出异常信息
            if (exceptionCount++ < ALLOWED_REPEATS) {
                addError("Appender [" + name + "] failed to append.", e);
            }
        } finally {
            // 释放当前线程的标识
            guard.set(Boolean.FALSE);
        }
    }
    /** 需子类实现 */
    abstract protected void append(E eventObject);
    
    /** 下面这四个方法都是静态代理的体现 */
    public void addFilter(Filter<E> newFilter) {
        // 添加过滤器到代理对象FilterAttachableImpl中
        fai.addFilter(newFilter);
    }
    public void clearAllFilters() {
        fai.clearAllFilters();
    }
    public List<Filter<E>> getCopyOfAttachedFiltersList() {
        return fai.getCopyOfAttachedFiltersList();
    }
    public FilterReply getFilterChainDecision(E event) {
        return fai.getFilterChainDecision(event);
    }
}

方法小结

  1. 使用静态代理对象FilterAttachableImpl实现对FilterAttachable接口的实现, UnsynchronizedAppenderBase中实现的FilterAttachable接口中的方法都是由静态代理对象FilterAttachableImpl处理
  2. 模板方法doAppend用来处理公共逻辑
  3. 一个线程一次只能打印一条日志, 为了避免子类appender中递归doAppend方法, 所以这里使用ThreadLocal做一个拦截校验
  4. 使用过滤器先对日志事件做一次拦截, 如果拦截器返回了FilterReply.DENY, 该日志将会被丢弃
  5. 最后调用appender方法交给子类实现具体的日志打印逻辑

这里读者可以去了解下静态代理、动态代理、正向代理、方向代理的区别, 以及门面模式、包装模式和静态代理的区别; 看到源码里面出现奇怪的设计, 请不要慌, 肯定是有章法的, 要有一颗好奇的心。

我们看一下FilterAttachableImpl类的getFilterChainDecision方法

FilterAttachableImpl

public FilterReply getFilterChainDecision(E event) {

    final Filter<E>[] filterArrray = filterList.asTypedArray();
    final int len = filterArrray.length;
	// 遍历过滤器
    for (int i = 0; i < len; i++) {
        // 过滤器处理之后返回FilterReply
        final FilterReply r = filterArrray[i].decide(event);
        // 只要是返回DENY或者ACCEPT类型, 就直接返回
        if (r == FilterReply.DENY || r == FilterReply.ACCEPT) {
            return r;
        }
    }

    // no decision
    return FilterReply.NEUTRAL;
}

这里看到过滤器只要返回FilterReply.DENYFilterReply.ACCEPT就直接返回了, FilterReply.DENY代表了肯定拒绝, FilterReply.ACCEPT代表了肯定通过, 只有FilterReply.NEUTRAL属于模糊状态, 需要继续走下去。就像你去追一个人, 对方要是说yes那就成了, 对方说no那就拜拜, 对方要是说or, 那就完犊子了, 要打持久战了。

OutputStreamAppender

OutputStreamAppender 是一个基础组件,它负责将日志事件写入输出流,并提供了一些通用功能,比如流管理和布局支持。由于它的设计简单且功能集中,其他复杂的 Appender(如文件和滚动日志输出)都可以基于它构建

public class OutputStreamAppender<E> extends UnsynchronizedAppenderBase<E> {
    
    /**
     * encoder最终负责将事件写入OutputStream。
     */
    // getter/setter
    protected Encoder<E> encoder;
    
    /** 数据写入的目的地 */
    private OutputStream outputStream;
    
    /** 是否立即刷新数据 */
    boolean immediateFlush = true;
    
    public void start() {
        int errors = 0;
        // ... encoder 和 outputStream不能为空
        if (this.encoder == null) {
            addStatus(new ErrorStatus("No encoder set for the appender named \"" + name + "\".", this));
            errors++;
        }
        // ... 省略部分代码
        // only error free appenders should be activated
        if (errors == 0) {
            // 标记为启动
            super.start();
            // 添加初始化数据, 每次loggerContext启动的时候(一般也是项目启动), 可以记录日志
            encoderInit();
        }
    }
    
    void encoderInit() {
        if (encoder != null && this.outputStream != null) {
            try {
                byte[] header = encoder.headerBytes();
                writeBytes(header);
            } catch (IOException ioe) {
                this.started = false;
                // ... 
            }
        }
    }
    /** 核心方法, 实现父类的抽象方法 */
    protected void subAppend(E event) {
        // 未启动不处理
        if (!isStarted()) {
            return;
        }
        try {
            // loggingEvent默认是DeferredProcessingAware
            if (event instanceof DeferredProcessingAware) {
                // 1.预处理消息; 包括填充消息中的占位符, 将mdc数据初始化到loggingEvent中
                ((DeferredProcessingAware) event).prepareForDeferredProcessing();
            }
            // 2.写出数据
            writeOut(event);

        } catch (IOException ioe) {
            // 标记为启动失败
            this.started = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
        }
    }
    
    protected void writeOut(E event) throws IOException {
        // 编码数据
        byte[] byteArray = this.encoder.encode(event);
        // 写出数据
        writeBytes(byteArray);
    }
    
    private void writeBytes(byte[] byteArray) throws IOException {
        if (byteArray == null || byteArray.length == 0)
            return;
		// 这里是加非公平锁
        streamWriteLock.lock();

        try {
            // 得启动成功后才能写数据
            if (isStarted()) {
                // 写出数据
                writeByteArrayToOutputStreamWithPossibleFlush(byteArray);
                // 更新写出数据量
                updateByteCount(byteArray);
            }
        } finally {
            streamWriteLock.unlock();
        }
    }
    
     protected final void writeByteArrayToOutputStreamWithPossibleFlush(byte[] byteArray) throws IOException {
         // 写出数据
        this.outputStream.write(byteArray);
        // 如果立即刷新(默认是true)
        if (immediateFlush) {
            // 数据刷出到目的地
            this.outputStream.flush();
        }
    }
    
    /** 设置layout */
    public void setLayout(Layout<E> layout) {
        addWarn("This appender no longer admits a layout as a sub-component, set an encoder instead.");
        addWarn("To ensure compatibility, wrapping your layout in LayoutWrappingEncoder.");
        addWarn("See also " + CODES_URL + "#layoutInsteadOfEncoder for details");
        // layout默认使用LayoutWrappingEncoder覆盖已有的encoder
        LayoutWrappingEncoder<E> lwe = new LayoutWrappingEncoder<E>();
        lwe.setLayout(layout);
        lwe.setContext(context);
        this.encoder = lwe;
    }
}

由于OutputStreamAppender类比较简单, 这里就不一个个方法详细看了

方法小结

  1. 如果配置了OutputStreamAppender类型的appender, 在logback框架启动的时候, 可以设置encoder的headerBytes参数, 打印在运行的开始
  2. OutputStreamAppender类实现了UnsynchronizedAppenderBase的subAppend方法
  3. 先处理日志信息, 例如填充日志的占位符(例如: log.info(“你好{}”, “uncelqiao”)), 这里会转换成"你好uncelqiao", 还会把mdc上下文放到日志事件LoggingEvent中
  4. 写出数据到outputStream中, 这里有一个immediateFlush的boolean字段, 用于控制是否立刻将内容刷出, 默认是true
  5. 更新写入的字节数, 可以用来记录总写入数,然后切割文件
  6. setLayout方法覆盖了encoder属性, 也就是说你先设置了encoder再设置layout标签的话, 前一个encoder会失效, 一般我们不这么用

这里可以看出OutputStreamAppender类型的appender是支持添加encoderOutputStreamimmediateFlushlayout标签的(因为对应的setter方法)

ConsoleAppender

用来将日志打到控制台的

public class ConsoleAppender<E> extends OutputStreamAppender<E> {
    /**
     * 日志除数目的地; 默认是System.out
     */
    protected ConsoleTarget target = ConsoleTarget.SystemOut;
    /**
     * 是否使用jansi框架打印日志
     */
    protected boolean withJansi = false;

    @Override
    public void start() {
        // 这里提醒我们打印到控制台的速度是很慢的, 应该避免在生产环境开启打印到控制台
        addInfo("BEWARE: Writing to the console can be very slow. Avoid logging to the ");
        addInfo("console in production environments, especially in high volume systems.");
        addInfo("See also " + CONSOLE_APPENDER_WARNING_URL);
        OutputStream targetStream = target.getStream();
        // 开启了jansi日志打印, 就使用withJansi的OutputStream
        if (withJansi) {
            targetStream = wrapWithJansi(targetStream);
        }
        // 设置日志打印目的地
        setOutputStream(targetStream);
        super.start();
    }
    
    /** 设置打印目的地, 这里可以选择System.out或者System.err */
    public void setTarget(String value) {
        ConsoleTarget t = ConsoleTarget.findByName(value.trim());
        if (t == null) {
            targetWarn(value);
        } else {
            target = t;
        }
    }
}

方法小结

  1. ConsoleAppender类主要是用来提供日志打印的位置OutputStream, 默认是OutputStream, 使用System.out.write写出数据, System.out.flush()刷新数据
  2. 可以在logback.xml的ConsoleAppender定义子标签target来设置使用System.out还是System.err
  3. 可以使用withJansi标签开启使用JANSI框架打印日志, 需要引入 org.fusesource.jansi:jansi:{version}包, 可以在这里看版本信息 https://mvnrepository.com/artifact/org.fusesource.jansi/jansi

ConsoleAppender给appender标签提供了target子节点来设置日志输出流

综上理解, 我们可以把ConsoleAppender配置成这样

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- ThresholdFilter用于控制当前appender允许打印的日志级别 -->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>INFO</level>
    </filter>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %X{mdcKey} %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <immediateFlush>true</immediateFlush>
    <target>System.out</target>
    <withJansi>false</withJansi>
</appender>

关于其中的layout和encoder的配置, appender中encoder和layout有先后顺序, 后面的覆盖前面的encoder, 这里推荐下面的第2种配置

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- 1.不指定时class, 默认是PatternLayoutEncoder, 它使用默认的PatternLayout类 -->
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <!-- 2.也可以使用可以指定layout的encoder; 推荐使用这种 -->
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}) %clr([%X{traceId}]){magenta} %clr([%thread]){blue} %clr(%-5level) %clr(%logger{50}){cyan} %clr(%file:%line){cyan} - %msg%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
        </layout>
    </encoder>
    <!-- 3.单独用layout的话, 默认使用的encoder就是LayoutWrappingEncoder -->
    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
    </layout>
</appender>

FileAppender

fileAppender也是OutputStreamAppender的直接子类, 用来将日志输入到文件

public class FileAppender<E> extends OutputStreamAppender<E> {
    /** 缓存流可缓存大小 */
    public static final long DEFAULT_BUFFER_SIZE = 8192;
    
    /** 日志文件内容追加到末尾 */
    protected boolean append = true;
    
    /**
     * 日志文件名
     */
    protected String fileName = null;
    /** 是否启用严格模式 */
    private boolean prudent = false;
    
    public void start() {
        int errors = 0;
        // 文件名, 获取的是fileName字段
        if (getFile() != null) {
            addInfo("File property is set to [" + fileName + "]");
			// 1.严格模式下必须设置为追加模式
            if (prudent) {
                if (!isAppend()) {
                    setAppend(true);
                    addWarn("Setting \"Append\" property to true on account of \"Prudent\" mode");
                }
            }
            // 2.判断当前文件是否在当前日志上下文中已经存在过
            if (checkForFileCollisionInPreviousFileAppenders()) {
                addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
                addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL);
                errors++;
            } else {
                // file should be opened only if collision free
                try {
                    // 3.打开文件, 设置outputStream
                    openFile(getFile());
                } catch (java.io.IOException e) {
                    errors++;
                    addError("openFile(" + fileName + "," + append + ") call failed.", e);
                }
            }
        } else {
            errors++;
            addError("\"File\" property not set for appender named [" + name + "].");
        }
        if (errors == 0) {
            super.start();
        }
    }
    
    public void openFile(String file_name) throws IOException {
        // 非公平锁
        streamWriteLock.lock();
        try {
            File file = new File(file_name);
            // 判断文件目录是否存在
            boolean result = FileUtil.createMissingParentDirectories(file);
            if (!result) {
                addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
            }
            // 得到文件的outputStream
            ResilientFileOutputStream resilientFos = new ResilientFileOutputStream(file, append, bufferSize.getSize());
            resilientFos.setContext(context);
            // 设置OutputStreamAppender中的outputStream属性值
            setOutputStream(resilientFos);
        } finally {
            streamWriteLock.unlock();
        }
    }
}

这个start方法有点东西

  1. 严格模式下, 必定开启文件追加模式(append=true)
  2. 当前日志框架启动过程中如果已经配置了当前日志文件在LoggerContext上下文中, 那么当前的appender将会失效(目前不知道什么场景下会有这种情况)
  3. fileName属性是全路径, 使用的是ResilientFileOutputStream包装了一下FileOutputStream类作为日志输出流

下面看看写入数据

@Override
protected void writeOut(E event) throws IOException {
    // 严格模式
    if (prudent) {
        // 安全写入
        safeWriteOut(event);
    } else {
        // 直接使用OutputStreamAppender的writeOut写出数据
        super.writeOut(event);
    }
}

private void safeWriteOut(E event) {
    byte[] byteArray = this.encoder.encode(event);
    if (byteArray == null || byteArray.length == 0)
        return;

    streamWriteLock.lock();
    try {
       safeWriteBytes(byteArray);
    } finally {
        streamWriteLock.unlock();
    }
}

private void safeWriteBytes(byte[] byteArray) {
    ResilientFileOutputStream resilientFOS = (ResilientFileOutputStream) getOutputStream();
    FileChannel fileChannel = resilientFOS.getChannel();
    if (fileChannel == null) {
        return;
    }

    // 1.清除当前线程的中断状态,并获取中断状态, 因为fileChannel.lock()有对当前线程是否中断的判断
    boolean interrupted = Thread.interrupted();

    FileLock fileLock = null;
    try {
        // 2.加个文件锁, 不是juc的类哦
        fileLock = fileChannel.lock();
        long position = fileChannel.position();
        long size = fileChannel.size();
        if (size != position) {
            // 3.移动写入点
            fileChannel.position(size);
        }
        // 4.调用OutputStreamAppender的writeByteArrayToOutputStreamWithPossibleFlush方法写出数据
        writeByteArrayToOutputStreamWithPossibleFlush(byteArray);
    } catch (IOException e) {
        // Mainly to catch FileLockInterruptionExceptions (see LOGBACK-875)
        resilientFOS.postIOFailure(e);
    } finally {
        releaseFileLock(fileLock);

        // 5.设置为线程本来的状态
        if (interrupted) {
            Thread.currentThread().interrupt();
        }
    }
}

方法小结

  1. 严格模式下, 会清除当前线程的中断状态
  2. 写数据之前加个文件锁, 当前文件只能我访问哦(降级效率的做法)
  3. 移动写入点到文件末尾
  4. 写入数据
  5. 还原线程原本的状态(这一趟清除中断状态 再到恢复状态 会不会有aba的问题呢??)
  6. 非严格模式下直接使用OutputStreamAppender的writeOut写出数据

要说明一点, 这里实际写入数据使用的是FileOutputStream, 但是移动指针是使用的是fileChannel, 其实底层FileOutputStream 和 FileChannel 使用同一个文件指针

做个对比

特性FileOutputStreamFileChannel
操作灵活性简单易用,只支持顺序写入支持随机访问,可移动文件指针、切片等操作
同步机制不支持直接同步多个线程操作支持多线程同步,线程安全
写入方式只能直接写字节数据支持直接写入和缓冲区操作
性能相对较低性能更高,特别是在处理大文件时

对FileAppender做一个小结

  1. FileAppender也是OutputStreamAppender的子类, 使用outputStream将日志写出到文件, 使用的是BufferedOutputStream
  2. 使用appender属性设置日志文件以追加的形式记录
  3. 使用file属性设置日志记录的位置, file属性是文件的全路径
  4. 使用prudent=true/false 来开关严格模式, 建议用false或者直接不设置, 蛮耗性能的, 还要独占文件

RollingFileAppender

主要用于将日志信息写入文件,并支持 文件滚动(Rolling),以避免日志文件过大或超出存储限制。

public class RollingFileAppender<E> extends FileAppender<E> {
    /**
     * 当前激活的文件
     */
    File currentlyActiveFile;
    
    /**
     * 触发策略
     */
    TriggeringPolicy<E> triggeringPolicy;
    
    /**
     * 滚动策略
     */
    RollingPolicy rollingPolicy;
    
    public void start() {
        // ...
    }
    
    @Override
    public String getFile() {
        // file属性可以为空, 为空时默认是以fileNamePattern的格式生成文件
        return rollingPolicy.getActiveFileName();
    }
    
    public void rollover() {
        // ...
    }
    
    protected void subAppend(E event) {
     	// ...   
    }
    
    protected void updateByteCount(byte[] byteArray) {
        // ...
    }
}

RollingFileAppender的核心就是这几个属性和方法

  • currentlyActiveFile: 当前记录日志的文件
  • triggeringPolicy 触发滚动的策略
  • rollingPolicy 滚动策略
  • start方法: 校验并初始化一些核心内容
  • getFile方法: 获取当前使用的日志文件
  • rollover方法: 滚动文件
  • subAppend: 添加日志
  • updateByteCount: 更新写入的数量

start方法

public void start() {
    // 需要先设置触发策略
    if (triggeringPolicy == null) {
        addWarn("No TriggeringPolicy was set for the RollingFileAppender named " + getName());
        addWarn(MORE_INFO_PREFIX + RFA_NO_TP_URL);
        return;
    }
    // 需要先启动触发策略,
    if (!triggeringPolicy.isStarted()) {
        addWarn("TriggeringPolicy has not started. RollingFileAppender will not start");
        return;
    }
    // 判断是否有同一个格式的文件名, 不允许
    if (checkForCollisionsInPreviousRollingFileAppenders()) {
        addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
        addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL);
        return;
    }

    // we don't want to void existing log files
    // 默认是append
    if (!append) {
        addWarn("Append mode is mandatory for RollingFileAppender. Defaulting to append=true.");
        append = true;
    }
    // 必须有滚动策略
    if (rollingPolicy == null) {
        addError("No RollingPolicy was set for the RollingFileAppender named " + getName());
        addError(MORE_INFO_PREFIX + RFA_NO_RP_URL);
        return;
    }

    // sanity check for http://jira.qos.ch/browse/LOGBACK-796
    // 校验文件名是否满足配置的文件格式, <file>标签和<fileNamePattern>标签格式不能相同
    if (checkForFileAndPatternCollisions()) {
        addError("File property collides with fileNamePattern. Aborting.");
        addError(MORE_INFO_PREFIX + COLLISION_URL);
        return;
    }
    // 严格模式
    if (isPrudent()) {
        if (rawFileProperty() != null) {
            addWarn("Setting \"File\" property to null on account of prudent mode");
            setFile(null);
        }
        if (rollingPolicy.getCompressionMode() != CompressionMode.NONE) {
            addError("Compression is not supported in prudent mode. Aborting");
            return;
        }
    }

    addInfo("Active log file name: " + getFile());
    currentlyActiveFile = new File(getFile());
    // 初始化-记录已有文件的长度
    initializeLengthCounter();
    // 父类启动, 设置outputStream
    super.start();
}

方法小结

  1. 触发策略不能为空, 并且要先启动, 它在ImplicitModelHandler中会默认先启动

  2. 默认是文件追加模式

  3. 滚动策略不能为空

  4. file标签和fileNamePattern标签格式不能相同, 例如下面的是不允许的

    <file>${log.path}/2025-02/roller_test.2025-02-09.log.gz</file>
        <fileNamePattern>${log.path}/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
    
  5. file标签是允许为空的, 但是fileNamePattern标签不能为空

  6. 使用rollingPolicy获取当前激活的文件(当前写入日志的文件)

  7. 初始化文件大小记录器

RollingPolicy

滚动策略, 这里以TimeBasedRollingPolicy为例介绍

public class TimeBasedRollingPolicy<E> extends RollingPolicyBase implements TriggeringPolicy<E> {
    /** 无压缩后缀的文件命名模式 */
    FileNamePattern fileNamePatternWithoutCompSuffix;
    
    /**
     * 压缩器
     */
    private Compressor compressor;
    
    /**
     * 文件从命名工具类
     */
    private RenameUtil renameUtil = new RenameUtil();
    
    /**
     * 存档删除器
     */
    private ArchiveRemover archiveRemover;
    
    /**
     * 触发策略; 默认是DefaultTimeBasedFileNamingAndTriggeringPolicy; 使用静态代理
     */
    TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedFileNamingAndTriggeringPolicy;
}

TimeBasedRollingPolicy继承了RollingPolicyBase抽象类, 说明它是一个滚动策略, 实现了TriggeringPolicy, 也说明了它是一个触发策略。

从几个属性可以看得出来

  1. compressor: 它支持压缩日志文件
  2. archiveRemover: 支持删除存档过期的文件
  3. timeBasedFileNamingAndTriggeringPolicy: 静态代理触发策略

start方法

 public void start() {
    // set the LR for our utility object
    renameUtil.setContext(this.context);

    // 从文件名模式中找出周期; 由fileNamePattern标签配置的文件表达式
    if (fileNamePatternStr != null) {
        fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context);
        // 根据后缀判断压缩模式, 并设置
        determineCompressionMode();
    } else {
        addWarn(FNP_NOT_SET);
        addWarn(CoreConstants.SEE_FNP_NOT_SET);
        throw new IllegalStateException(FNP_NOT_SET + CoreConstants.SEE_FNP_NOT_SET);
    }
    // 文件压缩对象
    compressor = new Compressor(compressionMode);
    compressor.setContext(context);

    // wcs : without compression suffix
    // 无压缩后缀的文件命名器
    fileNamePatternWithoutCompSuffix = new FileNamePattern(
            Compressor.computeFileNameStrWithoutCompSuffix(fileNamePatternStr, compressionMode), this.context);

    addInfo("Will use the pattern " + fileNamePatternWithoutCompSuffix + " for the active file");
    // 压缩模式
    if (compressionMode == CompressionMode.ZIP) {
        // 获取文件名
        String zipEntryFileNamePatternStr = transformFileNamePattern2ZipEntry(fileNamePatternStr);
        zipEntryFileNamePattern = new FileNamePattern(zipEntryFileNamePatternStr, context);
    }
    // 默认使用DefaultTimeBasedFileNamingAndTriggeringPolicy
    if (timeBasedFileNamingAndTriggeringPolicy == null) {
        timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<>();
    }
    timeBasedFileNamingAndTriggeringPolicy.setContext(context);
    // 触发策略设置滚动策略
    timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
    // 启动触发策略
    timeBasedFileNamingAndTriggeringPolicy.start();

    if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
        addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
        return;
    }

    // the maxHistory property is given to TimeBasedRollingPolicy instead of to
    // the TimeBasedFileNamingAndTriggeringPolicy. This makes it more convenient
    // for the user at the cost of inconsistency here.
    // 最大保存历史记录
    if (maxHistory != UNBOUNDED_HISTORY) {
        archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
        // 保留的文件个数
        archiveRemover.setMaxHistory(maxHistory);
        // 保留的文件大小最大阈值
        archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
        // 启动时删除过期文件
        if (cleanHistoryOnStart) {
            addInfo("Cleaning on start up");
            Instant now = Instant.ofEpochMilli(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
            cleanUpFuture = archiveRemover.cleanAsynchronously(now);
        }
    } else if (!isUnboundedTotalSizeCap()) {
        addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value [" + totalSizeCap + "]");
    }

    super.start();
}

方法小结

  1. fileNamePattern标签必须配置, 并且fileNamePattern对象是通过fileNamePattern标签的值构建的
  2. 压缩模式是根据fileNamePattern标签的后缀决定的, 支持.zip和.gz格式压缩(没有压缩后缀也行, 那就不进行压缩)
  3. 默认使用DefaultTimeBasedFileNamingAndTriggeringPolicy作为滚动触发策略
  4. 可以通过maxHistory标签配置最多保存多少天的存档文件, 和保存的文件大小

触发滚动

添加日志时会先判断是否需要先滚动文件

public boolean isTriggeringEvent(File activeFile, final E event) {
    return timeBasedFileNamingAndTriggeringPolicy.isTriggeringEvent(activeFile, event);
}

这里默认借助的是DefaultTimeBasedFileNamingAndTriggeringPolicy对象来判断是否需要滚动

DefaultTimeBasedFileNamingAndTriggeringPolicy

@NoAutoStart注解标识的LifeCycle对象不会在ImplicitModelHandler中注入时自动启动

@NoAutoStart
public class DefaultTimeBasedFileNamingAndTriggeringPolicy<E> extends TimeBasedFileNamingAndTriggeringPolicyBase<E> {
    public void start() {
        // 启动父类
        super.start();
        // 异常退出
        if (!super.isErrorFree()) {
            return;
        }
        // 默认按照时间混动的日志不支持%i的拆分
        if (tbrp.fileNamePattern.hasIntegerTokenCOnverter()) {
            addError("Filename pattern [" + tbrp.fileNamePattern
                    + "] contains an integer token converter, i.e. %i, INCOMPATIBLE with this configuration. Please remove it.");
            return;
        }
        // 实例化存档删除器
        archiveRemover = new TimeBasedArchiveRemover(tbrp.fileNamePattern, rc);
        archiveRemover.setContext(context);
        started = true;
    }
    
    public boolean isTriggeringEvent(File activeFile, final E event) {
        // 当前时间, 也可以由用户设置
        long currentTime = getCurrentTime();
        // 下一次检查的时间
        long localNextCheck = atomicNextCheck.get();
        // 当前已经到了检查时间
        if (currentTime >= localNextCheck) {
            // 根据当前时间和滚动单位计算下一次的检查时间, 滚动单位根据fileNamePattern标签配置的时间计算
            long nextCheck = computeNextCheck(currentTime);
            atomicNextCheck.set(nextCheck);
            // 当前文件的时间
            Instant instantOfElapsedPeriod = dateInCurrentPeriod;
            addInfo("Elapsed period: " + instantOfElapsedPeriod.toString());
            // 滚动时, 将当前时间转换成存档文件名,
            // 例如当前是2025-02-11, 配置的fileNamePattern为/Users/xxx/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log
            // 那么这里就是/Users/xxx/2025-02/roller_test.2025-02-11.log
            // 那么这个文件名就会保存当天所有的日志, 然后新建一个日志作为当前活动的日志文件
            this.elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convert(instantOfElapsedPeriod);
            // 设置下一次滚动的基础时间; 也就是接下来记录日志的时间
            setDateInCurrentPeriod(currentTime);
            return true;
        } else {
            return false;
        }
    }
}

DefaultTimeBasedFileNamingAndTriggeringPolicy继承自TimeBasedFileNamingAndTriggeringPolicyBase, 覆写了start方法, 并实现了isTriggeringEvent方法

start方法

  1. 启动父类TimeBasedFileNamingAndTriggeringPolicyBase
  2. 不允许fileNamePattern标签有%i标识, %i是给按照文件大小策略滚动用的
  3. 实例化存档删除器

isTriggeringEvent

这个方法用来判断是否需要触发文件滚动的

  1. 判断当前时间是否到了触发滚动的时间; 默认当前时间是系统的当前时间, 也可以使用标签配置; 默认滚动时间是程序启动时根据滚动周期计算的下一个周期时间
  2. 如果当前需要滚动, 那么计算并设置下一次滚动的时间(根据当前时间和滚动周期计算)
  3. 计算当前记录日志的文件滚动时归档的文件名

这里我们再来看一下父类的start方法

TimeBasedFileNamingAndTriggeringPolicyBase#start

public void start() {
    // 日期转换器(primary标识的, 也就是日志格式上没有AUX标识的)
    DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
    if (dtc == null) {
        throw new IllegalStateException(
                "FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
    }
    // 日期转换器; %d{yyyy-MM, Asia/Shanghai}, 这种没有有aux的primary是true,时区就是Asia/Shanghai
    if (dtc.getZoneId() != null) {
        TimeZone tz = TimeZone.getTimeZone(dtc.getZoneId());
        rc = new RollingCalendar(dtc.getDatePattern(), tz, Locale.getDefault());
    } else {
        // dtc.getDatePattern(): primary的日期格式, 如: %d{yyyy-MM} 中的yyyy-MM
        // 并且会设置滚动周期类型
        rc = new RollingCalendar(dtc.getDatePattern());
    }
    addInfo("The date pattern is '" + dtc.getDatePattern() + "' from file name pattern '"
            + tbrp.fileNamePattern.getPattern() + "'.");
    // 打印滚动周期
    rc.printPeriodicity(this);
	// 判断滚动周期是否正确
    if (!rc.isCollisionFree()) {
        addError(
                "The date format in FileNamePattern will result in collisions in the names of archived log files.");
        addError(CoreConstants.MORE_INFO_PREFIX + COLLIDING_DATE_FORMAT_URL);
        withErrors();
        return;
    }

    long timestamp = getCurrentTime();
    // 设置当前活动的日志时间, 也就是日志记录的一个周期时间,会根据这个时间判断是否需要滚动, 以及滚动后的文件名
    setDateInCurrentPeriod(timestamp);
    // file标签; appender的file
    if (tbrp.getParentsRawFileProperty() != null) {
        File currentFile = new File(tbrp.getParentsRawFileProperty());
        // <file>标签配置的文件存在才会设置自定义的时间为当前周期的时间
        if (currentFile.exists() && currentFile.canRead()) {
            timestamp = currentFile.lastModified();
            // 文件的修改时间作为周期计算的起点
            setDateInCurrentPeriod(timestamp);
        }
    }
    addInfo("Setting initial period to " + dateInCurrentPeriod);
    // 根据滚动周期计算并设置下一个检查时间
    long nextCheck = computeNextCheck(timestamp);
    atomicNextCheck.set(nextCheck);
}

方法小结

  1. 根据fileNamePattern标签配置的滚动周期实例化时间滚动器RollingCalendar
  2. 设置当前时间为当前的日志周期
  3. 如果当前记录日志的文件存在, 那么当文件的最后修改时间作为当前的日志周期
  4. 根据日志周期时间计算下一个周期时间

滚动rollover

public void rollover() throws RolloverFailure {

    // 当前日志周期归档文件名
    String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
    // 最后一个斜杆之后的内容, 也就是有后置的文件名
    String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);

    // fileNamePattern标签不是.zip或者.gz后缀
    if (compressionMode == CompressionMode.NONE) {
        // file属性不为空
        if (getParentsRawFileProperty() != null) {
            // 当前文件重命名为归档文件的名称
            renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
        }
    } else {
        // file属性为空
        if (getParentsRawFileProperty() == null) {
            compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName,
                    elapsedPeriodStem);
        } else {
            // 重命名并压缩
            compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
        }
    }
    // 清空归档文件
    if (archiveRemover != null) {
        Instant now = Instant.ofEpochMilli(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
        this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
    }
}

方法小结

  1. fileNamePattern标签不是.zip或者.gz后缀, 并且存在file标签内容, 那么将当前日志文件重命名为归档文件的名称
  2. 如果fileNamePattern有压缩后缀, 并且file标签内容存在, 那么直接压缩
  3. 如果fileNamePattern有压缩后缀, 并且file标签内容不存在, 会借助临时文件来压缩
  4. 删除归档的过期文件

FileNamePattern

FileNamePattern作为<fileNamePattern>标签的解析类, 它可以将一个字符串按照不同的部分生成一个转换链Converter, 然后可以通过这个转换链根据提供的参数替换占位符生成正确的字符串内容。下面大致看一下它的内容。

public class FileNamePattern extends ContextAwareBase {
    /**
     * 允许转换的字符串与其对应的转换器
     */
    static final Map<String, Supplier<DynamicConverter>> CONVERTER_MAP = new HashMap<>();
    static {
        // 对i的转换;数字转字符串, 然后补齐最小长度的转换器
        CONVERTER_MAP.put(IntegerTokenConverter.CONVERTER_KEY, IntegerTokenConverter::new);
        // 对d的转换;日期转字符串, 然后补齐最小长度的转换器
        CONVERTER_MAP.put(DateTokenConverter.CONVERTER_KEY, DateTokenConverter::new);
    }

    /**
     * 需要转换成转换链的模板字符串
     */
    String pattern;
    
    /**
     * 根据pattern生成的转换链的头部节点
     */
    Converter<Object> headTokenConverter;
    
    public FileNamePattern(String patternArg, Context contextArg) {
        // the pattern is slashified
        setPattern(FileFilterUtil.slashify(patternArg));
        setContext(contextArg);
        // 解析fileNamePattern属性得到的转换链
        parse();
        // start converters的各个节点
        ConverterUtil.startConverters(this.headTokenConverter);
    }

    void parse() {
        try {
            // )转为\)
            String patternForParsing = escapeRightParantesis(pattern);
            // 实例化解析器并解析各个部分生成token
            Parser<Object> p = new Parser<Object>(patternForParsing, new AlmostAsIsEscapeUtil());
            p.setContext(context);
            // 解析token生成语法树
            Node t = p.parse();
            // 根据树形节点生成树形转换器
            this.headTokenConverter = p.compile(t, CONVERTER_MAP);
        } catch (ScanException sce) {
            addError("Failed to parse pattern \"" + pattern + "\".", sce);
        }
    }
}

FileNamePattern将一个表达式字符串pattern解析成转换器链headTokenConverter, 并且默认支持对%i%d的解析, 这里%i是用来根据日志文件大小切割的序号, 是代表一个整型, %d就是日期格式, 下面列举它两个比较重要的方法

// 将参数经过转换器处理后得到转换后的字符串
public String convertMultipleArguments(Object... objectList) {
    StringBuilder buf = new StringBuilder();
    Converter<Object> c = headTokenConverter;
    while (c != null) {
        if (c instanceof MonoTypedConverter) {
            // date和Integer的TokenConverter的转换器,需要判断是否是可转换的
            MonoTypedConverter monoTyped = (MonoTypedConverter) c;
            for (Object o : objectList) {
                if (monoTyped.isApplicable(o)) {
                    buf.append(c.convert(o));
                }
            }
        } else {
            buf.append(c.convert(objectList));
        }
        c = c.getNext();
    }
    return buf.toString();
}
// 将数字转为正则, 将date转为字符串, 然后生成字符串
// 可以做到 根据文件格式, 生成对应的正则表达式, 注意只对%i和%d两种类型做处理
// 例如: /Users/uncleqiao/logs/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log.gz
// 转成: /Users/uncleqiao/logs/\d{4}-\d{2}/roller_test.\d{4}-\d{2}-\d{2}.log.gz
public String toRegex() {
    StringBuilder buf = new StringBuilder();
    Converter<Object> p = headTokenConverter;
    while (p != null) {
        // 普通文本
        if (p instanceof LiteralConverter) {
            buf.append(p.convert(null));
        } 
        // %i, 将数字格式转为正则表达式
        else if (p instanceof IntegerTokenConverter) {
            buf.append("\\d+");
        } 
        // %d, 将日期格式转为正则表达式
        else if (p instanceof DateTokenConverter) {
            DateTokenConverter<Object> dtc = (DateTokenConverter<Object>) p;
            buf.append(dtc.toRegex());
        }
        p = p.getNext();
    }
    return buf.toString();
}

三、总结

  1. appender主要分为UnsynchronizedAppenderBase和AppenderBase两大类, 前一种代表了异步添加日志, 后一种代表了同步添加日志, 一般我们使用的是UnsynchronizedAppenderBase, 效率更高, 同一个线程中日志还是有序的。
  2. appender可以通过<filter>标签设置打印日志前的过滤
  3. OutputStreamAppender类型的appender是支持添加encoderOutputStreamimmediateFlushlayout标签
  4. ConsoleAppender通过System.out打印日志到控制台
  5. FileAppender使用的是BufferedOutputStream将日志写入到文件中; FileOutputStream和FileChannel共享一个文件指针, 可以使用FileChannel移动指针, FileOutputStream读写文件
  6. RollingFileAppender支持按照时间(或者大小或者时间大小)滚动日志,
  • 它可以通过file指定当前活动的日志文件(当前写日志的文件)-非必须;
  • 可以通过rollingPolicy标签指定滚动策略,
  • 按时间滚动的话一般是用TimeBasedRollingPolicy, 它既是滚动策略, 也是触发滚动策
  • 可以通过fileNamePattern标签配置归档文件格式, 默认使用的是DefaultTimeBasedFileNamingAndTriggeringPolicy作为触发策略
  • 可以通过maxHistory设置日志归档文件最多保存的天数
  1. fileNamePattern标签支持%i占位数字对按照文件大小分割的文件进行编号; %d对日期占位
  • 一个fileNamePattern标签中%d占位的时间格式有一个必须不被aux修饰, 没有被aux修饰的第一个时间将会定义为primary, 用来定义文件滚动周期的, 例如${log.path}/%d{yyyy-MM, aux, Asia/Shanghai}/roller_test.%d{yyyy-MM-dd}.log 这里的yyyy-MM-dd就是约定按天滚动

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2297733.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Golang的消息队列架构

一、消息队列的定义和作用 消息队列是一种在不同组件之间传递消息的通信机制。它可以解耦系统的各个部分&#xff0c;提高系统的可靠性和扩展性。消息队列可以在系统之间传递消息&#xff0c;并且在消息发送者和消息接收者之间进行异步通信&#xff0c;使得系统可以更加灵活和高…

GESP5级语法知识(十一):高精度算法(一)

高精度加法&#xff1a; #include<iostream> #include<string> #include<algorithm> using namespace std; const int N501;//高精度数的最长长度 //c[]a[]b[]:高精度加法方案一&#xff1a;对应位相加&#xff0c;同时处理进位 void h_add_1(int a[],int b…

【前端】 react项目使用bootstrap、useRef和useState之间的区别和应用

一、场景描述 我想写一个轮播图的程序&#xff0c;只是把bootstrap里面的轮播图拉过来就用上感觉不是很合适&#xff0c;然后我就想自己写自动轮播&#xff0c;因此&#xff0c;这篇文章里面只是自动轮播的部分&#xff0c;没有按键跟自动轮播的衔接部分。 Ps: 本文用的是函数…

PYYAML反序列化详解

前言 最近看了很多pyyaml反序列化的漏洞利用&#xff0c;但是对漏洞怎么来的&#xff0c;没有进行很详细的分析&#xff0c;所以今天刚好学习一下反序列化的原理 Yaml基本语法 一个 .yml 文件中可以有多份配置文件&#xff0c;用 --- 隔开即可对大小写敏感YAML 中的值&#x…

LeeCode题库第十八题

项目场景&#xff1a; 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若两个四元组元素一一对应&#xff0c;则认为两个四元组重复&#xff09;&…

Zookeeper 和 Redis 哪种更好?

目录 前言 &#xff1a; 什么是Zookeeper 和 Redis &#xff1f; 1. 核心定位与功能 2. 关键差异点 (1) 一致性模型 (2) 性能 (3) 数据容量 (4) 高可用性 3. 适用场景 使用 Zookeeper 的场景 使用 Redis 的场景 4. 替代方案 5. 如何选择&#xff1f; 6. 常见误区 7. 总结 前言…

公然上线传销项目,Web3 的底线已经被无限突破

作者&#xff1a;Techub 热点速递 撰文&#xff1a;Yangz&#xff0c;Techub News 今天早些时候&#xff0c;OKX 将上线 PI 的消息在圈内引起轩然大波&#xff0c;对于上线被板上钉钉为传销盘子的「项目」 &#xff0c;Techub News 联系了 OKX 公关&#xff0c;但对方拒绝置评…

C语言第18节:自定义类型——联合和枚举

1. 联合体 C语言中的联合体&#xff08;Union&#xff09;是一种数据结构&#xff0c;它允许在同一内存位置存储不同类型的数据。不同于结构体&#xff08;struct&#xff09;&#xff0c;结构体的成员各自占有独立的内存空间&#xff0c;而联合体的所有成员共享同一块内存区域…

解锁网络安全:穿越数字世界的防护密码

个人主页&#xff1a;java之路-CSDN博客(期待您的关注) 目录 网络安全&#xff1a;数字时代的基石 网络安全面面观 &#xff08;一&#xff09;定义与范畴 &#xff08;二&#xff09;发展历程 网络安全面临的威胁 &#xff08;一&#xff09;恶意软件肆虐 &#xff08;二…

python爬虫解决无限debugger问题

方法一 关闭定时任务 关闭断点执行代码打开断点 # 无限debugger产生原因 # 1. web开发者工具打开 # 2. js代码中有debugger # 3. js有定时处理[推荐] for(let i0;i<99999;i){window.clearInterval(i)}方法二 关闭breakpoint 方法三 修改JS代码 使用fiddler&#xff0c;抓…

C# 两种方案实现调用 DeepSeek API

目录 序 开发运行环境 访问API的一个通用方法 原生官网实现 申请 API key 调用实现 调用示例 腾讯云知识引擎原子调用 申请 API key 调用示例 小结 序 DeepSeek&#xff08;深度求索&#xff09; 最近可谓火爆的一塌糊涂&#xff0c;具体的介绍这里不再赘述&#x…

Linux下的进程切换与调度

目录 1.进程的优先级 优先级是什么 Linux下优先级的具体做法 优先级的调整为什么要受限 2.Linux下的进程切换 3.Linux下进程的调度 1.进程的优先级 我们在使用计算机的时候&#xff0c;通常会启动多个程序&#xff0c;这些程序最后都会变成进程&#xff0c;但是我们的硬…

anolis os 8.9安装jenkins

一、系统版本 # cat /etc/anolis-release Anolis OS release 8.9 二、安装 # dnf install -y epel-release # wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo # rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.…

Java基础知识总结(四十八)--TCP传输、TCP客户端、TCP服务端

**TCP传输&#xff1a;**两个端点的建立连接后会有一个传输数据的通道&#xff0c;这通道称为流&#xff0c;而且是建立在网络基础上的流&#xff0c;称之为socket流。该流中既有读取&#xff0c;也有写入。 **tcp的两个端点&#xff1a;**一个是客户端&#xff0c;一个是服务…

【python】http.server内置库构建临时文件服务

需要从linux开发机上下载一个文件到本地&#xff0c;约700M比较大&#xff0c;通过sz命令下载较慢且传输过程不稳定连续失败&#xff0c;后采用下面方式解决。 cd到一个目录下执行python -m http.server port&#xff0c;port为服务的端口号&#xff1a; 启动后浏览器中访问…

网络安全ids是什么意思

1、 简述IPS和IDS的异同点&#xff1b; 入侵检测系统&#xff08;IDS&#xff09; IDS&#xff08;Intrusion Detection Systems&#xff0c;入侵检测系统&#xff09;&#xff0c;专业上讲就是依照一定的安全策略&#xff0c;对网络、系统、运行状况进行监视&#xff0c;尽可能…

优选驾考小程序

第2章 系统分析 2.1系统使用相关技术分析 2.1.1Java语言介绍 Java语言是一种分布式的简单的 开发语言&#xff0c;有很好的特征&#xff0c;在安全方面、性能方面等。非常适合在Internet环境中使用&#xff0c;也是目前企业级运用中最常用的一个编程语言&#xff0c;具有很大…

42.水果销售系统(springbootvue的Java项目[含微信小程序])

目录 1.系统的受众说明 2.开发环境与技术 2.1 MYSQL数据库 2.2 Java语言 2.3 微信小程序技术 2.4 SpringBoot框架 2.5 B/S架构 2.6 Tomcat 介绍 2.7 HTML简介 2.8 MyEclipse开发工具 3.系统分析 3.1 可行性分析 3.1.1 技术可行性 3.1.2 经济可行性 3.1.3 操作…

ffmpeg所有版本下载地址

地址如下&#xff1a;Index of /releaseshttps://ffmpeg.org/releases/

记PasteSpider部署工具的Windows.IIS版本开发过程之草稿-效果展示(4)

如果有人给你一串JSON数据,你需要编辑他,对于有开发基础的人来说,可能会好处理下,而对于没有开发基础的人来说,那就是灾难了! 那么有没有一个东西,可以让这个编辑更顺畅呢? 贴代码案例中的DynamicForm你值得拥有!本次展示作者在本机上操作IIS的示例,如下 IIS展示 先…