孤尽T31训练营03异常日志笔记

381 阅读5分钟

听了这3次课发现一些东西

  1. 这几次课包括下几次课都在讲阿里出的《Java开发手册》
  2. 这门课要求做分享笔记,99%都是把老师的课件重新打一遍
  3. 内外院的设置让人很不舒服

说了这么多,我也是其中一员,也是把笔记重新打一遍

这次笔记也结合课程、《Java开发手册》和工作中的经验把觉得重要的记一下

为什么要使用异常、日志

  1. 使用异常、日志为系统保驾护航
  2. 异常要日志来记录
  3. 异常应当描述导致当前异常发生的原因
  4. 根据异常栈快速定位到异常发生的位置
  5. 结合异常描述和异常栈解决异常

关于try...catch...finally

2个结论

  • 在finally中使用return将改变try{}中已经放入方法返回值中的变量值
  • 在finally中修改变量的值并不能改变try{}中已经放入方法返回值中的变量值

举例

public static void main(String[] args) {
  TryDemo td = new TryDemo();
  System.out.println("testTryCatchFinally(finally中无返回语句) 返回值:" + td.testTryCatchFinally1());
  System.out.println("testTryCatchFinally(finally中有返回语句) 返回值:" + td.testTryCatchFinally2());
}

// 在finally中修改变量的值并不能改变try{}中已经放入方法返回值中的变量值
// 在finally中使用return将改变try{}中已经放入方法返回值中的变量值
public String testTryCatchFinally1() {
  String ret = "";
  try {
    ret = "try";
    return ret;
  } catch (Exception e) {
    ret = "catch";
    return ret;
  } finally {
    ret = "finally";
  }
}

public String testTryCatchFinally2() {
  String ret = "";
  try {
    ret = "try";
    return ret;
  } catch (Exception e) {
    ret = "catch";
    return ret;
  } finally {
    ret = "finally";
    return ret;
  }
}

关于try with resource

举例:不同关闭资源的方式

    public static void main(String[] args) throws IOException {
        TryWithResourceDemo demo = new TryWithResourceDemo();
        demo.testCopy1();
        demo.testCopy2();
        demo.testCopy3();
    }

    String sourceFilePath = "pom.xml";
    String outFilePath = "out.txt";
	// 以前关闭资源的方式
    public void testCopy1() throws IOException{
        BufferedInputStream bin = null;
        BufferedOutputStream bout = null;
        try {
            bin = new BufferedInputStream(new FileInputStream(new File(sourceFilePath)));
            bout = new BufferedOutputStream(new FileOutputStream(new File(outFilePath)));
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bin != null) {
                try {
                    bin.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    throw e;
                } finally {
                    if (bout != null) {
                        try {
                            bout.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                            throw e;
                        }
                    }
                }
            }
        }
    }

	// 不正确的多资源关闭方式(使用try with resource)
    public void testCopy2() {
        File file = new File(sourceFilePath);
        System.out.println(file.getAbsolutePath());
        // 注意:有个流没有关闭
        try (FileInputStream fin = new FileInputStream(new File(sourceFilePath));
             GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File(outFilePath)))
        ) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

	// 正确的多资源关闭方式(使用try with resource)
    public void testCopy3() {
        File file = new File(sourceFilePath);
        System.out.println(file.getAbsolutePath());
        try (FileInputStream fin = new FileInputStream(new File(sourceFilePath));
             FileOutputStream fos = new FileOutputStream(new File(outFilePath));
             GZIPOutputStream out = new GZIPOutputStream(fos)
        ) {
            byte[] buffer = new byte[4096];
            int read;
            while ((read = fin.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

关于错误码规约

  • 错误码的功用:系统、人之间的相互沟通

  • 错误码不能直接输出给用户作为提示信息使用

  • 错误码和业务信息要分开(错误码之外 的业务独特信息由error_message来承载,而不是让错误码本身涵盖过多具体业务属性)

关于日志规约

  1. 总是使用Log Facade,而不是具体Log Implementation
  2. 只添加一个 Log Implementation依赖
  3. 具体的日志实现依赖应该设置为optional和使用runtime scope
  4. 如果有必要, 排除依赖的第三方库中的Log Impementation依赖
  5. 避免为不会输出的log付出代价
  6. 日志格式中最好不要使用行号,函数名等字段
  7. log中不要输出稀奇古怪的字符!

日志实践

个人总结,日志涉及到三个部分,日志门面、日志实现、日志适配器,日志门面相当于我们定义的接口,日志实现相当于接口的实现类,日志适配器则可以随意选择一个日志门面和日志实现搭配来实现日志

image-20211101093322094.png

门面JCL + 实现JUL

依赖pom.xml(jul依赖java默认自带)

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

使用JUL

import java.util.logging.Logger;

public class Jul {
	private static final Logger logger = Logger.getLogger(Jul.class.getName());
	public static void main(String[] args) {
		logger.info("jdk logging info: a msg");
	}
}

使用JCL

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JclWithJul {
	private static Log logger = LogFactory.getLog(JclWithJul.class);
	public static void main(String[] args) {
		if (logger.isTraceEnabled()) {
			logger.trace("commons-logging-jcl trace message");
		}
		if (logger.isDebugEnabled()) {
			logger.debug("commons-logging-jcl debug message");
		}
		if (logger.isInfoEnabled()) {
			logger.info("commons-logging-jcl info message");
		}
	}
}

门面JCL + 实现Log4j

依赖pom.xml

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

配置log4j.properties

log4j.rootLogger = trace, console
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} %m%n

使用log4j

import org.apache.log4j.Logger;

public class Log4j {
	private static final Logger logger = Logger.getLogger(Log4j.class);
	public static void main(String[] args) {
		if (logger.isTraceEnabled()) {
			logger.debug("log4j trace message");
		}
		if (logger.isDebugEnabled()) {
			logger.debug("log4j debug message");
		}
		if (logger.isInfoEnabled()) {
			logger.debug("log4j info message");
		}
	}
}

门面SLF4J + 实现Logback

依赖pom.xml

<!--
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.12</version>
</dependency>
-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

配置logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
configuration 根节点
默认属性:scan="true" scanPeriod="60 second" debug="false"
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true.
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟.
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration>
	<!--
	property 设置变量
	使用变量:<contextName>${app_name}</contextName>
	-->
	<property name="log_home" value="/data/logs" />
	<property name="app_name" value="slf4j-logback" />
	<!-- %-4relative [%thread] %-5level %logger{35} - %msg%n -->
	<property name="encoder_pattern"
			  value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />

	<!--
	contextName 设置上下文名称
	默认上下文名称为“default”。但可以使用<contextName>设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改。
	-->
	<contextName>${app_name}</contextName>

	<!--
	timestamp 获取时间戳字符串
	使用:<contextName>${by_second}</contextName>
	<timestamp key="by_second" datePattern="yyyyMMdd'T'HHmmss"/>
	-->

	<!--
	appender 是<configuration>的子节点,是负责写日志的组件
	与顺序有关系,引用appender前必须定义
	name: 日志名称
	class:ch.qos.logback.core.ConsoleAppender 表示写到控制台
	-->
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder><pattern>${encoder_pattern}</pattern></encoder>
	</appender>

	<!--
	appender RollingFileAppender
	fileNamePattern: 必要节点,包含文件名及“%d”转换符,%d”可以包含一个Java.text.SimpleDateFormat指定的时间格式,
			如:%d{yyyy-MM}。如果直接使用 %d,默认格式是 yyyy-MM-dd。RollingFileAppender 的file字节点可有可无,
			通过设置file,可以为活动文件和归档文件指定不同位置,当前日志总是记录到file指定的文件(活动文件),
			活动文件的名字不会改变;如果没设置file,活动文件的名字会根据fileNamePattern 的值,每隔一段时间改变一次。
			“/”或者“\”会被当做目录分隔符。
			${app_name}.%d.log
			${app_name}.%d.%i.log
	例如:每天生产一个日志文件,保存30天的日志文件
	-->
	<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<fileNamePattern>${log_home}/${app_name}.%d.%i.log</fileNamePattern>
			<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
			<maxFileSize>50MB</maxFileSize>
			<maxHistory>30</maxHistory>
		</rollingPolicy>
		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>${encoder_pattern}</pattern>
		</encoder>
		<!-- SizeAndTimeBasedRollingPolicy 1.2.3版本有 1.1.3版本没有 -->
		<!-- TimeBasedRollingPolicy 按照每天生成日志文件-->
		<!--
		FixedWindowRollingPolicy: 根据固定窗口算法重命名文件的滚动策略。有以下子节点:
		<minIndex>:窗口索引最小值。
		<maxIndex>:窗口索引最大值,当用户指定的窗口过大时,会自动将窗口设置为12。
		<fileNamePattern >: 必须包含“%i”例如,假设最小值和最大值分别为1和2,
			命名模式为 mylog%i.log,会产生归档文件mylog1.log和mylog2.log。
			还可以指定文件压缩选项,例如,mylog%i.log.gz 或者 没有log%i.log.zip
		-->

		<!--
		<triggeringPolicy >: 告知 RollingFileAppender 何时激活滚动。
		<triggeringPolicy></triggeringPolicy>
		-->
		<!--
		SizeBasedTriggeringPolicy: 查看当前活动文件的大小,如果超过指定大小会告知RollingFileAppender 触发当前活动文件滚动。只有一个节点:
		<maxFileSize>:这是活动文件的大小,默认值是10MB。
		-->
		<!--<rollingPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
			<!--<maxFileSize>1K</maxFileSize>-->
		<!--</rollingPolicy>-->
	</appender>

	<!--
	logger 用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>
	name:用来指定受此logger约束的某一个包或者具体的某一个类。
	level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
		还有一个特殊值INHERITED或者同义词NULL,代表强制执行上级的级别。
		如果未设置此属性,那么当前logger将会继承上级的级别。
	addtivity:是否向上级logger传递打印信息。默认是true。 root > appender
	-->
	<!--
	将控制demo.log.slf4j包下的所有类的日志的打印,但是并没有设置打印级别,所以继承他的上级<root>的日志级别“DEBUG”。
	没有设置addtivity,默认为true,将此logger的打印信息向上级传递。
	没有设置appender,此logger本身不打印任何信息。
	-->
	<logger name="demo.log.slf4j"/>
	<logger name="Slf4jWithLogbackDemo" level="INFO" additivity="false">
		<appender-ref ref="STDOUT" />
		<appender-ref ref="FILE" />
	</logger>

	<!--
	root 也是<logger>元素,但是它是根logger,被命名为”root”。只有一个level属性
	level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,不能设置为INHERITED或者同义词NULL。默认是DEBUG。
	<root>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger。
	-->
	<!-- 将这个appender(STDOUT)的打印级别设置为DEBUG -->
	<root level="DEBUG">
		<appender-ref ref="STDOUT" />
		<appender-ref ref="FILE" />
	</root>
</configuration>

使用slf4j

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4jWithLogback {
	private static final Logger logger = LoggerFactory.getLogger(Slf4jWithLogback.class);

	public static void main(String[] args) {
		if (logger.isDebugEnabled()) {
			logger.debug("slf4j-logback debug message");
		}
		if (logger.isInfoEnabled()) {
			logger.info("slf4j-logback info message");
		}
		if (logger.isTraceEnabled()) {
			logger.trace("slf4j-logback trace message");
		}

		int max = 100;
		for (int i=0; i<max; i++) {
			logger.info("log info {{}}", i);
		}
	}
}

门面Log4j2-API + Log4j2-core

依赖pom.xml

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.2</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.2</version>
</dependency>

配置log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
	<Appenders>
		<Console name="Console" target="SYSTEM_OUT">
			<PatternLayout pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
		</Console>
	</Appenders>
	<Loggers>
		<Root level="debug">
			<AppenderRef ref="Console" />
		</Root>
	</Loggers>
</Configuration>

使用log4j2

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2 {
	private static final Logger logger = LogManager.getLogger(Log4j2.class);
	public static void main(String[] args) {
		if (logger.isTraceEnabled()) {
			logger.debug("log4j trace message");
		}
		if (logger.isDebugEnabled()) {
			logger.debug("log4j debug message");
		}
		if (logger.isInfoEnabled()) {
			logger.debug("log4j info message");
		}
	}
}

日志框架中的转换

log4j实现 改成 slf4j门面 + logback实现

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
</dependency>

jcl实现 改成 slf4j门面 + logback实现

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.12</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
</dependency>

jul实现 改成 slf4j门面 + logback实现

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-over-slf4j</artifactId>
</dependency>

SpringBoot中的日志

SpringBoot默认使用 门面SLF4J + 实现Logback

image-20211101103037862.png

使用其他日志组合,依赖pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 排除自带的spring-boot-starter-logging -->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 引入slf4j+log4j2 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

image-20211101103009541.png