基于AOP和ThreadLocal实现日志记录

基于AOP和ThreadLocal实现的一个日志记录的例子

主要功能实现 : 在API每次被请求时,可以在整个方法调用链路中记录一条唯一的API请求日志,可以记录请求中绝大部分关键内容。并且可以自定义实现对日志收集(直接标准输出,或写入到文件或数据库)。

比如传参,响应,请求url,请求方法,clientIp,耗时,请求成功或异常,请求头等等。

实现的核心为AOP以及ThreadLocal。

  • AOP 会切所有被<font color='bule'>@AopLog</font >注解的方法,会记录一个线程中唯一一个<font color='bule'>LogData</font>对象,读取AOP中的方法信息(入参,方法等等)
  • 抓取请求的内容和HttpServletRequest中的内容,解析入参。
  • 日志收集(自定义实现,建议该过程异步)
  • 记录无论目标方法成功或失败,在执行完成后都将对ThreadLocal中的资源进行释放。

LogData记录的内容

字段 类型 注释 是否默认记录
clientIp String 请求客户端的Ip
reqUrl String 请求地址
headers Object 请求头部信息(可选择记录) 是,默认记录user-agent,content-type
type String 操作类型 是,默认值undefined
content StringBuilder 步骤内容信息 否,方法内容,可使用LogData.step进行内容步骤记录

AopLog注解选项说明

字段 类型 注释 默认
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> 指定日志收集器 默认空的收集器不指定

例子使用说明

@AopLog注解使用

直接在Controller 方法或类上加上注解<font color='bule'>@AopLog</font>,可以对该Controller中所有方法进行日志记录与收集

例如 :


@AopLog(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();
    }
}
LogData.step 记录详细步骤内容

这里调用了service方法,LogData.step 方法记录每一个步骤详细内容

/**
 * @author EalenXie Created on 2020/1/16 10:49.
 */
@Service
@Slf4j
public class DemoService {
    /**
     * 测试方法, 使用LogData.step记录步骤
     */
    public void sayHello(Map<String, Object> words) {
        LogData.step("1. 请求来了,执行业务动作");
        log.info("do somethings");
        LogData.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/aop-log

项目命名为aop-log, 有时间会一直维护和优化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342