SpringBoot項目記錄請求日志

在開發(fā)過程中,為了調試及后期維護過程快速排錯都會記錄請求的入?yún)⒁约胺祷刂党谇铮容^常用的方式是借助日志生成器通過硬編碼的方式記錄日志线召,代碼不夠簡潔铺韧、優(yōu)雅。因此缓淹,可以借助AOP來實現(xiàn)日志記錄哈打,無需在代碼中打印日志,并且能夠滿足不同的日志場景下的定制需求讯壶。

日志注解
package com.cube.share.log.annotation;

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * @author cube.li
 * @date 2021/4/3 21:42
 * @description 日志注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ApiLog {

    /**
     * 標題
     */
    String title() default "";

    @AliasFor("title")
    String name() default "";

    /**
     * 日志打印時排除的類型(例如 File),對入?yún)?出參都有效
     */
    Class<?>[] excludes() default {};

    LogLevel level() default LogLevel.DEBUG;

    LogType type() default LogType.BOTH;

    /**
     * 是否開啟
     */
    boolean enable() default true;

    /**
     * 是否打印方法信息
     */
    boolean printMethodInfo() default true;

    /**
     * 是否打印請求信息
     */
    boolean printRequestInfo() default true;

    /**
     * 是否打印耗時
     */
    boolean timeConsumption() default true;

    /**
     * 日志級別
     */
    enum LogLevel {
        DEBUG,
        INFO,
        WARN,
        ERROR
    }

    /**
     * 記日志類型
     */
    enum LogType {
        /**
         * 入?yún)?         */
        PARAM,
        /**
         * 返回值
         */
        RETURN,
        /**
         * 入?yún)?返回值
         */
        BOTH
    }
}

在需要打印日志的方法上增加該注解料仗,該注解內定義了若干參數(shù),可以根據(jù)實際場景給這些參數(shù)賦值伏蚊。

切面和切點

package com.cube.share.log.annotation.aspect;

import com.cube.share.log.annotation.ApiLog;
import com.cube.share.log.util.ApiLogHelper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;

/**
 * @author cube.li
 * @date 2021/4/3 21:58
 * @description 日志切面
 */
@Aspect
@Component
@Slf4j
@ConditionalOnProperty(prefix = "api-log", name = "enable", matchIfMissing = true, havingValue = "true")
public class ApiLogAop {

    @Resource
    private ApiLogHelper logHelper;

    @Pointcut("@annotation(com.cube.share.log.annotation.ApiLog)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object log(@NonNull ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //獲取此方法上的注解
        ApiLog apiLog = method.getAnnotation(ApiLog.class);

        if (!apiLog.enable()) {
            return point.proceed();
        }

        switch (apiLog.level()) {
            case DEBUG:
                if (!log.isDebugEnabled()) {
                    return point.proceed();
                }
                break;
            case INFO:
                if (!log.isInfoEnabled()) {
                    return point.proceed();
                }
                break;
            case WARN:
                if (!log.isWarnEnabled()) {
                    return point.proceed();
                }
                break;
            case ERROR:
                if (!log.isErrorEnabled()) {
                    return point.proceed();
                }
                break;
            default:
                break;
        }

        //記錄時間
        long start = System.currentTimeMillis(), end;

        logHelper.logBeforeProceed(apiLog, method, point.getArgs());

        Object result = point.proceed();
        end = System.currentTimeMillis();

        logHelper.logAfterProceed(apiLog, result, end - start);
        return result;
    }
}

定義日志切面立轧,并且將日志注解作為切入點,只要請求方法上具有該日志注解躏吊,即可自動進行日志記錄氛改。

打印日志的工具類
package com.cube.share.log.util;

import com.cube.share.base.utils.IpUtil;
import com.cube.share.log.annotation.ApiLog;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.MessageFormat;

/**
 * @author cube.li
 * @date 2021/4/3 22:05
 * @description 日志打印
 */
@Component
@Slf4j
public class ApiLogHelper {

    @Resource
    private HttpServletRequest request;

    private static final String SEPARATOR = " | ";

    private static final String REQUEST_INFO = "###請求信息###: URI:{0},Content-Type:{1},請求IP:{2}";

    private static final String METHOD_INFO = "###方法信息###: 方法名稱:{0}";

    /**
     * 記錄在執(zhí)行proceed()方法之前的日志,包括:
     * 方法信息
     * 請求信息
     * 參數(shù)信息
     *
     * @param apiLog 注解
     * @param method 方法
     * @param args   方法參數(shù)
     */
    public void logBeforeProceed(ApiLog apiLog, Method method, Object[] args) {
        StringBuilder content = new StringBuilder("######日志######\n");
        content.append("Title:").append(StringUtils.isEmpty(apiLog.title()) ? apiLog.name() : apiLog.title()).append("\n");
        if (apiLog.printRequestInfo()) {
            content.append(MessageFormat.format(REQUEST_INFO, request.getRequestURI(),
                    request.getContentType(),
                    IpUtil.getIpAddress(request)))
                    .append("\n");
        }
        if (apiLog.printMethodInfo()) {
            content.append(MessageFormat.format(METHOD_INFO,
                    method.getDeclaringClass().getSimpleName() + SEPARATOR + method.getName()))
                    .append("\n");
        }
        if (apiLog.type() == ApiLog.LogType.RETURN) {
            content.append("參數(shù)打印未啟用!\n");
        } else {
            //排除類型
            Class<?>[] excludes = apiLog.excludes();
            content.append(getParamContent(args, excludes));
        }
        print(content.toString(), apiLog.level());
    }

    private StringBuilder getParamContent(Object[] args, Class<?>[] excludes) {
        StringBuilder paramContent = new StringBuilder("###參數(shù)信息###: ");
        for (Object arg : args) {
            if (arg == null) {
                continue;
            }
            if (exclude(arg.getClass(), excludes)) {
                paramContent.append("#排除的參數(shù)類型:").append(arg.getClass()).append(SEPARATOR);
            } else {
                paramContent.append("#參數(shù)類型:").append(arg.getClass())
                        .append(" ").append("參數(shù)值:")
                        .append(arg.toString())
                        .append(SEPARATOR);
            }
        }
        return paramContent;
    }

    /**
     * 判斷指定類型是否需要排除
     *
     * @param target   指定類型
     * @param excludes 需要排除的類型集合
     * @return 排除:true
     */
    private boolean exclude(@Nullable Class<?> target, @NonNull Class<?>[] excludes) {
        if (ArrayUtils.isEmpty(excludes) || target == null) {
            return false;
        }

        for (Class<?> clazz : excludes) {
            if (clazz.equals(target)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 記錄在執(zhí)行proceed()方法之后的日志,包括:
     * 返回值信息
     * 執(zhí)行耗時
     *
     * @param apiLog          注解
     * @param result          返回結果
     * @param timeConsumption 耗時
     */
    public void logAfterProceed(ApiLog apiLog, Object result, long timeConsumption) {
        StringBuilder content = new StringBuilder("###返回值信息###: ");
        if (apiLog.type() == ApiLog.LogType.PARAM) {
            content.append("未啟用返回值打印");
        } else {
            content.append(getReturnContent(result, apiLog.excludes()));
        }

        if (apiLog.timeConsumption()) {
            content.append("執(zhí)行耗時:").append(timeConsumption).append("MS");
        } else {
            content.append("未啟用方法耗時打印");
        }
        print(content.toString(), apiLog.level());
    }

    private StringBuilder getReturnContent(@Nullable Object result, @NonNull Class<?>[] excludes) {
        StringBuilder content = new StringBuilder();
        try {
            if (result == null) {
                content.append("null");
                return content;
            }
            Class<?> clazz = result.getClass();
            if (exclude(clazz, excludes)) {
                content.append("被排除的類型:").append(clazz.getSimpleName());
            } else {
                content.append("返回值類型:").append(clazz.getSimpleName())
                        .append(SEPARATOR).append("返回值:").append(new ObjectMapper().writeValueAsString(result));
            }
            content.append("\n");
        } catch (JsonProcessingException e) {
            log.error("Java對象轉Json字符串失敗!");
        }
        return content;
    }


    /**
     * 打印日志
     *
     * @param content 日志內容
     * @param level   日志級別
     */
    public void print(String content, ApiLog.LogLevel level) {

        switch (level) {
            case DEBUG:
                log.debug(content);
                break;
            case INFO:
                log.info(content);
                break;
            case WARN:
                log.warn(content);
                break;
            case ERROR:
                log.error(content);
                break;
            default:
                break;
        }
    }
}
測試
package com.cube.share.log.controller;

import com.cube.share.base.templates.ApiResult;
import com.cube.share.log.annotation.ApiLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

/**
 * @author cube.li
 * @date 2021/4/4 10:49
 * @description
 */
@RestController
@RequestMapping("/log")
@Slf4j
public class LogController {

    @GetMapping("/info")
    @ApiLog(level = ApiLog.LogLevel.INFO)
    public ApiResult info(@RequestParam("name") String name, @RequestParam("id") Integer id) {
        return ApiResult.success();
    }

    @PostMapping("/upload")
    @ApiLog(excludes = Integer.class)
    public ApiResult upload(MultipartFile file, @RequestParam("fileName") String fileName, @RequestParam("id") Integer id) {
        return ApiResult.success();
    }

    @ApiLog(level = ApiLog.LogLevel.ERROR, title = "標題")
    @RequestMapping("/error")
    public void error() {
    }
}

調用info方法,日志打印如下:

2021-04-04 12:54:08.390  INFO 11580 --- [nio-9876-exec-3] com.cube.share.log.util.ApiLogHelper     : ######日志######
Title:
###請求信息###: URI:/log/info,Content-Type:null,請求IP:127.0.0.1
###方法信息###: 方法名稱:LogController | info
###參數(shù)信息###: #參數(shù)類型:class java.lang.String 參數(shù)值:li | #參數(shù)類型:class java.lang.Integer 參數(shù)值:4 | 
2021-04-04 12:54:08.395  INFO 11580 --- [nio-9876-exec-3] com.cube.share.log.util.ApiLogHelper     : ###返回值信息###: 返回值類型:ApiResult | 返回值:{"code":200,"msg":null,"data":null}
執(zhí)行耗時:1MS

調用upload方法比伏,日志打印如下:

2021-04-04 12:54:42.860 DEBUG 11580 --- [nio-9876-exec-4] com.cube.share.log.util.ApiLogHelper     : ######日志######
Title:
###請求信息###: URI:/log/upload,Content-Type:multipart/form-data; boundary=--------------------------112997737393373574958179,請求IP:127.0.0.1
###方法信息###: 方法名稱:LogController | upload
###參數(shù)信息###: #參數(shù)類型:class java.lang.String 參數(shù)值:文件名 | #排除的參數(shù)類型:class java.lang.Integer | 
2021-04-04 12:54:42.861 DEBUG 11580 --- [nio-9876-exec-4] com.cube.share.log.util.ApiLogHelper     : ###返回值信息###: 返回值類型:ApiResult | 返回值:{"code":200,"msg":null,"data":null}
執(zhí)行耗時:1MS

由于注解中設置了排除Integer類型平窘,因此參數(shù)id并未被打印

調用error方法,日志打印如下:

021-04-04 12:54:59.242 ERROR 11580 --- [nio-9876-exec-6] com.cube.share.log.util.ApiLogHelper     : ######日志######
Title:標題
###請求信息###: URI:/log/error,Content-Type:null,請求IP:127.0.0.1
###方法信息###: 方法名稱:LogController | error
###參數(shù)信息###: 
2021-04-04 12:54:59.242 ERROR 11580 --- [nio-9876-exec-6] com.cube.share.log.util.ApiLogHelper     : ###返回值信息###: null執(zhí)行耗時:0MS
@ConditionalOnProperty注解

這里順帶提一下@ConditionalOnProperty注解凳怨,這個注解定義如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {

    // 數(shù)組,獲取對應property名稱的值是鬼,與name不可同時使用
    String[] value() default {};

    // 配置屬性名稱的前綴
    String prefix() default "";

    // 數(shù)組肤舞,配置屬性完整名稱或部分名稱
    // 可與prefix組合使用,組成完整的配置屬性名稱均蜜,與value不可同時使用
    String[] name() default {};

    // 可與name組合使用李剖,比較獲取到的屬性值與havingValue給定的值是否相同,相同才加載配置
    String havingValue() default "";

    // 缺少該配置屬性時是否可以加載囤耳。如果為true篙顺,沒有該配置屬性時也會正常加載偶芍;反之則不會生效
    boolean matchIfMissing() default false;
}

可以使用@ConditionalOnProperty來配置一個Bean或者切面是否生效,例如德玫,在本文中匪蟀,如果想要使ApiLogAop不生效,則可以在ApiLogAop切面上加上@ConditionalOnProperty(prefix = "api-log", name = "enable", matchIfMissing = true, havingValue = "true")
表示宰僧,如果在配置了api-log.enable屬性且其值為true材彪,則啟用ApiLogAop切面,如果沒有配置則默認為true即啟用琴儿。
在配置文件中增加如下配置:

api-log:
  enable: false

調用如上幾個方法發(fā)現(xiàn)并沒有打印日志段化,即ApiLogAop未生效。
完整代碼:https://gitee.com/li-cube/share/tree/master/log

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末造成,一起剝皮案震驚了整個濱河市显熏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晒屎,老刑警劉巖喘蟆,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異夷磕,居然都是意外死亡履肃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門坐桩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尺棋,“玉大人,你說我怎么就攤上這事绵跷”烀” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵碾局,是天一觀的道長荆残。 經(jīng)常有香客問我,道長净当,這世上最難降的妖魔是什么内斯? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮像啼,結果婚禮上俘闯,老公的妹妹穿的比我還像新娘。我一直安慰自己忽冻,他們只是感情好真朗,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著僧诚,像睡著了一般遮婶。 火紅的嫁衣襯著肌膚如雪蝗碎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天旗扑,我揣著相機與錄音蹦骑,去河邊找鬼。 笑死肩豁,一個胖子當著我的面吹牛脊串,可吹牛的內容都是我干的。 我是一名探鬼主播清钥,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼琼锋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祟昭?” 一聲冷哼從身側響起缕坎,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎篡悟,沒想到半個月后谜叹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡搬葬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年荷腊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片急凰。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡女仰,死狀恐怖,靈堂內的尸體忽然破棺而出抡锈,到底是詐尸還是另有隱情疾忍,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布床三,位于F島的核電站一罩,受9級特大地震影響,放射性物質發(fā)生泄漏撇簿。R本人自食惡果不足惜聂渊,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望四瘫。 院中可真熱鬧汉嗽,春花似錦、人聲如沸莲组。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锹杈。三九已至撵孤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間竭望,已是汗流浹背邪码。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留咬清,地道東北人闭专。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像旧烧,于是被迫代替她去往敵國和親影钉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內容