在日常开发中经常通过打印日志记录程序执行的步骤或者排查问题,如下代码类似很多,但是,它是如何执行的呢?

  1. package chapters;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. // 省略...
  5. Logger logger = LoggerFactory.getLogger(LogbackTest.class);
  6. logger.info(" {} is best player in world", "Greizmann");

本文以Logback日志框架来分析以上代码的实现。

slf4j

如今日志框架常用的有log4j、log4j2、jul(common-log)以及logback。假如项目中用的是jul,如今想改成用log4j,如果直接引用java.util.logging包中Logger,需要修改大量代码,为了解决这个麻烦的事情,Ceki Gülcü 大神开发了slf4j(Simple Logging Facade for Java) 。slf4j 是众多日志框架抽象的门面接口,有了slf4j 想要切换日志实现,只需要把对应日志jar替换和添加对应的适配器。


> 图片来源: [一个著名的日志系统是怎么设计出来的?](https://mp.weixin.qq.com/s/XiCky-Z8-n4vqItJVHjDIg)

从图中就可以知道我们开始的代码为什么引 slf4j 包。在阿里的开发手册上一条

强制:应用中不可直接使用日志系统(log4j、logback)中的 API ,而应依赖使用日志框架 SLF4J 中的 API 。使用门面模式的日志框架,有利于维护和各个类的日志处理方式的统一。

Logback 实现了 SLF4J ,少了中间适配层, Logback也是Ceki Gülcü 大神开发的。

Logger & Appender & Layouts

Logback 主要的三个类 logger,appender和layouts。这三个组件一起作用可以满足我们开发中根据消息的类型以及日志的级别打印日志到不同的地方。

Logger

ch.qos.logback.classic.Logger类结构:

Logger 依附在LoggerContext上,LoggerContext负责生产Logger,通过一个树状的层次结构来进行管理。Logger 维护着当前节点的日志级别及level值。logger按 "." 分代(层级),日志级别有继承能力,如:名字为 chapters.LogbackTest 如果没有设置日志级别,会继承它的父类chapters 日志级别。所有日志的老祖宗都是ROOT名字的Logger,默认DEBUG级别。当前节点设置了日志级别不会考虑父类的日志级别。Logger 通过日志级别控制日志的启用和禁用。日志级别 TRACE < DEBUG < INFO < WARN < ERROR

接下来我们结合配置文件看一下Logger属性对应的配置标签:

  1. <configuration>
  2. <turboFilter class="ch.qos.logback.classic.turbo.MDCFilter">
  3. <MDCKey>username</MDCKey>
  4. <Value>sebastien</Value>
  5. <OnMatch>ACCEPT</OnMatch>
  6. </turboFilter>
  7. <appender name="FILE" class="ch.qos.logback.core.FileAppender">
  8. <file>/Users/wolf/study/logback/logback-examples/myApp.log</file>
  9. <encoder>
  10. <pattern>%msg%n</pattern>
  11. </encoder>
  12. </appender>
  13. <logger name="chapters.LogbackTest" level="DEBUG"></logger>
  14. <root>
  15. <appender-ref ref="FILE"/>
  16. </root>
  17. </configuration>

name:logger 标签中 name 属性值。

level:logger 标签中 level 属性值。

parent:封装了父类 "chapters",以及"chapters"的父类“ROOT”的logger对象。

aai:appender-ref 标签,及这里对应 FileAppender 的实现类对象。如果没有appender-ref标签该值为null。

loggerContext:维护着过滤器,如 turbo 过滤器等。

Appender

Appender 作用是控制日志输出的目的地。日志输出的目的地是多元化,你可以把日志输出到console、file、remote socket server、MySQL、PostgreSQL、Oracle 或者其它的数据库、JMS、remote UNIX Syslog daemons 中。一个日志可以输出到多个目的地。如

  1. <configuration>
  2. <appender name="FILE" class="ch.qos.logback.core.FileAppender">
  3. <file>/Users/wolf/study/logback/logback-examples/myApp.log</file>
  4. <encoder>
  5. <pattern>%msg%n</pattern>
  6. </encoder>
  7. </appender>
  8. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  9. <encoder>
  10. <pattern>%msg%n</pattern>
  11. </encoder>
  12. </appender>
  13. <root>
  14. <appender-ref ref="STDOUT" />
  15. <appender-ref ref="FILE"/>
  16. </root>
  17. </configuration>

该xml配置把日志输出到了myApp.log文件和console中。

Layouts/Encoder

有上面Logger和Appender两大组件,日志已经输出到目的地了,但是这样打印的日志对我们这种凡人不太友好,读起来费劲。凡人就要做到美观,那就用Layouts或Encoder美化一下日志输出格式吧。Encoder 在 logback 0.9.19 版本引进。在之前的版本中,大多数的 appender 依赖 layout 将日志事件转换为 string,然后再通过 java.io.Writer 写出。在之前的版本中,用户需要在 FileAppender 中内置一个 PatternLayout。在 0.9.19 之后的版本中,FileAppender 以及子类需要一个 encoder 而不是 layout。

源码

Logger创建

  1. Logger logger = LoggerFactory.getLogger(LogbackTest.class);

接下来我们根据源码分析一下logger的初始化。分析源码之前还是按照老规矩来一张接口调用时序图吧。

第步:org.slf4j.LoggerFactory#getLogger(java.lang.String)

  1. public static Logger getLogger(String name) {
  2. ILoggerFactory iLoggerFactory = getILoggerFactory();
  3. return iLoggerFactory.getLogger(name);
  4. }

获取一个ILoggerFactory,即LoggerContext。然后从其获取到Logger对象。

第3步:org.slf4j.LoggerFactory#getILoggerFactory

  1. public static ILoggerFactory getILoggerFactory() {
  2. return getProvider().getLoggerFactory();
  3. }

第4步:org.slf4j.LoggerFactory#getProvider

  1. static SLF4JServiceProvider getProvider() {
  2. if (INITIALIZATION_STATE == UNINITIALIZED) {
  3. synchronized (LoggerFactory.class) {
  4. if (INITIALIZATION_STATE == UNINITIALIZED) {
  5. INITIALIZATION_STATE = ONGOING_INITIALIZATION;
  6. performInitialization();
  7. }
  8. }
  9. }
  10. switch (INITIALIZATION_STATE) {
  11. case SUCCESSFUL_INITIALIZATION:
  12. return PROVIDER;
  13. case NOP_FALLBACK_INITIALIZATION:
  14. return NOP_FALLBACK_FACTORY;
  15. case FAILED_INITIALIZATION:
  16. throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
  17. case ONGOING_INITIALIZATION:
  18. return SUBST_PROVIDER;
  19. }
  20. throw new IllegalStateException("Unreachable code");
  21. }

对SLF4JServiceProvider初始化,即LogbackServiceProvider对象。然后检查初始化状态,如果成功就返回PROVIDER。

第5步:org.slf4j.LoggerFactory#performInitialization

  1. private final static void performInitialization() {
  2. bind();
  3. if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
  4. versionSanityCheck();
  5. }
  6. }

第6步:org.slf4j.LoggerFactory#bind

  1. private final static void bind() {
  2. try {
  3. // 加载 SLF4JServiceProvider
  4. List<SLF4JServiceProvider> providersList = findServiceProviders();
  5. reportMultipleBindingAmbiguity(providersList);
  6. if (providersList != null && !providersList.isEmpty()) {
  7. PROVIDER = providersList.get(0);
  8. // SLF4JServiceProvider.initialize()
  9. PROVIDER.initialize();
  10. INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
  11. reportActualBinding(providersList);
  12. fixSubstituteLoggers();
  13. replayEvents();
  14. SUBST_PROVIDER.getSubstituteLoggerFactory().clear();
  15. } else {
  16. // 省略代码。。。
  17. }
  18. } catch (Exception e) {
  19. // 失败,设置状态值,上报
  20. failedBinding(e);
  21. throw new IllegalStateException("Unexpected initialization failure", e);
  22. }
  23. }

通过ServiceLoader加载LogbackServiceProvider,然后进行初始化相关字段。初始化成功后把初始化状态设置成功状态值。

第7步:ch.qos.logback.classic.spi.LogbackServiceProvider#initialize

  1. public void initialize() {
  2. // 初始化默认的loggerContext
  3. defaultLoggerContext = new LoggerContext();
  4. defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
  5. initializeLoggerContext();
  6. markerFactory = new BasicMarkerFactory();
  7. mdcAdapter = new LogbackMDCAdapter();
  8. }

创建名字为default的LoggerContext对象,并初始化一些字段默认值。

ch.qos.logback.classic.LoggerContext#LoggerContext

  1. public LoggerContext() {
  2. super();
  3. this.loggerCache = new ConcurrentHashMap<String, Logger>();
  4. this.loggerContextRemoteView = new LoggerContextVO(this);
  5. this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
  6. this.root.setLevel(Level.DEBUG);
  7. loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
  8. initEvaluatorMap();
  9. size = 1;
  10. this.frameworkPackages = new ArrayList<String>();
  11. }

初始化LoggerContext时设置了ROOT的Logger,日志级别为DEBUG。

第8步:ch.qos.logback.classic.spi.LogbackServiceProvider#initializeLoggerContext

  1. private void initializeLoggerContext() {
  2. try {
  3. try {
  4. new ContextInitializer(defaultLoggerContext).autoConfig();
  5. } catch (JoranException je) {
  6. Util.report("Failed to auto configure default logger context", je);
  7. }
  8. // 省略代码。。。
  9. } catch (Exception t) { // see LOGBACK-1159
  10. Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
  11. }
  12. }

把第7步初始化好的LoggerContext当做参数传入ContextInitializer,构建其对象。然后解析配置文件。

第9步:ch.qos.logback.classic.util.ContextInitializer#autoConfig

  1. public void autoConfig() throws JoranException {
  2. StatusListenerConfigHelper.installIfAsked(loggerContext);
  3. // (1) 从指定路径获取
  4. URL url = findURLOfDefaultConfigurationFile(true);
  5. if (url != null) {
  6. configureByResource(url);
  7. } else {
  8. // (2) 从运行环境中获取
  9. Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
  10. if (c != null) {
  11. // 省略代码。。。
  12. } else {
  13. // (3)设置默认的
  14. BasicConfigurator basicConfigurator = new BasicConfigurator();
  15. basicConfigurator.setContext(loggerContext);
  16. basicConfigurator.configure(loggerContext);
  17. }
  18. }
  19. }

首先从指定的路径获取资源URL,如果存在就进行解析;如果不存在再从运行环境中获取配置;如果以上都没有最后会构建一个BasicConfigurator当作默认的。

ch.qos.logback.classic.util.ContextInitializer#findURLOfDefaultConfigurationFile

  1. public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
  2. ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
  3. // 启动参数中获取
  4. URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
  5. if (url != null) {
  6. return url;
  7. }
  8. // logback-test.xml
  9. url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
  10. if (url != null) {
  11. return url;
  12. }
  13. //logback.groovy
  14. url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus);
  15. if (url != null) {
  16. return url;
  17. }
  18. // logback.xml
  19. return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
  20. }

先从启动参数中查找logback.configurationFile参数值,如果没有再从classpath中一次查找logback-test.xml -> logback.groovy -> logback.xml 。由此可知文件的优先级是 启动参数 -> logback-test.xml -> logback.groovy -> logback.xml

第10步:ch.qos.logback.classic.util.ContextInitializer#configureByResource

  1. public void configureByResource(URL url) throws JoranException {
  2. if (url == null) {
  3. throw new IllegalArgumentException("URL argument cannot be null");
  4. }
  5. final String urlString = url.toString();
  6. if (urlString.endsWith("groovy")) {
  7. // 省略代码。。。
  8. } else if (urlString.endsWith("xml")) {
  9. JoranConfigurator configurator = new JoranConfigurator();
  10. configurator.setContext(loggerContext);
  11. configurator.doConfigure(url);
  12. } else {
  13. // 省略代码。。。
  14. }
  15. }

根据文件后缀判断是 groovy或者xml,然后交给不同的配置解析器处理。这里也是把第7步中的LoggerContext传进去,继续封装它的字段值。

第12步:ch.qos.logback.core.joran.GenericConfigurator#doConfigure(org.xml.sax.InputSource)

  1. public final void doConfigure(final InputSource inputSource) throws JoranException {
  2. long threshold = System.currentTimeMillis();
  3. SaxEventRecorder recorder = new SaxEventRecorder(context);
  4. recorder.recordEvents(inputSource);
  5. // 处理配置文件,封装到 LoggerContext 中
  6. playEventsAndProcessModel(recorder.saxEventList);
  7. StatusUtil statusUtil = new StatusUtil(context);
  8. if (statusUtil.noXMLParsingErrorsOccurred(threshold)) {
  9. registerSafeConfiguration(recorder.saxEventList);
  10. }
  11. }

真正解析配置文件的逻辑在playEventsAndProcessModel方法中,这里就不展开分析了。到这一步LoggerContext基本初始化完成了。

第13步:ch.qos.logback.classic.LoggerContext#getLogger(java.lang.String)

  1. @Override
  2. public Logger getLogger(final String name) {
  3. // 省略代码。。。
  4. if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
  5. return root;
  6. }
  7. int i = 0;
  8. Logger logger = root;
  9. // 从缓存中获取, 有直接返回
  10. Logger childLogger = (Logger) loggerCache.get(name);
  11. if (childLogger != null) {
  12. return childLogger;
  13. }
  14. // if the desired logger does not exist, them create all the loggers
  15. // in between as well (if they don't already exist)
  16. String childName;
  17. while (true) {
  18. int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
  19. if (h == -1) {
  20. childName = name;
  21. } else {
  22. childName = name.substring(0, h);
  23. }
  24. // move i left of the last point
  25. i = h + 1;
  26. synchronized (logger) {
  27. childLogger = logger.getChildByName(childName);
  28. if (childLogger == null) {
  29. childLogger = logger.createChildByName(childName);
  30. loggerCache.put(childName, childLogger);
  31. incSize();
  32. }
  33. }
  34. logger = childLogger;
  35. if (h == -1) {
  36. return childLogger;
  37. }
  38. }
  39. }

经过前面漫长的对LoggerContext进行初始化工作,这一步就是从LoggerContext获取Logger对象。如果缓存中直接返回。否则通过“.”分代构建层次结构。

日志执行步骤

上一节Logger创建完成,接下来分析一下打日志的流程。

  1. logger.info(" {} is best player in world", "Greizmann");

第1步:ch.qos.logback.classic.Logger#info(java.lang.String, java.lang.Object)

  1. public void info(String format, Object arg) {
  2. filterAndLog_1(FQCN, null, Level.INFO, format, arg, null);
  3. }

把接口的日志级别(Level.INFO)传到下一个方法。

第2步:ch.qos.logback.classic.Logger#filterAndLog_1

  1. private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) {
  2. // 先通过turboFilter过滤
  3. final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t);
  4. // 判断日志级别
  5. if (decision == FilterReply.NEUTRAL) {
  6. if (effectiveLevelInt > level.levelInt) {
  7. return;
  8. }
  9. } else if (decision == FilterReply.DENY) {
  10. return;
  11. }
  12. buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t);
  13. }

如果TurboFilter过滤器存在就会执行相关操作,并返回FilterReply。如果结果是FilterReply.DENY本条日志消息直接丢弃;如果是FilterReply.NEUTRAL会继续判断日志级别是否在该方法级别之上;如果是FilterReply.ACCEPT直接跳到下一步。

第3步:ch.qos.logback.classic.Logger#buildLoggingEventAndAppend

  1. private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) {
  2. LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
  3. le.setMarker(marker);
  4. callAppenders(le);
  5. }

创建了LoggingEvent对象,该对象包含日志请求所有相关的参数,请求的 logger,日志请求的级别,日志信息,与日志一同传递的异常信息,当前时间,当前线程,以及当前类的各种信息和 MDC。其实打印日志就是一个事件,所以这个对象是相关重要,下面全部是在操作该对象。

第4步:ch.qos.logback.classic.Logger#callAppenders

  1. public void callAppenders(ILoggingEvent event) {
  2. int writes = 0;
  3. // 从自己往父辈查找满足
  4. for (Logger l = this; l != null; l = l.parent) {
  5. // 写文件
  6. writes += l.appendLoopOnAppenders(event);
  7. if (!l.additive) {
  8. break;
  9. }
  10. }
  11. // No appenders in hierarchy
  12. if (writes == 0) {
  13. loggerContext.noAppenderDefinedWarning(this);
  14. }
  15. }

第5步:ch.qos.logback.classic.Logger#appendLoopOnAppenders

  1. private int appendLoopOnAppenders(ILoggingEvent event) {
  2. if (aai != null) {
  3. return aai.appendLoopOnAppenders(event);
  4. } else {
  5. return 0;
  6. }
  7. }

从当前Logger到父节点遍历,直到AppenderAttachableImpl不为空(有appender-ref 标签)。

第6步:ch.qos.logback.core.spi.AppenderAttachableImpl#appendLoopOnAppenders

  1. public int appendLoopOnAppenders(E e) {
  2. int size = 0;
  3. final Appender<E>[] appenderArray = appenderList.asTypedArray();
  4. final int len = appenderArray.length;
  5. for (int i = 0; i < len; i++) {
  6. appenderArray[i].doAppend(e);
  7. size++;
  8. }
  9. return size;
  10. }

如果设置了多个日志输出目的地,这里就是循环调用对应的Appender进行输出。

第7步:ch.qos.logback.core.UnsynchronizedAppenderBase#doAppend

  1. public void doAppend(E eventObject) {
  2. if (Boolean.TRUE.equals(guard.get())) {
  3. return;
  4. }
  5. try {
  6. guard.set(Boolean.TRUE);
  7. if (!this.started) {
  8. if (statusRepeatCount++ < ALLOWED_REPEATS) {
  9. addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
  10. }
  11. return;
  12. }
  13. if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
  14. return;
  15. }
  16. this.append(eventObject);
  17. } catch (Exception e) {
  18. if (exceptionCount++ < ALLOWED_REPEATS) {
  19. addError("Appender [" + name + "] failed to append.", e);
  20. }
  21. } finally {
  22. guard.set(Boolean.FALSE);
  23. }
  24. }

通过ThreadLocal控制递归导致的重复提交

第8步:ch.qos.logback.core.OutputStreamAppender#append

  1. protected void append(E eventObject) {
  2. if (!isStarted()) {
  3. return;
  4. }
  5. subAppend(eventObject);
  6. }

第9步:ch.qos.logback.core.OutputStreamAppender#subAppend

  1. protected void subAppend(E event) {
  2. if (!isStarted()) {
  3. return;
  4. }
  5. try {
  6. if (event instanceof DeferredProcessingAware) {
  7. // 拼接日志信息(填充占位符),设置当前线程以及MDC等信息
  8. ((DeferredProcessingAware) event).prepareForDeferredProcessing();
  9. }
  10. byte[] byteArray = this.encoder.encode(event);
  11. writeBytes(byteArray);
  12. } catch (IOException ioe) {
  13. this.started = false;
  14. addStatus(new ErrorStatus("IO failure in appender", this, ioe));
  15. }
  16. }

Encoder在这里惨淡登场,返回byte数组。

第10步:ch.qos.logback.core.encoder.LayoutWrappingEncoder#encode

  1. public byte[] encode(E event) {
  2. String txt = layout.doLayout(event);
  3. return convertToBytes(txt);
  4. }

Encoder先把LoggerEvent交给Layout,Layout组装日志信息,在每条信息后加上换行符。

第11步:ch.qos.logback.core.OutputStreamAppender#writeBytes

  1. private void writeBytes(byte[] byteArray) throws IOException {
  2. if(byteArray == null || byteArray.length == 0)
  3. return;
  4. lock.lock();
  5. try {
  6. this.outputStream.write(byteArray);
  7. if (immediateFlush) {
  8. this.outputStream.flush();
  9. }
  10. } finally {
  11. lock.unlock();
  12. }
  13. }

使用AQS锁控制并发问题。这也是Logback性能不如 Log4j2的原因。后面有时间分析一下Log4j2。

本文到此结束了,还有两天就要放假了,祝大家新年快乐。

Logback源码分析的更多相关文章

  1. Spring Security 源码分析(四):Spring Social实现微信社交登录

    社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ.人人网.开心网.新浪微博.搜狐微博.腾讯微博.淘宝.豆瓣.MSN.Google等社会化媒体账号登录该网站. 前言 ...

  2. java 日志体系(四)log4j 源码分析

    java 日志体系(四)log4j 源码分析 logback.log4j2.jul 都是在 log4j 的基础上扩展的,其实现的逻辑都差不多,下面以 log4j 为例剖析一下日志框架的基本组件. 一. ...

  3. supervisor启动worker源码分析-worker.clj

    supervisor通过调用sync-processes函数来启动worker,关于sync-processes函数的详细分析请参见"storm启动supervisor源码分析-superv ...

  4. commons-logging + log4j源码分析

    分析之前先理清楚几个概念 Log4J = Log For Java SLF4J = Simple Logging Facade for Java 看到Facade首先想到的就是设计模式中的门面(Fac ...

  5. DolphinScheduler1.2.1源码分析

    DolphinScheduler在2020年2月24日发布了新版本1.2.1,从版本号就可以看出,这是一个小版本.主要涉及BUG修复.功能增强.新特性三个方面,我们会根据其发布内容,做简要的源码分析. ...

  6. DolphinScheduler源码分析之任务日志

    DolphinScheduler源码分析之任务日志 任务日志打印在调度系统中算是一个比较重要的功能,下面就简要分析一下其打印的逻辑和前端页面查询的流程. AbstractTask 所有的任务都会继承A ...

  7. 【Spring源码分析】预备篇

    前言 最新想学习一下Spring源码,开篇博客记录下学习过程,欢迎一块交流学习. 作为预备篇,主要演示搭建一个最简单的Spring项目样例,对Spring进行最基本梳理. 构建一个最简单的spring ...

  8. 精尽Spring Boot源码分析 - 日志系统

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  9. 精尽Spring Boot源码分析 - @ConfigurationProperties 注解的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

随机推荐

  1. H3C OSPF基本配置命令

  2. python模块之包

    包:将解决一类问题的模块放在同一目录下就形成了一个包 为了更好的了解包,我们就模拟创建一个包 import os os.makedirs('glance/api') os.makedirs('glan ...

  3. H3C保存当前配置--用户图示(console)以上

    <H3C>save         //此种保存只默认保存为Startup.cfg ,系统默认是加载此文件 The current configuration will be writte ...

  4. Java 注解与单元测试

    注解 Java注解是在JDK1.5 之后出现的新特性,用来说明程序的,注解的主要作用体现在以下几个方面: 编译检查,例如 @Override 编写文档,java doc 会根据注解生成对应的文档 代码 ...

  5. Vue CLI 介绍安装

    https://cli.vuejs.org/zh/guide/ 介绍 警告 这份文档是对应 @vue/cli 3.x 版本的.老版本的 vue-cli 文档请移步这里. Vue CLI 是一个基于 V ...

  6. Delphi中的Val函数和iif函数(出错的时候,会有索引值)

    在delphi中Val是一个将字符串转换为数字的函数,Val(S; var V; var Code: Integer)第一个参数是要转换的字符串,第二个参数存放转换后的数字,可以是整数或浮点数,第三个 ...

  7. Qt、Vc下用fopen打开中文名字的文件(转换成Unicode后,使用_wfopen函数)

    在做一个Qt项目的时候,完成上传文件时,通过fopen打开文件用来读时发现fopen不能打开中文的文件名,自己在网查找一下,解决方法如下 参考:http://weidaohang.org/wanglu ...

  8. 002.MFC_对话框_静态文本_编辑框

    一.建立 名为dialogAndCtl的MFC工程,并添加如图控件 1.将上方static text 控件 Caption属性设置为在文本框中如数文本,可以统计字符 2.edit control控件属 ...

  9. TCP三次握手、四次挥手详解

    1.TCP报文格式 TCP(Transmission Control Protocol) 传输控制协议.TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接. 我们需要 ...

  10. mysql锁及四种事务隔离级别笔记

    前言 数据库是一个共享资源,为了充分利用数据库资源,发挥数据 库共享资源的特点,应该允许多个用户并行地存取数据库.但这样就会产生多个用户程序并 发存取同一数据的情况,为了避免破坏一致性,所以必须提供并 ...