Spring boot學(xué)習(xí)(六)Spring boot實(shí)現(xiàn)AOP記錄操作日志

前言

在實(shí)際的項(xiàng)目中,特別是管理系統(tǒng)中,對(duì)于那些重要的操作我們通常都會(huì)記錄操作日志。比如對(duì)數(shù)據(jù)庫(kù)的CRUD操作,我們都會(huì)對(duì)每一次重要的操作進(jìn)行記錄,通常的做法是向數(shù)據(jù)庫(kù)指定的日志表中插入一條記錄。這里就產(chǎn)生了一個(gè)問(wèn)題,難道要我們每次在 CRUD的時(shí)候都手動(dòng)的插入日志記錄嗎?這肯定是不合適的,這樣的操作無(wú)疑是加大了開(kāi)發(fā)量,而且不易維護(hù),所以實(shí)際項(xiàng)目中總是利用AOP(Aspect Oriented Programming)即面向切面編程這一技術(shù)來(lái)記錄系統(tǒng)中的操作日志。

文章首發(fā)于個(gè)人博客:【http://www.xiongfrblog.cn

日志分類(lèi)

這里我把日志按照面向的對(duì)象不同分為兩類(lèi):

  • 面向用戶(hù)的日志:用戶(hù)是指使用系統(tǒng)的人,這一類(lèi)日志通常記錄在數(shù)據(jù)庫(kù)里邊,并且通常是記錄對(duì)數(shù)據(jù)庫(kù)的一些CRUD操作。
  • 面向開(kāi)發(fā)者的日志:查看這一類(lèi)日志的一般都是開(kāi)發(fā)人員,這類(lèi)日志通常保存在文件或者在控制臺(tái)打印(開(kāi)發(fā)的時(shí)候在控制臺(tái),項(xiàng)目上線之后之后保存在文件中),這一類(lèi)日志主要用于開(kāi)發(fā)者開(kāi)發(fā)時(shí)期和后期維護(hù)時(shí)期定位錯(cuò)誤。

面向不同對(duì)象的日志,我們采用不同的策略去記錄。很容易看出,對(duì)于面向用戶(hù)的日志具有很強(qiáng)的靈活性,需要開(kāi)發(fā)者控制用戶(hù)的哪些操作需要向數(shù)據(jù)庫(kù)記錄日志,所以這一類(lèi)保存在數(shù)據(jù)庫(kù)的日志我們?cè)谑褂?AOP記錄時(shí)用自定義注解的方式去匹配;而面向開(kāi)發(fā)者的日志我們則使用表達(dá)式去匹配就可以了(這里有可能敘述的有點(diǎn)模糊,看了下面去案例將會(huì)很清晰),下面分別介紹兩種日志的實(shí)現(xiàn)。

實(shí)現(xiàn)AOP記錄面向用戶(hù)的日志

接下來(lái)分步驟介紹Spring boot中怎樣實(shí)現(xiàn)通過(guò)AOP記錄操作日志。

添加依賴(lài)

pom.xml文件中添加如下依賴(lài):

<!-- aop依賴(lài) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

修改配置文件

在項(xiàng)目的application.properties文件中添加下面一句配置:

spring.aop.auto=true

這里特別說(shuō)明下,這句話不加其實(shí)也可以,因?yàn)槟J(rèn)就是true,只要我們?cè)?code>pom.xml中添加了依賴(lài)就可以了,這里提出來(lái)是讓大家知道有這個(gè)有這個(gè)配置。

自定義注解

上邊介紹過(guò)了了,因?yàn)檫@類(lèi)日志比較靈活,所以我們需要自定義一個(gè)注解,使用的時(shí)候在需要記錄日志的方法上添加這個(gè)注解就可以了,首先在啟動(dòng)類(lèi)的同級(jí)包下邊新建一個(gè)config包,在這個(gè)報(bào)下邊新建new一個(gè)名為LogAnnotation文件,文件內(nèi)容如下:

package com.web.springbootaoplog.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author Promise
* @createTime 2018年12月18日 下午9:26:25
* @description  定義一個(gè)方法級(jí)別的@log注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}

這里用到的是Java元注解的相關(guān)知識(shí),不清楚相關(guān)概念的朋友可以去這篇博客get一下【傳送門(mén)】。

準(zhǔn)備數(shù)據(jù)庫(kù)日志表以及實(shí)體類(lèi),sql接口,xml文件

既然是向數(shù)據(jù)庫(kù)中插入記錄,那么前提是需要?jiǎng)?chuàng)建一張記錄日志的表,下面給出我的表sql,由于是寫(xiě)樣例,我這里這張表設(shè)計(jì)的很簡(jiǎn)單,大家可以自行設(shè)計(jì)。

CREATE TABLE `sys_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `user_id` int(11) NOT NULL COMMENT '操作員id',
  `user_action` varchar(255) NOT NULL COMMENT '用戶(hù)操作',
  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COMMENT='日志記錄表';

通過(guò)上篇博客介紹的MBG生成相應(yīng)的實(shí)體類(lèi),sql接口文件,以及xml文件,這里不再概述,不清楚的朋友請(qǐng)移步【傳送門(mén)

當(dāng)然還需要?jiǎng)?chuàng)建service接口文件以及接口實(shí)現(xiàn)類(lèi),這里直接給出代碼:

ISysLogServcie.java

package com.web.springbootaoplog.service;

import com.web.springbootaoplog.entity.SysLog;

/**
* @author Promise
* @createTime 2018年12月18日 下午9:29:48
* @description 日志接口
*/
public interface ISysLogService {

    /**
     * 插入日志
     * @param entity
     * @return
     */
    int insertLog(SysLog entity);
}

SysLogServiceImpl.java

package com.web.springbootaoplog.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.web.springbootaoplog.config.Log;
import com.web.springbootaoplog.dao.SysLogMapper;
import com.web.springbootaoplog.entity.SysLog;
import com.web.springbootaoplog.service.ISysLogService;


/**
* @author Promise
* @createTime 2018年12月18日 下午9:30:57
* @description 
*/
@Service("sysLogService")
public class SysLogServiceImpl implements ISysLogService{

    @Autowired
    private SysLogMapper sysLogMapper;
    
    @Override
    public int insertLog(SysLog entity) {
        // TODO Auto-generated method stub
        return sysLogMapper.insert(entity);
    }
}

AOP的切面和切點(diǎn)

準(zhǔn)備上邊的相關(guān)文件后,下面來(lái)介紹重點(diǎn)--創(chuàng)建AOP切面實(shí)現(xiàn)類(lèi),同樣我們這里將該類(lèi)放在config包下,命名為LogAsPect.java,內(nèi)容如下:

package com.web.springbootaoplog.config;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import com.web.springbootaoplog.entity.SysLog;
import com.web.springbootaoplog.service.ISysLogService;


/**
* @author Promise
* @createTime 2018年12月18日 下午9:33:28
* @description 切面日志配置
*/
@Aspect
@Component
public class LogAsPect {
    
    private final static Logger log = org.slf4j.LoggerFactory.getLogger(LogAsPect.class);

    @Autowired
    private ISysLogService sysLogService;
    
    //表示匹配帶有自定義注解的方法
    @Pointcut("@annotation(com.web.springbootaoplog.config.Log)")
    public void pointcut() {}
    
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) {
        Object result =null;
        long beginTime = System.currentTimeMillis();
        
        try {
            log.info("我在目標(biāo)方法之前執(zhí)行!");
            result = point.proceed();
            long endTime = System.currentTimeMillis();
            insertLog(point,endTime-beginTime);
        } catch (Throwable e) {
            // TODO Auto-generated catch block
        }
        return result;
    }
    
    private void insertLog(ProceedingJoinPoint point ,long time) {
        MethodSignature signature = (MethodSignature)point.getSignature();
        Method method = signature.getMethod();
        SysLog sys_log = new SysLog();
        
        Log userAction = method.getAnnotation(Log.class);
        if (userAction != null) {
            // 注解上的描述
            sys_log.setUserAction(userAction.value());
        }
        
        // 請(qǐng)求的類(lèi)名
        String className = point.getTarget().getClass().getName();
        // 請(qǐng)求的方法名
        String methodName = signature.getName();
        // 請(qǐng)求的方法參數(shù)值
        String args = Arrays.toString(point.getArgs());
        
        //從session中獲取當(dāng)前登陸人id
//      Long useride = (Long)SecurityUtils.getSubject().getSession().getAttribute("userid");
        
        Long userid = 1L;//應(yīng)該從session中獲取當(dāng)前登錄人的id,這里簡(jiǎn)單模擬下
        
        sys_log.setUserId(userid);
        
        sys_log.setCreateTime(new java.sql.Timestamp(new Date().getTime()));
        
        log.info("當(dāng)前登陸人:{},類(lèi)名:{},方法名:{},參數(shù):{},執(zhí)行時(shí)間:{}",userid, className, methodName, args, time);
        
        sysLogService.insertLog(sys_log);
    }
}

這里簡(jiǎn)單介紹下關(guān)于AOP的幾個(gè)重要注解:

  • @Aspect:這個(gè)注解表示將當(dāng)前類(lèi)視為一個(gè)切面類(lèi)
  • @Component:表示將當(dāng)前類(lèi)交由Spring管理。
  • @Pointcut:切點(diǎn)表達(dá)式,定義我們的匹配規(guī)則,上邊我們使用@Pointcut("@annotation(com.web.springbootaoplog.config.Log)")表示匹配帶有我們自定義注解的方法。
  • @Around:環(huán)繞通知,可以在目標(biāo)方法執(zhí)行前后執(zhí)行一些操作,以及目標(biāo)方法拋出異常時(shí)執(zhí)行的操作。

我們用到的注解就這幾個(gè),當(dāng)然還有其他的注解,這里我就不一一介紹了,想要深入了解AOP相關(guān)知識(shí)的朋友可以移步官方文檔【傳送門(mén)

下面看一段關(guān)鍵的代碼:

log.info("我在目標(biāo)方法之前執(zhí)行!");
result = point.proceed();
long endTime = System.currentTimeMillis();
insertLog(point,endTime-beginTime);

其中result = point.proceed();這句話表示執(zhí)行目標(biāo)方法,可以看出我們?cè)谶@段代碼執(zhí)行之前打印了一句日志,并在執(zhí)行之后調(diào)用了insertLog()插入日志的方法,并且在方法中我們可以拿到目標(biāo)方法所在的類(lèi)名,方法名,參數(shù)等重要的信息。

測(cè)試控制器

controller包下新建一個(gè)HomeCOntroller.java(名字大家隨意),內(nèi)容如下:

package com.web.springbootaoplog.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.web.springbootaoplog.config.Log;
import com.web.springbootaoplog.entity.SysLog;
import com.web.springbootaoplog.service.ISysLogService;

/**
* @author Promise
* @createTime 2019年1月2日 下午10:35:30
* @description  測(cè)試controller
*/
@Controller
public class HomeController {

    private final static Logger log = org.slf4j.LoggerFactory.getLogger(HomeController.class);
    
    @Autowired
    private ISysLogService logService;

    @RequestMapping("/aop")
    @ResponseBody
    @Log("測(cè)試aoplog")
    public Object aop(String name, String nick) {
        Map<String, Object> map =new HashMap<>();
        log.info("我被執(zhí)行了!");
        map.put("res", "ok");
        return map;
    }
}

定義一個(gè)測(cè)試方法,帶有兩個(gè)參數(shù),并且為該方法添加了我們自定義的@Log注解,啟動(dòng)項(xiàng)目,瀏覽器訪問(wèn)localhost:8080/aop?name=xfr&nick=eran,這時(shí)候查看eclipse控制臺(tái)的部分輸出信息如下:

2019-01-24 22:02:17.682  INFO 3832 --- [nio-8080-exec-1] c.web.springbootaoplog.config.LogAsPect  : 我在目標(biāo)方法之前執(zhí)行!
2019-01-24 22:02:17.688  INFO 3832 --- [nio-8080-exec-1] c.w.s.controller.HomeController          : 我被執(zhí)行了!
2019-01-24 22:02:17.689  INFO 3832 --- [nio-8080-exec-1] c.web.springbootaoplog.config.LogAsPect  : 當(dāng)前登陸人:1,類(lèi)名:com.web.springbootaoplog.controller.HomeController,方法名:aop,參數(shù):[xfr, eran],執(zhí)行時(shí)間:6

可以看到我們成功在目標(biāo)方法執(zhí)行前后插入了一些邏輯代碼,現(xiàn)在再看數(shù)據(jù)庫(kù)里邊的數(shù)據(jù):

在這里插入圖片描述

成功記錄了一條數(shù)據(jù)。

實(shí)現(xiàn)AOP記錄面向開(kāi)發(fā)者的日志

首先這里我列舉一個(gè)使用該方式的應(yīng)用場(chǎng)景,在項(xiàng)目中出現(xiàn)了bug,我們想要知道前臺(tái)的請(qǐng)求是否進(jìn)入了我們控制器中,以及參數(shù)的獲取情況,下面開(kāi)始介紹實(shí)現(xiàn)步驟。

其實(shí)原理跟上邊是一樣的,只是切點(diǎn)的匹配規(guī)則變了而已,而且不用將日志記錄到數(shù)據(jù)庫(kù),打印出來(lái)即可。

首先在LogAsPect.java中定義一個(gè)新的切點(diǎn)表達(dá)式,如下:

@Pointcut("execution(public * com.web.springbootaoplog.controller..*.*(..))")
public void pointcutController() {}

@Pointcut("execution(public * com.web.springbootaoplog.controller..*.*(..))")表示匹配com.web.springbootaoplog.controller包及其子包下的所有公有方法。

關(guān)于這個(gè)表達(dá)式詳細(xì)的使用方法可以移步這里,【傳送門(mén)

再添加匹配到方法時(shí)我們要做的操作:

@Before("pointcutController()")
public void around2(JoinPoint point) {
    //獲取目標(biāo)方法
    String methodNam = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
    
    //獲取方法參數(shù)
    String params = Arrays.toString(point.getArgs());
    
    log.info("get in {} params :{}",methodNam,params);
}

@Before:表示目標(biāo)方法執(zhí)行之前執(zhí)行以下方法體的內(nèi)容。

再在控制器中添加一個(gè)測(cè)試方法:

@RequestMapping("/testaop3")
@ResponseBody
public Object testAop3(String name, String nick) {
    Map<String, Object> map = new HashMap<>();
    
    map.put("res", "ok");
    return map;
}

可以看到這個(gè)方法我們并沒(méi)有加上@Log注解,重啟項(xiàng)目,瀏覽器訪問(wèn)localhost:8080/testaop3?name=xfr&nick=eran,這時(shí)候查看eclipse控制臺(tái)的部分輸出信息如下:

2019-01-24 23:19:49.108  INFO 884 --- [nio-8080-exec-1] c.web.springbootaoplog.config.LogAsPect  : get in com.web.springbootaoplog.controller.HomeController.testAop3 params :[xfr, eran]

打印出了關(guān)鍵日志,這樣我們就能知道是不是進(jìn)入了該方法,參數(shù)獲取是否正確等關(guān)鍵信息。

這里有的朋友或許會(huì)有疑問(wèn)這樣會(huì)不會(huì)與添加了@Log的方法重復(fù)了呢,的確會(huì),所以在項(xiàng)目中我通常都將@Log注解用在了Service層的方法上,這樣也更加合理。

結(jié)語(yǔ)

好了,關(guān)于Aop記錄日志的內(nèi)容就介紹這么多了,下一篇博客再見(jiàn)。bye~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容