国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Spring aop+自定義注解統一記錄用戶行為日志

haobowd / 3145人閱讀

摘要:自定義注解新增日志注解類,注解作用于方法級別,運行時起作用。自定義注解,聲明一種行為,使配置簡化,代碼層面更加簡潔。

寫在前面

本文不涉及過多的Spring aop基本概念以及基本用法介紹,以實際場景使用為主。

場景

我們通常有這樣一個需求:打印后臺接口請求的具體參數,打印接口請求的最終響應結果,以及記錄哪個用戶在什么時間點,訪問了哪些接口,接口響應耗時多長時間等等。這樣做的目的是為了記錄用戶的訪問行為,同時便于跟蹤接口調用情況,以便于出現問題時能夠快速定位問題所在。

最簡單的做法是這樣的:

    @GetMapping(value = "/info")
    public BaseResult userInfo() {
        //1.打印接口入參日志信息,標記接口訪問時間戳
        BaseResult result = mUserService.userInfo();
        //2.打印/入庫 接口響應信息,響應時間等
        return result;
    }

這種做法沒毛病,但是稍微比較敏感的同學就會發覺有以下缺點:

每個接口都充斥著重復的代碼,有沒有辦法提取這部分代碼,做到統一管理呢?答案是使用 Spring aop 面向切面執行這段公共代碼。

充斥著 硬編碼 的味道,有些場景會要求在接口響應結束后,打印日志信息,保存到數據庫,甚至要把日志記錄到elk日志系統等待,同時這些操作要做到可控,有沒有什么操作可以直接聲明即可?答案是使用自定義注解,聲明式的處理訪問日志。

自定義注解

新增日志注解類,注解作用于方法級別,運行時起作用。

@Target({ElementType.METHOD}) //注解作用于方法級別
@Retention(RetentionPolicy.RUNTIME) //運行時起作用
public @interface Loggable {

    /**
     * 是否輸出日志
     */
    boolean loggable() default true;

    /**
     * 日志信息描述,可以記錄該方法的作用等信息。
     */
    String descp() default "";

    /**
     * 日志類型,可能存在多種接口類型都需要記錄日志,比如dubbo接口,web接口
     */
    LogTypeEnum type() default LogTypeEnum.WEB;

    /**
     * 日志等級
     */
    String level() default "INFO";

    /**
     * 日志輸出范圍,用于標記需要記錄的日志信息范圍,包含入參、返回值等。
     * ALL-入參和出參, BEFORE-入參, AFTER-出參
     */
    LogScopeEnum scope() default LogScopeEnum.ALL;

    /**
     * 入參輸出范圍,值為入參變量名,多個則逗號分割。不為空時,入參日志僅打印include中的變量
     */
    String include() default "";

    /**
     * 是否存入數據庫
     */
    boolean db() default true;

    /**
     * 是否輸出到控制臺
     *
     * @return
     */
    boolean console() default true;
}

日志類型枚舉類:

public enum LogTypeEnum {

    WEB("-1"), DUBBO("1"), MQ("2");

    private final String value;

    LogTypeEnum(String value) {
        this.value = value;
    }

    public String value() {
        return this.value;
    }
}

日志作用范圍枚舉類:

public enum LogScopeEnum {

    ALL, BEFORE, AFTER;

    public boolean contains(LogScopeEnum scope) {
        if (this == ALL) {
            return true;
        } else {
            return this == scope;
        }
    }

    @Override
    public String toString() {
        String str = "";
        switch (this) {
            case ALL:
                break;
            case BEFORE:
                str = "REQUEST";
                break;
            case AFTER:
                str = "RESPONSE";
                break;
            default:
                break;
        }
        return str;
    }
}

相關說明已在代碼中注釋,這里不再說明。

使用 Spring aop 重構

引入依賴:

    
            org.aspectj
            aspectjweaver
            1.8.8
        
        
            org.aspectj
            aspectjrt
            1.8.13
        
        
            org.javassist
            javassist
            3.22.0-GA
    

配置文件啟動aop注解,基于類的代理,并且在 spring 中注入 aop 實現類。




    
    
    

    
    
    
     
    

新增 WebLogAspect 類實現

/**
 * 日志記錄AOP實現
 * create by zhangshaolin on 2018/5/1
 */
@Aspect
@Component
public class WebLogAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);

    // 開始時間
    private long startTime = 0L;

    // 結束時間
    private long endTime = 0L;

    /**
     * Controller層切點
     */
    @Pointcut("execution(* *..controller..*.*(..))")
    public void controllerAspect() {
    }

    /**
     * 前置通知 用于攔截Controller層記錄用戶的操作
     *
     * @param joinPoint 切點
     */
    @Before("controllerAspect()")
    public void doBeforeInServiceLayer(JoinPoint joinPoint) {
    }

    /**
     * 配置controller環繞通知,使用在方法aspect()上注冊的切入點
     *
     * @param point 切點
     * @return
     * @throws Throwable
     */
    @Around("controllerAspect()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        // 獲取request
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = servletRequestAttributes.getRequest();

        //目標方法實體
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        boolean hasMethodLogAnno = method
                .isAnnotationPresent(Loggable.class);
        //沒加注解 直接執行返回結果
        if (!hasMethodLogAnno) {
            return point.proceed();
        }

        //日志打印外部開關默認關閉
        String logSwitch = StringUtils.equals(RedisUtil.get(BaseConstants.CACHE_WEB_LOG_SWITCH), BaseConstants.YES) ? BaseConstants.YES : BaseConstants.NO;

        //記錄日志信息
        LogMessage logMessage = new LogMessage();

        //方法注解實體
        Loggable methodLogAnnon = method.getAnnotation(Loggable.class);
        
        //處理入參日志
        handleRequstLog(point, methodLogAnnon, request, logMessage, logSwitch);
        
        //執行目標方法內容,獲取執行結果
        Object result = point.proceed();
        
        //處理接口響應日志
        handleResponseLog(logSwitch, logMessage, methodLogAnnon, result);
        return result;
    }
    
    /**
     * 處理入參日志
     *
     * @param point           切點
     * @param methodLogAnnon  日志注解
     * @param logMessage      日志信息記錄實體
     */
    private void handleRequstLog(ProceedingJoinPoint point, Loggable methodLogAnnon, HttpServletRequest request,
                                 LogMessage logMessage, String logSwitch) throws Exception {

        String paramsText = "";
        //參數列表
        String includeParam = methodLogAnnon.include();
        Map methodParamNames = getMethodParamNames(
                point.getTarget().getClass(), point.getSignature().getName(), includeParam);
        Map params = getArgsMap(
                point, methodParamNames);
        if (params != null) {
            //序列化參數列表
            paramsText = JSON.toJSONString(params);
        }
        logMessage.setParameter(paramsText);
        //判斷是否輸出日志
        if (methodLogAnnon.loggable()
                && methodLogAnnon.scope().contains(LogScopeEnum.BEFORE)
                && methodLogAnnon.console()
                && StringUtils.equals(logSwitch, BaseConstants.YES)) {
            //打印入參日志
            LOGGER.info("【{}】 接口入參成功!, 方法名稱:【{}】, 請求參數:【{}】", methodLogAnnon.descp().toString(), point.getSignature().getName(), paramsText);
        }
        startTime = System.currentTimeMillis();
        //接口描述
        logMessage.setDescription(methodLogAnnon.descp().toString());
        
        //...省略部分構造logMessage信息代碼
    }

    /**
     * 處理響應日志
     *
     * @param logSwitch         外部日志開關,用于外部動態開啟日志打印
     * @param logMessage        日志記錄信息實體
     * @param methodLogAnnon    日志注解實體
     * @param result           接口執行結果
     */
    private void handleResponseLog(String logSwitch, LogMessage logMessage, Loggable methodLogAnnon, Object result) {
        endTime = System.currentTimeMillis();
        //結束時間
        logMessage.setEndTime(DateUtils.getNowDate());
        //消耗時間
        logMessage.setSpendTime(endTime - startTime);
        //是否輸出日志
        if (methodLogAnnon.loggable()
                && methodLogAnnon.scope().contains(LogScopeEnum.AFTER)) {
            //判斷是否入庫
            if (methodLogAnnon.db()) {
                //...省略入庫代碼
            }
            //判斷是否輸出到控制臺
            if (methodLogAnnon.console() 
                    && StringUtils.equals(logSwitch, BaseConstants.YES)) {
                //...省略打印日志代碼
            }
        }
    }
    /**
     * 獲取方法入參變量名
     *
     * @param cls        觸發的類
     * @param methodName 觸發的方法名
     * @param include    需要打印的變量名
     * @return
     * @throws Exception
     */
    private Map getMethodParamNames(Class cls,
                                                    String methodName, String include) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(cls));
        CtMethod cm = pool.get(cls.getName()).getDeclaredMethod(methodName);
        LocalVariableAttribute attr = (LocalVariableAttribute) cm
                .getMethodInfo().getCodeAttribute()
                .getAttribute(LocalVariableAttribute.tag);

        if (attr == null) {
            throw new Exception("attr is null");
        } else {
            Map paramNames = new HashMap<>();
            int paramNamesLen = cm.getParameterTypes().length;
            int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
            if (StringUtils.isEmpty(include)) {
                for (int i = 0; i < paramNamesLen; i++) {
                    paramNames.put(attr.variableName(i + pos), i);
                }
            } else { // 若include不為空
                for (int i = 0; i < paramNamesLen; i++) {
                    String paramName = attr.variableName(i + pos);
                    if (include.indexOf(paramName) > -1) {
                        paramNames.put(paramName, i);
                    }
                }
            }
            return paramNames;
        }
    }

    /**
     * 組裝入參Map
     *
     * @param point       切點
     * @param methodParamNames 參數名稱集合
     * @return
     */
    private Map getArgsMap(ProceedingJoinPoint point,
                           Map methodParamNames) {
        Object[] args = point.getArgs();
        if (null == methodParamNames) {
            return Collections.EMPTY_MAP;
        }
        for (Map.Entry entry : methodParamNames.entrySet()) {
            int index = Integer.valueOf(String.valueOf(entry.getValue()));
            if (args != null && args.length > 0) {
                Object arg = (null == args[index] ? "" : args[index]);
                methodParamNames.put(entry.getKey(), arg);
            }
        }
        return methodParamNames;
    }
}
使用注解的方式處理接口日志

接口改造如下:

    @Loggable(descp = "用戶個人資料", include = "")
    @GetMapping(value = "/info")
    public BaseResult userInfo() {
        return mUserService.userInfo();
    }

可以看到,只添加了注解@Loggable,所有的web層接口只需要添加@Loggable注解就能實現日志處理了,方便簡潔!最終效果如下:

訪問入參,響應日志信息:

用戶行為日志入庫部分信息:

簡單總結

編寫代碼時,看到重復性代碼應當立即重構,杜絕重復代碼。

Spring aop 可以在方法執行前,執行時,執行后切入執行一段公共代碼,非常適合用于公共邏輯處理。

自定義注解,聲明一種行為,使配置簡化,代碼層面更加簡潔。

最后
更多原創文章會第一時間推送公眾號【張少林同學】,歡迎關注!

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/72848.html

相關文章

  • Dubbo定義日志攔截器

    摘要:前言上一篇文章自定義注解統一記錄用戶行為日志記錄了層中通過自定義注解配合自動記錄用戶行為日志的過程。那么按照分布式架構中服務層的調用過程是否也可以實現統一記錄日志自定義日志攔截器可以實現這個需求。 showImg(https://segmentfault.com/img/remote/1460000017824155?w=1024&h=768); 前言 上一篇文章 Spring ao...

    meteor199 評論0 收藏0
  • 手把手教你如何優雅的使用Aop記錄帶參數的復雜Web接口日志

    摘要:幾乎每一個接口被調用后,都要記錄一條跟這個參數掛鉤的特定的日志到數據庫。我最終采用了的方式,采取攔截的請求的方式,來記錄日志。所有打上了這個注解的方法,將會記錄日志。那么如何從眾多可能的參數中,為當前的日志指定對應的參數呢。 前言 不久前,因為需求的原因,需要實現一個操作日志。幾乎每一個接口被調用后,都要記錄一條跟這個參數掛鉤的特定的日志到數據庫。舉個例子,就比如禁言操作,日志中需要記...

    Loong_T 評論0 收藏0
  • 慕課網_《Spring入門篇》學習總結

    摘要:入門篇學習總結時間年月日星期三說明本文部分內容均來自慕課網。主要的功能是日志記錄,性能統計,安全控制,事務處理,異常處理等等。 《Spring入門篇》學習總結 時間:2017年1月18日星期三說明:本文部分內容均來自慕課網。@慕課網:http://www.imooc.com教學示例源碼:https://github.com/zccodere/s...個人學習源碼:https://git...

    Ververica 評論0 收藏0
  • Spring體系常用項目一覽

    摘要:的面向的異常遵從通用的異常層次結構。比如以前常用的框架,現在常用的框架包含許多項目,下面挑一些最常用的出來總結一下。狀態是流程中事件發生的地點,在流程中通過轉移的方式從一個狀態到另一個狀態,流程的當前狀況稱為流程數據。 如今做Java尤其是web幾乎是避免不了和Spring打交道了,但是Spring是這樣的大而全,新鮮名詞不斷產生,學起來給人一種凌亂的感覺,我就在這里總結一下,理順頭緒...

    OnlyLing 評論0 收藏0
  • Spring【DAO模塊】就是這么簡單

    摘要:連接對象執行命令對象執行關閉值得注意的是,對數據庫連接池是有很好的支持的。給我們提供了事務的管理器類,事務管理器類又分為兩種,因為的事務和的事務是不一樣的。 前言 上一篇Spring博文主要講解了如何使用Spring來實現AOP編程,本博文主要講解Spring的DAO模塊對JDBC的支持,以及Spring對事務的控制... 對于JDBC而言,我們肯定不會陌生,我們在初學的時候肯定寫過非...

    NSFish 評論0 收藏0

發表評論

0條評論

haobowd

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<