基於AOP和ThreadLocal實現日誌記錄

EalenXie發表於2020-06-29

基於AOP和ThreadLocal實現的一個日誌記錄的例子

主要功能實現 : 在API每次被請求時,可以在整個方法呼叫鏈路中記錄一條唯一的API請求日誌,可以記錄請求中絕大部分關鍵內容。並且可以自定義實現對日誌收集(直接標準輸出,或寫入到檔案或資料庫)。

比如傳參,響應,請求url,請求方法,clientIp,耗時,請求成功或異常,請求頭等等。

實現的核心為AOP以及ThreadLocal。

  • AOP 會切所有被@Log4a註解的方法,會記錄一個執行緒中唯一一個Log4物件,讀取AOP中的方法資訊(入參,方法等等)
  • 抓取請求的內容和HttpServletRequest中的內容,解析入參。
  • 日誌收集(自定義實現,建議該過程非同步)
  • 記錄無論目標方法成功或失敗,在執行完成後都將對ThreadLocal中的資源進行釋放。

Log4 記錄的內容

欄位 型別 註釋 是否預設記錄
clientIp String 請求客戶端的Ip
reqUrl String 請求地址
headers Object 請求頭部資訊(可選擇記錄) 是,預設記錄user-agent,content-type
type String 操作型別 是,預設值undefined
content StringBuilder 步驟內容資訊 否,方法內容,可使用Log4.step進行內容步驟記錄

Log4a 註解選項說明

欄位 型別 註釋 預設
type String 操作型別 預設值"undefined"
method boolean 是否記錄請求的本地java方法 true
costTime boolean 是否記錄整個方法耗時 true
headers String[] 記錄的header資訊 預設"User-Agent","content-type"
args boolean 是否記錄請求引數 true
respBody boolean 是否記錄響應引數 true
stackTrace boolean 當目標方法發生異常時,是否追加異常堆疊資訊到content false
costTime boolean 是否記錄整個方法耗時 true
collector Class<? extends LogCollector> 指定日誌收集器 預設空的收集器不指定

例子使用說明

@Log4a註解使用

直接在Controller 方法或類上加上註解@Log4a,可以對該Controller中所有方法進行日誌記錄與收集

例如 :


@Log4a(type = "測試API", stackTrace = true)
@RestController
public class DemoController {
    @Resource
    private DemoService demoService;
    /**
     * JSON資料測試
     */
    @PostMapping("/sayHello")
    public ResponseEntity<?> sayHello(@RequestBody Map<String, Object> request) {
        demoService.sayHello(request);
        return ResponseEntity.ok(request);
    }
    /**
     * RequestParam 引數測試
     */
    @PostMapping("/params")
    public ResponseEntity<?> params(@RequestParam Integer a) {
        return ResponseEntity.ok(a);
    }
    /**
     * 無參測試
     */
    @GetMapping("/noArgs")
    public ResponseEntity<?> noArgs() {
        return ResponseEntity.ok().build();
    }
    /**
     * XML 格式資料測試
     */
    @PostMapping(value = "/callXml", consumes = {MediaType.APPLICATION_XML_VALUE})
    public XmlDataDTO  callXml(@RequestBody XmlDataDTO dataDTO) {
        return dataDTO;
    }
    /**
     * 特殊物件測試
     */
    @GetMapping("/callHttpServletRequest")
    public ResponseEntity<?> callHttpServletRequest(HttpServletRequest request) {
        return ResponseEntity.ok().build();
    }
}
Log4.step 記錄詳細步驟內容

這裡呼叫了service方法,Log4.step 方法記錄每一個步驟詳細內容

/**
 * @author EalenXie Created on 2020/1/16 10:49.
 */
@Service
@Slf4j
public class DemoService {
    /**
     * 測試方法, 使用Log4.step記錄步驟
     */
    public void sayHello(Map<String, Object> words) {
        Log4.step("1. 請求來了,執行業務動作");
        log.info("do somethings");
        Log4.step("2. 業務動作執行完成");
    }
}
自定義的全域性日誌收集器

本例中寫了一個最簡單的直接append寫入到檔案中,你可以選擇自定義的方式進行日誌收集(例如寫入到資料庫或者日誌檔案,或日誌收集框架中,這個過程建議非同步處理,可在collect方法上面加入註解@Async)


@Component
public class DemoLogCollector implements LogCollector {
    
    @Override
    public void collect(Log4 log4) throws LogCollectException {
        try {
            File file = new File("D:\\home\\temp\\日誌.txt");
            if (!file.getParentFile().exists()) {
                FileUtils.forceMkdir(file.getParentFile());
            }
            try (FileWriter fw = new FileWriter(file, true)) {
                fw.append(log4.toString());
            }
        } catch (IOException e) {
            throw new LogCollectException(e);
        }
    }
}

測試後 , 可以從 D:\home\temp\日誌.txt中獲取到記錄的日誌內容。

json格式的資料記錄(引數JSON):
{
	"args": {
		"id": 999,
		"value": "content"
	},
	"clientIp": "192.168.1.54",
	"content": "1. 請求來了,執行業務動作\n2. 業務動作執行完成\n",
	"costTime": 2,
	"headers": {
		"User-Agent": "Apache-HttpClient/4.5.10 (Java/11.0.5)",
		"Content-Type": "application/json"
	},
	"logDate": 1593341797293,
	"method": "name.ealen.demo.controller.DemoController#sayHello",
	"reqUrl": "http://localhost:9527/sayHello",
	"respBody": {
		"headers": {},
		"statusCodeValue": 200,
		"body": {
			"id": 999,
			"value": "content"
		},
		"statusCode": "OK"
	},
	"success": true,
	"type": "測試API"
}

XML格式的資料(引數XML):

{
	"args": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><xml><message>1111 </message><username>zhangsan</username></xml>",
	"clientIp": "192.168.1.54",
	"content": "",
	"costTime": 4,
	"headers": {
		"User-Agent": "Apache-HttpClient/4.5.10 (Java/11.0.5)",
		"Content-Type": "application/xml"
	},
	"logDate": 1593394523000,
	"method": "name.ealen.demo.controller.DemoController#callXml",
	"reqUrl": "http://localhost:9527/callXml",
	"respBody": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><xml><message>1111 </message><username>zhangsan</username></xml>",
	"success": true,
	"type": "測試API"
}
form引數格式的資料(以引數鍵值對形式):

{
	"args": "z=11&a=1",
	"clientIp": "192.168.1.54",
	"content": "",
	"costTime": 1,
	"headers": {
		"User-Agent": "Apache-HttpClient/4.5.10 (Java/11.0.5)",
		"Content-Type": "application/x-www-form-urlencoded"
	},
	"logDate": 1593342114342,
	"method": "name.ealen.demo.controller.DemoController#params",
	"reqUrl": "http://localhost:9527/params",
	"respBody": {
		"headers": {},
		"statusCodeValue": 200,
		"body": 1,
		"statusCode": "OK"
	},
	"success": true,
	"type": "測試API"
}

特殊引數格式(目前暫為鍵值對形式,引數預設取物件的toString()方法):

{
	"args": "request=org.apache.catalina.connector.RequestFacade@754f30c3",
	"clientIp": "192.168.1.54",
	"content": "",
	"costTime": 1,
	"headers": {
		"User-Agent": "Apache-HttpClient/4.5.10 (Java/11.0.5)"
	},
	"logDate": 1593342220880,
	"method": "name.ealen.demo.controller.DemoController#callHttpServletRequest",
	"reqUrl": "http://localhost:9527/callHttpServletRequest",
	"respBody": {
		"headers": {},
		"statusCodeValue": 200,
		"body": null,
		"statusCode": "OK"
	},
	"success": true,
	"type": "測試API"
}

Github專案地址 :https://github.com/EalenXie/Log4a

目前暫時專案命名為Log4a(Log for API), 有時間會一直維護和優化。

相關文章