自定義注解&Spring AOP實現日志組件(可重用)

項目需求


**需求1: **web項目一般而言都需要日志記錄,比較常用的是實用log4j來記錄項目的異常日志,將日志單獨存儲于文件當中衍菱,這樣有利于我們快速進行bug 排解涂邀。
**需求2: **異常的記錄一般就是將異常的堆棧保存在文件中,這樣文件大小會急劇上升侍芝,有效異常信息也不能被立即定位研铆,有沒有一種方式可以可以讓我們重寫異常記錄,并保存在異常日志文件當中呢州叠。
**需求3: **在異常日志之上棵红,我們一般還需要對系統(tǒng)中各角色的各個重要操作進行一些日志記錄,以方便我們找到操作人咧栗,以及責任人逆甜。
福利彩蛋

針對1中的需求,我們大家熟悉的是實用log4j進行日志記錄楼熄。配置簡單忆绰,實現快速。
針對2可岂,3中的需求我們一般采用的是基于攔截器實現(Aop思想的一種實現方式)在方法操作之前進行一定的處理错敢,獲取操作人、操作方法名缕粹、操作參數稚茅,異常捕獲與記錄,這樣實現也是完全可以的平斩。
今天記錄的是基于自定義注解面向切面(AOP)進行統(tǒng)一操作日志以及異常日志記錄的實現亚享。

項目代碼


項目中代碼如下所示:
1.首先定義兩個注解:分別為SystemControllerLog(用于攔截Controller層操作注解,起切點表達式作用绘面,明確切面應該從哪里注入),SystemServiceLog(用于攔截Service層操作注解欺税,起切點表達式作用,明確切面應該從哪里注入)
這兩個注解在切面中定義切點表達式的時候會用到揭璃。
SystemControllerLog.java

package com.fxmms.common.log.logannotation;
import java.lang.annotation.*;

/**
 * Created by mark on 16/11/25.
 * @usage 自定義注解晚凿,攔截Controller
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemControllerLog {
    String description() default "";
}

SystemServiceLog

package com.fxmms.common.log.logannotation;
import java.lang.annotation.*;

/**
 * Created by mark on 16/11/25.
 * @usage 自定義注解 攔截service
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemServiceLog {
    String description() default "";
}

2.接下來定義切面類,這里面主要定義了幾個通知瘦馍,在調用被代理對象目標方法前后歼秽,或目標方法拋出異常之后使用。

package com.fxmms.common.log.logaspect;
import com.fxmms.common.log.logannotation.SystemControllerLog;
import com.fxmms.common.log.logannotation.SystemServiceLog;
import com.fxmms.common.security.ScottSecurityUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * 切點類
 * @author tiangai
 * @since 2014-08-05 Pm 20:35
 * @version 1.0
 */
@Aspect
@Component
public class SystemLogAspect {
    //注入Service用于把日志保存數據庫 nodo service 層實現

    //本地異常日志記錄對象
    private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);

    /**
     * Service層切點 使用到了我們定義的 SystemServiceLog 作為切點表達式情组。
     * 而且我們可以看出此表達式基于 annotation燥筷。
     *
     */
    @Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemServiceLog)")
    public void serviceAspect() {
    }

    /**
     * Controller層切點 使用到了我們定義的 SystemControllerLog 作為切點表達式箩祥。
     * 而且我們可以看出此表達式是基于 annotation 的。
     */
    @Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemControllerLog)")
    public void controllerAspect() {
    }

    /**
     * 前置通知 用于攔截Controller層記錄用戶的操作
     *
     * @param joinPoint 連接點
     */
    @Before("controllerAspect()")
    public void doBefore(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //請求的IP
        String ip = request.getRemoteAddr();
        System.out.println(ip+"sdsdsdsdsd");
        try {
            //控制臺輸出
            System.out.println("=====前置通知開始=====");
            Object object = joinPoint.getTarget();
            System.out.println("請求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
            System.out.println("方法描述:" + getControllerMethodDescription(joinPoint));
            System.out.println("請求人:" + ScottSecurityUtil.getLoginName());
            System.out.println("請求IP:" + ip);
            //構造數據庫日志對象

            //保存數據庫

            System.out.println("=====前置通知結束=====");
        } catch (Exception e) {
            //記錄本地異常日志
            logger.error("==前置通知異常==");
            logger.error("異常信息:{}", e.getMessage());
        }
    }

    /**
     * 異常通知 用于攔截service層記錄異常日志
     *
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(pointcut = "serviceAspect()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //獲取請求ip
        String ip = request.getRemoteAddr();
        //獲取用戶請求方法的參數并組織成字符串
        String params = "";
        if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
            for (int i = 0; i < joinPoint.getArgs().length; i++) {
                params += joinPoint.getArgs()[i]+ ",";
            }
        }
        try {
            //控制臺輸出
            System.out.println("=====異常通知開始=====");
            System.out.println("異常代碼:" + e.getClass().getName());
            System.out.println("異常信息:" + e.getMessage());
            System.out.println("異常方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
            System.out.println("方法描述:" + getServiceMthodDescription(joinPoint));
            System.out.println("請求人:" + ScottSecurityUtil.getLoginName());
            System.out.println("請求IP:" + ip);
            System.out.println("請求參數:" + params);
            //構造數據庫日志對象

            //保存數據庫

            System.out.println("=====異常通知結束=====");
        } catch (Exception ex) {
            //記錄本地異常日志
            logger.error("==異常通知異常==");
            logger.error("異常信息:{}", ex);
        }
         //錄本地異常日志
        logger.error("異常方法:{}異常代碼:{}異常信息:{}參數:{}", joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName(), e.getClass().getName(), e.getMessage(), params);
    }

    /**
     * 獲取注解中對方法的描述信息 用于service層注解
     *
     * @param joinPoint 切點
     * @return 方法描述
     * @throws Exception
     */
    public static String getServiceMthodDescription(JoinPoint joinPoint)
            throws Exception {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] arguments = joinPoint.getArgs();
        Class targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        String description = "";
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                Class[] clazzs = method.getParameterTypes();
                if (clazzs.length == arguments.length) {
                    description = method.getAnnotation(SystemServiceLog.class).description();
                    break;
                }
            }
        }
        return description;
    }

    /**
     * 獲取注解中對方法的描述信息 用于Controller層注解
     *
     * @param joinPoint 切點
     * @return 方法描述
     * @throws Exception
     */
    public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] arguments = joinPoint.getArgs();
        Class targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        String description = "";
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                Class[] clazzs = method.getParameterTypes();
                if (clazzs.length == arguments.length) {
                    description = method.getAnnotation(SystemControllerLog.class).description();
                    break;
                }
            }
        }
        return description;
    }
}

上面的切面類中定義了公共切點 serviceAspect肆氓、serviceAspect袍祖,并實現了Controller層的前置通知,Service業(yè)務邏輯層的異常通知做院。
其中預留了保存日志到數據庫的代碼段盲泛,我們可以根據業(yè)務自行進行填充。
3.創(chuàng)建好切點键耕、切面類之后寺滚,如何讓他起作用呢,我們需要在配置文件中進行配置了屈雄。我將web項目中關于不同層的配置文件進行的切割村视,數據訪問層配置文件是data-access.xml、業(yè)務邏輯層是service-application.xml酒奶、控制層是defalut-servlet.xml
首先看defalut-servlet.xml中的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!--開啟aop-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
    <mvc:annotation-driven>
        <!--json解析-->
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <context:component-scan base-package="com.fxmms.www.controller">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <!--掃描日志記錄切面-->
    <context:component-scan base-package="com.fxmms.common.log" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    </context:component-scan>
    <!--配置異常處理器-->
    <context:component-scan base-package="com.fxmms.common.exception_handler" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    </context:component-scan>
    <!--因為web.xml中defaultDispatcherServlet對所有請求進行了攔截蚁孔,所以對一些.css .jpg .html .jsp也進行了攔截,所以此配置項
    保證對對靜態(tài)資源不攔截-->
    <mvc:default-servlet-handler/>
    <!--視圖解析器-->
    <bean id="viewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    <!--配置文件上上傳-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="utf-8"/>
        <property name="maxUploadSize" value="10485760000"/>
        <property name="maxInMemorySize" value="40960"/>
    </bean>
</beans>

注意:以上配置有兩個重要的點:

1.項惋嚎,
2. <aop:aspectj-autoproxy proxy-target-class="true"/>
proxy-target-class="true"默認是false,更改為true時使用的是cglib動態(tài)代理杠氢。這樣只能實現對Controller層的日志記錄。

service-application.xml配置AOP另伍,實現對Service層的日志記錄.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:task="http://www.springframework.org/schema/task"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
  http://www.springframework.org/schema/aop
  http://www.springframework.org/schema/aop/spring-aop.xsd
  http://www.springframework.org/schema/tx
  http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/task
        http://www.springframework.org/schema/task/spring-task.xsd">
    <!--開啟AOP-->
    <aop:aspectj-autoproxy/>

    <!--設置定時任務-->
    <task:annotation-driven/>
    <context:component-scan base-package="com.fxmms.www" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
    </context:component-scan>
    <!--ioc管理切面-->
    <context:component-scan base-package="com.fxmms.common.log" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    </context:component-scan>

    <!-- enable the configuration of transactional behavior based on annotations -->
    <tx:annotation-driven transaction-manager="txManager"/>

    <bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

</beans>

這樣Service也是可以實現操作鼻百、異常日志記錄了。
4.在代碼中使用自定義注解摆尝,相當于在目標方法上設置了一個切點温艇,通過切點注入切面。
Controller層上運用SystemControllerLog注解:
TestNullPointExceptionController.java(驗證Controller層中異常堕汞,Controller中調用Service層代碼)

package com.fxmms.www.controller.admin;

import com.fxmms.common.log.logannotation.SystemControllerLog;
import com.fxmms.www.service.TestExceptionLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Created by mark on 16/11/25.
 */
@Controller
public class TestNullPointExceptionController {
    private static Log logger = LogFactory.getLog(TestNullPointExceptionController.class);
    //自動注入一個Service層類對象
    @Autowired
    TestExceptionLog testExceptionLog;

    @ResponseBody
    @RequestMapping("/admin/testexcption")
    @SystemControllerLog(description = "testException")//使用   SystemControllerLog注解勺爱,此為切點
    public String testException(String str){
        return testExceptionLog.equalStr(str);
    }
}

** TestExceptionLog.java**

package com.fxmms.www.service;

import com.fxmms.common.log.logannotation.SystemServiceLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Service;

/**
 * Created by mark on 16/11/25.
 */
@Service
public class TestExceptionLog {
    private static Log logger = LogFactory.getLog(TestExceptionLog.class);

    @SystemServiceLog(description = "equalstr")
    public String equalStr(String str) {
        str = null;
        if (str.equals("sd")) {
            return "sd";
        }else {
            return "sd";
        }
    }
}

我在其中手動設置str = null,用于模擬前臺輸入讯检。
程序在運行時會報運行時異常琐鲁。
最終啟動項目項目console日志輸出如下圖所示:


日志控制臺輸出
error.log輸出

這樣就完成了自定義注解&Aop&自定義異常&操作日志的記錄,而且自定義的注解與切面可以進行重用人灼,操作日志與異常日志可以進行數據庫記錄绣否,后期甚至可以做一個關于異常分析的系統(tǒng),我們可以直接從日志后臺系統(tǒng)中查看異常出現的頻率挡毅,以及定位異常的發(fā)聲位置,明確操作人等暴构。
完跪呈。

福利彩蛋

職位:騰訊OMG 廣告后臺高級開發(fā)工程師;
Base:深圳;
場景:海量數據段磨,To B,To C耗绿,場景極具挑戰(zhàn)性苹支。
基礎要求:
熟悉常用數據結構與算法;
熟悉常用網絡協(xié)議,熟悉網絡編程;
熟悉操作系統(tǒng)误阻,有線上排查問題經驗;
熟悉MySQL,oracle;
熟悉JAVA债蜜,GoLang,c++其中一種語言均可;
可內推究反,歡迎各位優(yōu)秀開發(fā)道友私信[微笑]
期待關注我的開發(fā)小哥哥寻定,小姐姐們私信我,機會很好精耐,平臺對標抖音狼速,廣告生態(tài)平臺,類似Facebook 廣告平臺卦停,希望你們用簡歷砸我~
聯(lián)系方式 微信 13609184526

博客搬家:大坤的個人博客
歡迎評論哦~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末向胡,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子惊完,更是在濱河造成了極大的恐慌僵芹,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件小槐,死亡現場離奇詭異拇派,居然都是意外死亡,警方通過查閱死者的電腦和手機本股,發(fā)現死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門攀痊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拄显,你說我怎么就攤上這事苟径。” “怎么了躬审?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵棘街,是天一觀的道長。 經常有香客問我承边,道長遭殉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任博助,我火速辦了婚禮险污,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己蛔糯,他們只是感情好拯腮,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚁飒,像睡著了一般动壤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淮逻,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天琼懊,我揣著相機與錄音,去河邊找鬼爬早。 笑死哼丈,一個胖子當著我的面吹牛,可吹牛的內容都是我干的凸椿。 我是一名探鬼主播削祈,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼脑漫!你這毒婦竟也來了髓抑?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤优幸,失蹤者是張志新(化名)和其女友劉穎吨拍,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體网杆,經...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡羹饰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了碳却。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片队秩。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖昼浦,靈堂內的尸體忽然破棺而出馍资,到底是詐尸還是另有隱情,我是刑警寧澤关噪,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布鸟蟹,位于F島的核電站,受9級特大地震影響使兔,放射性物質發(fā)生泄漏建钥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一虐沥、第九天 我趴在偏房一處隱蔽的房頂上張望熊经。 院中可真熱鬧,春花似錦、人聲如沸奈搜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽馋吗。三九已至,卻和暖如春秋秤,著一層夾襖步出監(jiān)牢的瞬間宏粤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工灼卢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绍哎,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓鞋真,卻偏偏與公主長得像崇堰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涩咖,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內容