01 log4j入门及xml配置详解

本篇文章中涉及到的所有代码都已经上传到gitee中: gitee.com/sss123a/log...

Hello world!

引入maven依赖:

xml 复制代码
<dependency>  
    <groupId>log4j</groupId>  
    <artifactId>log4j</artifactId>  
    <version>1.2.17</version>  
</dependency>

第一个log4j程序:

java 复制代码
import org.apache.log4j.Logger;  
  
public class HelloWorld {  
    public static void main(String[] args) {  
        Logger logger = Logger.getLogger(HelloWorld.class);  
        logger.info("Hello world!");  
    }  
}

输出结果如下: 这里红色提示其实是log4j内部使用LogLog工具类打印的,我们可以调用LogLog.setQuietMode(true); 来禁用log4j内部一切日志输出。具体可以参考LogLog类中的静态代码块支持更强大的功能配置。

那至于为什么log4j要输出这样的提示信息,那是因为还缺少对Logger的进一步配置。 比如我们可以调用BasicConfigurator.configure(); 。其本质是为该logger对象增加了一个ConsoleAppender,它负责将日志输出到console。原理就等同于jul中的java.util.logging.ConsoleHandler类。具体代入如下:

java 复制代码
package com.matio.log4j.helloworld;  
  
import org.apache.log4j.BasicConfigurator;  
import org.apache.log4j.Logger;  
  
public class HelloWorld {  
    public static void main(String[] args) {  
        BasicConfigurator.configure();  
        Logger logger = Logger.getLogger(HelloWorld.class);  
        logger.info("Hello world!");  
    }  
}

另外,log4j默认支持xml格式和properties格式的配置文件,对应log4j中的两个工具类:

  • DOMConfigurator:解析xml文件
  • PropertyConfigurator 解析properties文件

接下来我们以xml配置文件为例来配置我们的log4j:

1.如果xml文件在在resources目录下,且文件名为log4j.xml,那么log4j可以自动读取该文件,具体可以参考org.apache.log4j.LogManager的静态代码块

2.如果xml文件在resources目录下:

java 复制代码
 DOMConfigurator.configure(Loader.getResource("log4j-asyncappender.xml"))

3.如果xml文件在磁盘上任意位置:

java 复制代码
DOMConfigurator.configure("E:/WorkspaceIdea/01src/log/matio-log4j/src/main/resources/log4j-asyncappender.xml");

4.也可以把xml文件放到一个指定的位置,并且使用环境变量log4j.configuration 来完成配置文件的指定。注意,在log4j.configuration的值中,可以使用文件名称或者url的方式。比如:

log4j.configuration=log4jconfig.properties (resources目录下) log4j.configuration=file:/c:/log4jconfig.xml

log4j.xml配置文件解析

java 复制代码
package org.apache.log4j.spi;  
  
import java.io.InputStream;  
import java.net.URL;  

public interface Configurator {  

    void doConfigure(InputStream inputStream, LoggerRepository repository);  

    void doConfigure(URL url, LoggerRepository repository);  
}

该接口主要有两个核心实现类

  • DOMConfigurator:解析xml文件
  • PropertyConfigurator 解析properties文件

两者实现方式大同小异,我们以xml文件为例解剖log4j内部实现:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">  
<log4j:configuration  
debug="${matio.log4j.debug}"  
reset="${matio.log4j.reset}"  
threshold="${matio.log4j.threshold}"  
xmlns:log4j='http://jakarta.apache.org/log4j/'>  
  
    <!-- debug:同configDebug标签: LogLog.setInternalDebugging(boolean)-->  
    <!-- reset:LoggerRepository.resetConfiguration()-->  
    <!-- threshold:设置LoggerRepository#setThreshold(String) -->  

    <!-- loggerFactory:同categoryFactory标签,可以同时存在多个,但是最后一个才生效 -->  
    <loggerFactory class="${matio.log4j.loggerFactory.class}">  
        <!-- param属性会通过反射注入到class对象中 -->  
        <param name="p1" value="v1"/>  
        <param name="p2" value="v2"/>  
        <!-- class类也可以实现UnrecognizedElementHandler接口然后自己去解析自定义的属性 -->  
        <age>30</age>  
        <name value="matio"/>  
    </loggerFactory>  

    <!-- appender: name和class属性必须-->  
    <appender name="myConsole" class="org.apache.log4j.ConsoleAppender">  
        <layout class="org.apache.log4j.PatternLayout">  
            <param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n"/>  
        </layout>  

        <!--过滤器设置输出的级别-->  
        <filter class="org.apache.log4j.varia.LevelRangeFilter">  
            <param name="levelMin" value="debug"/>  
            <param name="levelMax" value="warn"/>  
            <param name="acceptOnMatch" value="true"/>  
        </filter>  
    </appender>  

    <appender name="myFile" class="org.apache.log4j.RollingFileAppender">  
        <param name="File" value="D:/output.log"/><!-- 设置日志输出文件名 -->  
        <!-- 设置是否在重新启动服务时,在原有日志的基础添加新日志 -->  
        <param name="Append" value="true"/>  
        <param name="MaxBackupIndex" value="10"/>  
        <layout class="org.apache.log4j.PatternLayout">  
            <param name="ConversionPattern" value="%p (%c:%L)- %m%n"/>  
        </layout>  
    </appender>  

    <appender name="activexAppender" class="org.apache.log4j.DailyRollingFileAppender">  
        <param name="File" value="E:/activex.log"/>  
        <param name="DatePattern" value="'.'yyyy-MM-dd'.log'"/>  
        <layout class="org.apache.log4j.PatternLayout">  
            <param name="ConversionPattern" value="[%d{MMdd HH:mm:ss SSS\} %-5p] [%t] %c{3\} - %m%n"/>  
        </layout>  
    </appender>  

    <!-- 指定logger的设置,additivity指示是否遵循缺省的继承机制-->  
    <!-- logger:同category标签-->  
    <logger name="com.runway.bssp.activeXdemo" additivity="false">  
        <!-- level:同priority, -->  
        <level value="info"/>  
        <!-- appender-ref:可以存在多个,开始解析目标appender了, -->  
        <appender-ref ref="activexAppender"/>  
        <appender-ref ref="myFile"/>  
    </logger>  

    <logger name="com.matio.test1" class="com.matio.log4j.logger.LoggerUtil" additivity="false">  
        <priority value="info" class="com.matio.log4j.level.CusLevel"/>  
    </logger>  

    <!-- renderer:可以存在多个 -->  
    <!-- 如果打印的日志message类型是User,就调用CusObjectRenderer.doRender()二次处理生成新的message -->  
    <renderer renderingClass="com.matio.log4j.objectrenderer.CusObjectRenderer"  
    renderedClass="com.matio.log4j.User"/>  
    <renderer renderingClass="com.matio.log4j.objectrenderer.CusObjectRenderer2"  
    renderedClass="com.matio.log4j.Test1"/>  

    <!-- throwableRenderer:class类需要实现ThrowableRenderer接口 -->  
    <throwableRenderer class="com.matio.log4j.throwablerenderer.CusThrowableRenderer">  
        <!-- param属性会通过反射注入到当前class对象中 -->  
        <param name="p100" value="v100"/>  
        <!-- class类也可以实现UnrecognizedElementHandler接口然后自己去解析自定义的属性 -->  
        <lover>zcf</lover>  
    </throwableRenderer>  

    <!-- 根logger的设置-->  
    <root>  
        <priority value="debug"/>  
        <appender-ref ref="myConsole"/>  
        <appender-ref ref="myFile"/>  
    </root>
  
</log4j:configuration>

根节点:log4j:configuration

DOMConfigurator#parse(Element)

用法如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">  
<log4j:configuration  
    debug="${matio.log4j.debug}"  
    reset="${matio.log4j.reset}"  
    threshold="${matio.log4j.threshold}"  
    xmlns:log4j='http://jakarta.apache.org/log4j/'>
    
</log4j:configuration>    

重点: 可以通过${}从系统属性中获取,也可以固定写死

类型 描述 默认值
log4j:configuration 根标签 同configuration
debug 属性 是否打印log4j内部debug日志,同configDebug,LogLog.setInternalDebugging(boolean); false
reset 属性 如果为true则调用LoggerRepository.resetConfiguration();
threshold 属性 默认的日志level,LoggerRepository.setThreshold(); Level.ALL

loggerFactory同categoryFactory:自定义LoggerFactory

如果配置了该标签,那么该xml文件中所有logger都会通过该loggerFactory去生成,不走默认的了

DOMConfigurator#parseCategoryFactory(Element)

可以同时存在多个,但是最后一个才生效

xml 复制代码
<!-- loggerFactory:同categoryFactory标签,可以同时存在多个,但是最后一个才生效 -->  
<loggerFactory class="${matio.log4j.loggerFactory.class}">  
    <!-- param属性会通过反射注入到class对象中 -->  
    <param name="p1" value="v1"/>  
    <param name="p2" value="v2"/>  
    <!-- class类也可以实现UnrecognizedElementHandler接口然后自己去解析自定义的属性 -->  
    <age>30</age>  
    <name value="matio"/>  
</loggerFactory>
类型 描述
loggerFactory 标签 用户可以自定义LoggerFactory,同categoryFactory标签
class 属性 LoggerFactory具体实现类,如果没有设置class属性,则直接跳过该标签
param 标签 通过反射将value设置给class类中的属性
param.name 属性 class类中的属性名称
param.value 属性 class类中的属性值

class属性:可以通过${}从系统属性中获取,比如

System.setProperty("matio.log4j.loggerFactory.class", "com.matio.log4j.loggerfactory.CusLoggerFactory");

也可以固定写死,比如class="com.matio.log4j.loggerfactory.CusLoggerFactory"

备注:如果class实现类实现了UnrecognizedElementHandler接口,那么我们还可以解析自定义xml标签,示例代码:

java 复制代码
package com.matio.log4j.loggerfactory;  
  
import org.apache.log4j.Logger;  
import org.apache.log4j.spi.LoggerFactory;  
import org.apache.log4j.xml.DOMConfigurator;  
import org.apache.log4j.xml.UnrecognizedElementHandler;  
import org.w3c.dom.Element;  
  
import java.util.Properties;  
  
public class CusLoggerFactory implements LoggerFactory, UnrecognizedElementHandler {  
  
    private String p1; // <param name="p1" value="v1"/>  
    private String p2; // <param name="p2" value="v2"/>  
    private int age;  
    private String name;  

    public Logger makeNewLoggerInstance(String name) {  
        return Logger.getLogger(name);  
    }  

    public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {  
        String nodeName = element.getNodeName();  
        if ("age".equals(nodeName)) { // 解析:<age>30</age>  
            String ageStr = DOMConfigurator.subst(element.getNodeValue(), props);  
            if (ageStr != null) {  
                age = Integer.parseInt(ageStr);  
            }  
        } else if ("name".equals(nodeName)) { // 解析:<name value="matio"/>  
            name = DOMConfigurator.subst(element.getAttribute("value"), props);  
        }  
        return true;  
    }  
    // 忽略getter()和setter()...
}

logger同category

定义一个logger对象

DOMConfigurator#parseCategory(Element)

可以同时存在多个logger标签

xml 复制代码
<logger name="com.runway.bssp.activeXdemo" additivity="false">  
    <!-- level:同priority, -->  
    <level value="info"/>  
    <!-- appender-ref:可以存在多个,开始解析目标appender了, -->  
    <appender-ref ref="activexAppender"/>  
    <appender-ref ref="myFile"/>  
</logger>  
  
<logger name="com.matio.test1" class="com.matio.log4j.logger.LoggerUtil" additivity="false">  
    <priority value="info" class="com.matio.log4j.level.CusLevel"/>  
</logger>
类型 描述
logger 标签 创建一个logger对象,同category标签
class 属性 通过该类名反射调用其内部的getLogger()方法去生成logger对象
name 属性 该logger的name
additivity 属性 是否遵循缺省的继承机制,默认true
appender-ref 标签 appender的name,这个时候才开始去解析目标appender标签
level 标签 该logger的日志level,可以通过class属性自定义level
priority 标签 同level
param 标签 通过反射将value设置给class类中的属性
param.name 属性 class类中的属性名称
param.value 属性 class类中的属性值

在每个logger标签被解析完成后,如果发现该logger类实现了org.apache.log4j.spi.OptionHandler接口,就会回调其activateOptions()方法

root

只能存在一个root标签

xml 复制代码
<!-- 根logger的设置-->  
<root>  
    <priority value="debug"/>  
    <appender-ref ref="myConsole"/>  
    <appender-ref ref="myFile"/>  
</root>
类型 描述
appender-ref 标签 appender的name,这个时候才开始去解析目标appender标签
level 标签 该logger的日志level,可以通过class属性自定义level
priority 标签 同level
param 标签 通过反射将value设置给class类中的属性
param.name 属性 class类中的属性名称
param.value 属性 class类中的属性值

在root标签被解析完成后,如果发现该logger类实现了org.apache.log4j.spi.OptionHandler接口,就会回调其activateOptions()方法

renderer

不知道大家有没有注意到logger.info();方法接收的参数类型是什么.其实都是Object类型(jul接受的都是string类型),这说明log4j可以打印任意类型的日志,比如:

java 复制代码
BasicConfigurator.configure(); // 日志输出到console上
Logger logger = Logger.getLogger("test");
logger.info(new User("best", "zcf", "matio"));  
logger.info(new Test1("aaaa"));

以上两行代码打印结果如下:

js 复制代码
0 [main] INFO test  - User{school='best', like='zcf', name='matio'}
1 [main] INFO test  - Test1{x='aaaa'}

我们都知道输出一个对象其实就是输出其string()方法,但是log4j提供了一个工具来用户根据指定的日志对象类型输出对应的日志,这个工具就是org.apache.log4j.or。ObjectRenderer,其接口定义是如下:

java 复制代码
public interface ObjectRenderer {  
 String doRender(Object o);  
}

参数o就是我们上面的User对象和Test1对象,返回值就是最终要打印的日志结果

这里为了更好的理解什么是ObjectRenderer,附带了一个DEMO,它不需要xml配置文件,示例如下:

java 复制代码
package com.matio.log4j.objectrenderer;  
  
import com.matio.log4j.Test1;  
import com.matio.log4j.User;  
import org.apache.log4j.BasicConfigurator;  
import org.apache.log4j.LogManager;  
import org.apache.log4j.Logger;  
import org.apache.log4j.or.RendererMap;  
import org.apache.log4j.spi.RendererSupport;  
  
public class ObjectRendererTest {  
    public static void main(String[] args) {  
        // 注册一个consoleAppender,等会日志打印在控制台上  
        BasicConfigurator.configure();  

        // 注册一个ObjectRenderer专门用于处理User类型的消息  
        {  
            // message的class  
            String renderedClassName = "com.matio.log4j.User";  
            // ObjectRenderer实现类  
            String renderingClassName = "com.matio.log4j.objectrenderer.CusObjectRenderer";  
            RendererMap.addRenderer((RendererSupport) LogManager.getLoggerRepository(), renderedClassName, renderingClassName);  
        }  

        // 再注册一个ObjectRenderer专门用于处理Test1类型的消息  
        {  
            // message的class  
            String renderedClassName = "com.matio.log4j.Test1";  
            // ObjectRenderer实现类  
            String renderingClassName = "com.matio.log4j.objectrenderer.CusObjectRenderer2";  
            RendererMap.addRenderer((RendererSupport) LogManager.getLoggerRepository(), renderedClassName, renderingClassName);  
        }  

        Logger logger = Logger.getLogger("test");  
        logger.info(new User("best", "zcf", "matio"));  
        logger.info(new Test1("aaaa"));  
    }  
}

自定义ObjectRenderer,用于处理User类型和Test1类型的消息:

java 复制代码
package com.matio.log4j.objectrenderer;  
  
import com.matio.log4j.User;  
import org.apache.log4j.or.ObjectRenderer;  
import org.apache.log4j.xml.UnrecognizedElementHandler;  
import org.w3c.dom.Element;  
import java.util.Properties;  
  
public class CusObjectRenderer implements ObjectRenderer, UnrecognizedElementHandler {  
  
    public String doRender(Object o) {  
        if (o instanceof User) {  
            User user = (User) o;  
            return "测试ObjectRenderer : " + user.toString();  
        }  
        return o.toString();  
    }  

    // 这个只在xml配置生效  
    public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {  
        return true;  
    }  
}

public class CusObjectRenderer2 implements ObjectRenderer {  
  
    public String doRender(Object o) {  
        Test1 test1 = (Test1) o;  
        return "测试ObjectRenderer222 : " + test1.toString();  
    }  
}

其打印结果如下:

js 复制代码
0 [main] INFO test  - 测试ObjectRenderer : User{school='best', like='zcf', name='matio'}
0 [main] INFO test  - 测试ObjectRenderer222 : Test1{x='aaaa'}

那么在xml文件中我们该如何配置renderer呢,具体可以参考:

DOMConfigurator#parseRenderer(Element);

renderer在xml中的配置如下,支持多个:

xml 复制代码
<!-- renderer:可以存在多个 -->  
<!-- 如果打印的日志message类型是User,就调用CusObjectRenderer.doRender()二次处理生成新的message -->  
<renderer renderingClass="com.matio.log4j.objectrenderer.CusObjectRenderer"  
renderedClass="com.matio.log4j.User"/>

<renderer renderingClass="com.matio.log4j.objectrenderer.CusObjectRenderer2"  
renderedClass="com.matio.log4j.Test1"/>
类型 描述
renderer 标签 真的指定类型的日志,就调用CusObjectRenderer.doRender()二次处理生成新的日志
renderingClass 属性 ObjectRenderer的实现类
renderedClass 属性 日志类型

throwableRenderer

什么是throwableRenderer?

它可以将Throwable转成String,对应log4j中的接口定义如下:

java 复制代码
package org.apache.log4j.spi;  

public interface ThrowableRenderer {  
    public String[] doRender(Throwable t);  
}

log4j默认使用DefaultThrowableRenderer

没有注册自定义ThrowableRenderer之前,实例代码如下:

java 复制代码
package com.matio.log4j.throwablerenderer;  
  
import org.apache.log4j.BasicConfigurator;  
import org.apache.log4j.LogManager;  
import org.apache.log4j.Logger;  
import org.apache.log4j.spi.ThrowableRendererSupport;  
  
public class TestThrowableRenderer {  
  
    public static void main(String[] args) {  
        BasicConfigurator.configure(); // 日志输出到console上  
        Logger logger = Logger.getLogger("Te");  
        logger.error("错误xxx:", new IllegalArgumentException());  
        logger.error("错误:");  
    }  
}

输出结果如下:

js 复制代码
1 [main] ERROR Te  - 错误xxx:
java.lang.IllegalArgumentException
	at com.matio.log4j.throwablerenderer.TestThrowableRenderer.main(TestThrowableRenderer.java:17)
3 [main] ERROR Te  - 错误:

当注册一个自定义的ThrowableRenderer后代码如下:

java 复制代码
package com.matio.log4j.throwablerenderer;  
  
import org.apache.log4j.BasicConfigurator;  
import org.apache.log4j.LogManager;  
import org.apache.log4j.Logger;  
import org.apache.log4j.spi.ThrowableRendererSupport;  
  
public class TestThrowableRenderer {  
  
    public static void main(String[] args) {  
        BasicConfigurator.configure(); // 日志输出到console上  

        // 注册一个自定义的ThrowableRenderer  
        {  
            CusThrowableRenderer renderer = new CusThrowableRenderer();  
            ((ThrowableRendererSupport) LogManager.getLoggerRepository()).setThrowableRenderer(renderer);  
        }  
        Logger logger = Logger.getLogger("Te");  
        logger.error("错误xxx:", new IllegalArgumentException());  
        logger.error("错误:");  
    }  
}
java 复制代码
package com.matio.log4j.throwablerenderer;  
  
import org.apache.log4j.spi.OptionHandler;  
import org.apache.log4j.spi.ThrowableRenderer;  
import org.apache.log4j.xml.DOMConfigurator;  
import org.apache.log4j.xml.UnrecognizedElementHandler;  
import org.w3c.dom.Element;  
  
import java.io.PrintWriter;  
import java.io.StringWriter;  
import java.util.Properties;  
  
public class CusThrowableRenderer implements ThrowableRenderer, OptionHandler, UnrecognizedElementHandler {  
  
    private String p100;    
    private String lover;  
  
    public String[] doRender(Throwable t) {  
        StringWriter stringWriter = new StringWriter();  
        PrintWriter printWriter = new PrintWriter(stringWriter);  
        t.printStackTrace(printWriter);  
        printWriter.close();  
        String[] strings = new String[1];  
        strings[0] = "自定义ThrowableRenderer: " + stringWriter.toString();  
        return strings;  
    }  

    public void activateOptions() {  

    }  

    // 这里只在xml配置中生效  
    public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {  
        this.lover = DOMConfigurator.subst(element.getNodeValue(), props);  
        return true;  
    }  
  
    // 省略getter()和setter()...  
}

输出结果如下:

js 复制代码
0 [main] ERROR Te  - 错误xxx:
自定义ThrowableRenderer: java.lang.IllegalArgumentException
	at com.matio.log4j.throwablerenderer.TestThrowableRenderer.main(TestThrowableRenderer.java:19)

2 [main] ERROR Te  - 错误:

那么在xml中如何声明throwableRenderer呢,具体参考

DOMConfigurator#parseThrowableRenderer(Element);

xml 复制代码
<!-- throwableRenderer:class类需要实现ThrowableRenderer接口 -->  
<throwableRenderer class="com.matio.log4j.throwablerenderer.CusThrowableRenderer">  
    <!-- param属性会通过反射注入到当前class对象中 -->  
    <param name="p100" value="v100"/>  
    <!-- class类也可以实现UnrecognizedElementHandler接口然后自己去解析自定义的属性 -->  
    <lover>zcf</lover>  
</throwableRenderer>
类型 描述
throwableRenderer 标签 自定义ThrowableRenderer
class 属性 ThrowableRendere的实现类
param 标签 通过反射将value设置给class类中的属性
param.name 属性 class类中的属性名称
param.value 属性 class类中的属性值

在throwableRenderer标签被解析完成后,如果发现该throwableRenderer类实现了org.apache.log4j.spi.OptionHandler接口,就会回调其activateOptions()方法

appender

每一个handler标签都会被实例化成一个Appender对象(它其实就等同于jul中的Handler),负责将日志输出到console上、保存到文件中,保存到数据库中、或者通过socket发送给远端等

DOMConfigurator#parseAppender(Element);

xml 复制代码
<!-- appender: name和class属性必须-->  
<appender name="myConsole" class="org.apache.log4j.ConsoleAppender">  
    <layout class="org.apache.log4j.PatternLayout">  
        <param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n"/>  
    </layout>  

    <!--过滤器设置输出的级别-->  
    <filter class="org.apache.log4j.varia.LevelRangeFilter">  
        <param name="levelMin" value="debug"/>  
        <param name="levelMax" value="warn"/>  
        <param name="acceptOnMatch" value="true"/>  
    </filter>  
    
    <!-- class类也可以实现UnrecognizedElementHandler接口然后自己去解析自定义的属性 --> 
    <age>30</age> 
    <name value="matio"/>
</appender>
类型 描述
appender 标签 对应jul中的handler对象
class 属性 Appender实现类
name 属性 该appender的name
param 标签 通过反射将value设置给class类中的属性
param.name 属性 class类中的属性名称
param.value 属性 class类中的属性值
layout 标签 定义该appender的日志输出样式,见parseLayout()
filter 标签 定义该appender的日志过滤器,见parseFilters()
errorHandler 标签 输出日志时如果出现异常就交给该handler处理,见parseErrorHandler()
appender-ref 标签 只有该appender实现了AppenderAttachable接口才有效,就是代表真正的appender,可以有多个

在每个appender标签被解析完成后,如果发现该appender类实现了org.apache.log4j.spi.OptionHandler接口,就会回调其activateOptions()方法

最后再添加到对应的logger中

相关推荐
一只叫煤球的猫4 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9654 小时前
tcp/ip 中的多路复用
后端
bobz9654 小时前
tls ingress 简单记录
后端
皮皮林5516 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友6 小时前
什么是OpenSSL
后端·安全·程序员
bobz9656 小时前
mcp 直接操作浏览器
后端
前端小张同学8 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook8 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康9 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在9 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net