一、JAVA常用的日誌打印組件
- log4j、
- logback、
- log4j2、
- java.util.looging;
假設Spring 框架要打印日誌,應該選擇中其中哪一個組件?
發現哪個都不能選,只能基於應用實際使用的日誌組件來。不然日誌打印就會多份。
怎麼找到應用實際使用的日誌組件, Apache Commons Loging 解決了這個問題
1.1、Apache Commons Loging (JCL)
Commons Loging 本身只提供日誌接口,具體實現在運行時動態尋找對應組件,比如:log4j、jdk14looger 等。但這種動態綁定的方式當系統特別寵大的時候會出現綁定失敗的問題。現在比較流行的slf4j 基於靜態綁定的方式解決了這個問題;
1.2、slf4j
slf4j 本身也只提供日誌接口,與commons loging 不同的是其採用在classPath 加入以下jar包來表示具體採用哪種實現 :
• slfj-log4j12.jar (表示指定 log4j)
• slf4j-jdk14.jar(表示指定jdk Looging)
• slf4j-jcl.jar(表示指定jcl)
• log4j-slf4j-impl(表示指定log4j2)
• logback-classic(表示指定logback)
log4j的配置
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--橋接器,將log4j與slf4j連接起來-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
package com.maltose.studyproject.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Author: sgw
* @Date 2019/11/10 16:06
* @Description: Slf4j日誌
**/
public class HelloSlf4j {
private static Logger logger
= LoggerFactory.getLogger(HelloSlf4j.class);
public static void main(String[] args) {
logger.error("abc");
}
}
正常情況下,就會打印下邊的日誌
可是由於spring boot默認採用的是logback打印日誌,所以如果是spring boot項目的話,會報如下的錯
即找到兩個日誌框架(log4j與logback),所以報錯了;
spring boot在spring-boot-starter-logging裏包含了logback框架
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
而spring-boot-starter包含了spring-boot-starter-logging,所以只要使用spring boot
就默認使用了logback作爲日誌框架了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
那麼,想要使用自己的log4j,而不使用spring boot默認的logback,需要使用exclusions節點將spring-boot-starter-logging這個依賴排除掉
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
記住,要加上log4j.properties文件,否則報錯
此時,日誌打印使用的就是log4j了:
1.3、日誌框架升級
之前老項目使用的jcl日誌框架,現在想要換成slf4j作爲日誌框架,如果從頭開始替換,工作量會非常大,此時,只需引入一個jar即可:jcl-over-slf4j.jar
驗證上邊的jcl-over-slf4j.jar
先使用老的jcl打印日誌,添加jcl依賴:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging-api</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
測試代碼:
package com.maltose.studyproject.controller;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @Author: sgw
* @Date 2019/11/10 17:16
* @Description: JCL日誌
**/
public class HelloJcl {
public static void main(String[] args) {
Log logger = LogFactory.getLog(HelloJcl.class);
logger.error("error message jcl");
}
}
結果:
jcl升級日誌框架到slf4j:
在上邊的基礎上,再引入下邊的包
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--log4j2-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.3</version>
</dependency>
<!--slf4j與log4j2橋接器-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.3</version>
</dependency>
<!--併發包,不然log4j2容易報錯-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
添加log4j2.xml:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="error">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%m"/>
</Console>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
還是運行剛纔的代碼:
修改log4j2.xml,看日誌有沒有改變(加上了%d)
日誌變了:
說明現在走的就是slf4j下的log4j2的日誌框架了;
1.4、log4j2.xml文件解讀
有兩大塊,Appenders與Loggers
1.4.1、Appenders
定義輸出源—— 輸出到哪裏去,用什麼方式輸出,Appenders節點下有三種配置方式
方式一:Console
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d %m"/>
</Console>
target=“SYSTEM_OUT”:即System.out.print();
target=“SYSTEM_ERR”:即System.err.print();
PatternLayout :模版匹配文件
<PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>
- %d:時間
- %-5:前邊5個空格
- t:線程名稱
- m:方法名(method)
- C:class名稱
- F:File文件名
- L:Line行數
- n:換行
方式二:File
<File name="file" fileName="logs/all.log" append="false">
<PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>
</File>
fileName:日誌保存的目錄
append:要不要追加日誌 false——不追加
再次執行方法:
方式三:RollingFile 滾動保存
<RollingFile name="rollingfile" fileName="logs/all-2.log" filePattern="logs/$${date:yyyy-MM-dd}/all-%d{yyyy-MM-dd-HH}-%i.zip">
<PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<!--每個日誌文件2MB,夠了2kb就生成新的日誌文件-->
<SizeBasedTriggeringPolicy size="200 MB"/>
</Policies>
<!--最多保存10個日誌文件-->
<DefaultRollverStrategy max="10"/>
</RollingFile>
執行方法後:
1.5、error與info日誌分別保存到不同目錄
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="error">
<!--Appenders,定義輸出源: 輸出到哪裏去,用什麼方式輸出-->
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<!-- <PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>-->
<PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>
</Console>
<File name="file" fileName="logs/all.log" append="false">
<PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>
</File>
<RollingFile name="rollingfile" fileName="logs/all-2.log" filePattern="logs/$${date:yyyy-MM-dd}/all-%d{yyyy-MM-dd-HH}-%i.log">
<PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<!--每個日誌文件2kb,夠了2kb就生成新的日誌文件-->
<SizeBasedTriggeringPolicy size="2 KB"/>
</Policies>
<!--最多保存10個日誌文件-->
<DefaultRollverStrategy max="10"/>
</RollingFile>
<!--單獨將error日誌保存到另一個目錄-->
<RollingFile name="rollingfileError" fileName="logs/error.log" filePattern="logs/$${date:yyyy-MM-dd}-error/all-%d{yyyy-MM-dd-HH}-%i.log">
<PatternLayout pattern="%d [%-5level][%t] %m (%C:%F:%L) %n"/>
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<!--每個日誌文件2kb,夠了2kb就生成新的日誌文件-->
<SizeBasedTriggeringPolicy size="2 KB"/>
</Policies>
<!--最多保存10個日誌文件-->
<DefaultRollverStrategy max="10"/>
</RollingFile>
</Appenders>
<!--具體的日誌配置-->
<Loggers>
<Logger name="com.maltose.studyproject.controller" level="error">
<AppenderRef ref="rollingfileError"/>
</Logger>
<!--如果代碼裏的日誌不符合上邊Logger裏的配置,就會匹配Root裏的配置來打印日誌-->
<Root level="debug">
<AppenderRef ref="Console"/>
<AppenderRef ref="file"/>
<AppenderRef ref="rollingfile"/>
</Root>
</Loggers>
</Configuration>
上邊注意點:
二、系統異常設計規範與原則
系統異常設計的出發點:
- 良好的異常信息展示,開發運維人員能快速定位問題。
- 響應外部調用異常時,應能明確指明是內部異常還是調用條件不滿足導至。
- 響應用戶操作異常時,能友好的提示用戶。
如何做到以上3點?
2.1、首先我們需要對異常進行分類。
分類一、 內部異常
- 資源環境導致(系統環境異常、數據庫連接超時、第三方服務響應超時)
- 第三方服務錯誤響應
- 第三方響應結果錯誤
- 外部傳入參數非法
- 錯誤的編碼邏輯
- 錯誤的配置
- 異常的業務數據(業務數據缺失)
分類二、業務異常
- 用戶操作錯誤
- 業務條件不滿足
2.2、其次需要在系統中正確的捕獲這類異常,並拋出。
2.2.1、方法入參進行合法性驗證。
- 對系統外部提供的接口,是必須要進行參數驗證(必須)
- 系統內部對外層提供接口,進行驗證
- 工具類進行參數驗證
- public 方法要進行驗證
- private 方法(不建議參數驗證)
2.2.2、第三方響應結果合法性驗證
- 獲取第三方法結果後,根據你們的約定進行驗證。
2.2.3、業務處理前,對業務業務前置條件進行驗證
- 業務處理前,驗證業務條件(驗證佘額、驗證這個帳戶有沒有被公安門鎖定)
- 要考慮性能成本(驗證身份證號碼是不是存在的)
2.2.4、業務處理後,對處理結果進行驗證。
- 驗證對方帳戶是不是到帳了,轉出帳戶是不是成功扣款
2.2.5、對於可能會出現異常的代碼進行 try catch 捕獲
- 嘗試恢復處理
- 直接拋出
- 轉換後拋出
2.3、最後 在系統出口統一攔截處理。
統一攔截的目的是確保出去的異常是可控的, 調用方能夠明白的異常信息。
這裏出口是指系統對外統一響應邏輯,一般我們可分三類場景
2.3.1、WEB Response
- 內部異常:引導至異常提示頁。
- 業務異常:返回對應提示消息至前端。
- 未知異常:嘗試進行認別,如果認識不了,轉換成 編碼異常(BUG)。
2.3.2、Http API 接口響應
- 內部異常:返回接口不可用消息。
- 參數錯誤:基於API文檔中的異常列表進行進行響應返回。表明參數非法,需要調用方加強參數合法性驗證
- 業務錯誤:基於掊口約定反回對應code與消息
2.3.3、RPC Service 響應
- 內部異常:返回服務不可用消息
- 參數錯誤:基於接口文檔進行響應,直接返回異常堆棧。
- 業務錯誤 :直接返回異常堆棧。
checkedException 與uncheckedException 聲明原則
- 如果是參數非法拋出,返回結果非法(即軟件BUG) uncheckedException
- 如果你認爲調用方程序員需要有意識地採取措施,那麼拋出檢查型異常(checkedException )。
- 程序產品有明確的條件約束的要求,可聲明檢查型業務異常(checkedException )。
總結異常設計與處理原則
異常的定義技巧
- 基於分包表示異常的分類,不建議使用繼承(service業務異常的話,在service包下新建異常類接口實現Exception(或者在common包下新建各異常的子包,如common下新建serviceException包,systemException包…),所有的service都實現這個接口,這樣service層有異常時,一看日誌就能定位到是service層的異常,比如service層異常日誌會這樣打印:com.maltose.studyproject.service.serviceException)
- 創建異常來類定義業務異常,不建議使用Code來定義
- 使用枚舉來表示業務異常的幾種結果,不建議使用code
2.4、全局異常處理實戰
2.4.1、場景
新增商品的方法,需要接收兩個參數:
price:價格
name:名稱
然後對數據進行校驗:
價格不可以爲空(我們要做的就是:價格爲空時,做統一異常處理)
新增時,自動生成ID,然後隨商品對象一起返回;
2.4.2、代碼
實體類:
package com.leyou.item.pojo;
/**
* @Author: sgw
* @Date 2019/12/15 10:51
* @Description: TODO
**/
@Data
public class Item {
private Integer id;
private String name;
private Long price;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getPrice() {
return price;
}
public void setPrice(Long price) {
this.price = price;
}
}
service:
package com.leyou.item.service;
import com.leyou.item.pojo.Item;
import org.springframework.stereotype.Service;
import java.util.Random;
/**
* @Author: sgw
* @Date 2019/12/15 10:53
* @Description: TODO
**/
@Service
public class ItemService {
public Item saveItem(Item item){
int id=new Random().nextInt(100);
item.setId(id);
return item;
}
}
全局異常處理類:
package com.leyou.common.utils.com.leyou.common.advice;
import com.leyou.common.utils.com.leyou.common.advice.com.leyou.common.exception.LyException;
import com.leyou.common.vo.ExceptionResult;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @Author: sgw
* @Date 2019/12/14 22:20
* @Description: 攔截所有Controller裏的異常
**/
@ControllerAdvice
public class ControllerHanderException {
@ExceptionHandler(LyException.class)
public ResponseEntity<ExceptionResult> handleException(LyException e) {
return ResponseEntity.status(e.getExceptionEnum().getCode()).body(new ExceptionResult(e.getExceptionEnum()));
}
}
service服務引用common(不是微服務的話,則忽略此處):
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
注意,通用異常處理器想生效,靠的是註解@ControllerAdvice,且這個異常處理類寫在了common服務裏,service引用common;所以common裏的異常處理類所在的包名,必須是service掃描的包名完全一致(或者是service掃描包的子包),即下邊兩個包名必須一致:
枚舉:
package com.leyou.common.utils.enums;
/**
* @Author: sgw
* @Date 2019/12/15 11:26
* @Description: 異常枚舉
**/
public enum ExceptionEnum {
//枚舉只能放在做最前邊(相當於創建了一個對象,枚舉類只能自己創建對象,其他類是不可以創建枚舉類的對象的)
PRICE_CANNOT_BE_NULL(400,"價格不能爲空"),
NMAE_CANNOT_BE_NULL(401,"名稱不能爲空"),
ID_CANNOT_BE_NULL(402,"ID不能爲空");
private int code;
private String msg;
ExceptionEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
ExceptionEnum() {
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
自定義異常:
package com.leyou.common.utils.com.leyou.common.advice.com.leyou.common.exception;
import com.leyou.common.utils.enums.ExceptionEnum;
/**
* @Author: sgw
* @Date 2019/12/15 11:22
* @Description: 自定義異常
**/
public class LyException extends RuntimeException{
private ExceptionEnum exceptionEnum;
public LyException() {
}
public LyException(ExceptionEnum exceptionEnum) {
this.exceptionEnum = exceptionEnum;
}
public ExceptionEnum getExceptionEnum() {
return exceptionEnum;
}
public void setExceptionEnum(ExceptionEnum exceptionEnum) {
this.exceptionEnum = exceptionEnum;
}
}
將異常封裝爲自定義格式:
package com.leyou.common.vo;
import com.leyou.common.utils.enums.ExceptionEnum;
/**
* @Author: sgw
* @Date 2019/12/15 12:04
* @Description: 封裝響應結果
**/
public class ExceptionResult {
private int status;
private String message;
private Long timestamp;
public ExceptionResult(ExceptionEnum em) {
this.status = em.getCode();
this.message=em.getMsg();
this.timestamp=System.currentTimeMillis();
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
}
controller拋異常的時候,拋出自定義異常:
package com.leyou.item.web;
import com.leyou.common.utils.com.leyou.common.advice.com.leyou.common.exception.LyException;
import com.leyou.common.utils.enums.ExceptionEnum;
import com.leyou.item.pojo.Item;
import com.leyou.item.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: sgw
* @Date 2019/12/15 10:57
* @Description: TODO
**/
@RestController("/item")
public class ItemController {
@Autowired
private ItemService itemService;
@PostMapping
public ResponseEntity<Item> saveItem(Item item) {
if (item.getPrice() == null) {
throw new LyException(ExceptionEnum.PRICE_CANNOT_BE_NULL);
}
item = itemService.saveItem(item);
return ResponseEntity.status(HttpStatus.CREATED).body(item);
}
}
結果: