[編碼實踐]SpringBoot實戰:利用Spring AOP實現操做日誌審計管理

設計原則和思路:java

  • 元註解方式結合AOP,靈活記錄操做日誌
  • 可以記錄詳細錯誤日誌爲運營以及審計提供支持
  • 日誌記錄儘量減小性能影響
  • 操做描述參數支持動態獲取,其餘參數自動記錄。

1.定義日誌記錄元註解,spring

根據業務狀況,要求description支持動態入參。例:新增應用{applicationName},其中applicationName是請求參數名。
/**
 * 自定義註解 攔截Controller
 * 
 * @author jianggy
 *
 */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemControllerLog {
	/**
	 * 描述業務操做 例:Xxx管理-執行Xxx操做
	 * 支持動態入參,例:新增應用{applicationName},其中applicationName是請求參數名
	 * @return
	 */
	String description() default "";
}

2.定義用於記錄日誌的實體類數據庫

package com.guahao.wcp.core.dal.dataobject;

import com.guahao.wcp.core.utils.StringUtils;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;

/**
 * 日誌類-記錄用戶操做行爲
 *
 * @author lin.r.x
 */
public class OperateLogDO extends BaseDO implements Serializable {
    private static final long serialVersionUID = -4000845735266995243L;

    private String userId;           //用戶ID
    private String userName;         //用戶名
    private String desc;            //日誌描述
    private int isDeleted;           //狀態標識

    private String menuName;         //菜單名稱
    private String remoteAddr;       //請求地址
    private String requestUri;       //URI
    private String method;           //請求方式
    private String params;           //提交參數
    private String exception;        //異常信息
    private String type;             //日誌類型


    public String getType() {
        return StringUtils.isBlank(type) ? type : type.trim();
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getDesc() {
        return StringUtils.isBlank(desc) ? desc : desc.trim();
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getRemoteAddr() {
        return StringUtils.isBlank(remoteAddr) ? remoteAddr : remoteAddr.trim();
    }

    public void setRemoteAddr(String remoteAddr) {
        this.remoteAddr = remoteAddr;
    }

    public String getRequestUri() {
        return StringUtils.isBlank(requestUri) ? requestUri : requestUri.trim();
    }

    public void setRequestUri(String requestUri) {
        this.requestUri = requestUri;
    }

    public String getMethod() {
        return StringUtils.isBlank(method) ? method : method.trim();
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getParams() {
        return StringUtils.isBlank(params) ? params : params.trim();
    }

    public void setParams(String params) {
        this.params = params;
    }

    /**
     * 設置請求參數
     *
     * @param paramMap
     */
    public void setMapToParams(Map<String, String[]> paramMap) {
        if (paramMap == null) {
            return;
        }
        StringBuilder params = new StringBuilder();
        for (Map.Entry<String, String[]> param : ((Map<String, String[]>) paramMap).entrySet()) {
            params.append(("".equals(params.toString()) ? "" : "&") + param.getKey() + "=");
            String paramValue = (param.getValue() != null && param.getValue().length > 0 ? param.getValue()[0] : "");
            params.append(StringUtils.abbr(StringUtils.endsWithIgnoreCase(param.getKey(), "password") ? "" : paramValue, 100));
        }
        this.params = params.toString();
    }

    public String getException() {
        return StringUtils.isBlank(exception) ? exception : exception.trim();
    }

    public void setException(String exception) {
        this.exception = exception;
    }

    public String getUserName() {
        return StringUtils.isBlank(userName) ? userName : userName.trim();
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getMenuName() {
        return menuName;
    }

    public void setMenuName(String menuName) {
        this.menuName = menuName;
    }

    public int getIsDeleted() {
        return isDeleted;
    }

    public void setIsDeleted(int isDeleted) {
        this.isDeleted = isDeleted;
    }

    @Override
    public String toString() {
        return "OperateLogDO{" +
                "userId='" + userId + '\'' +
                ", userName='" + userName + '\'' +
                ", desc='" + desc + '\'' +
                ", isDeleted=" + isDeleted +
                ", menuName='" + menuName + '\'' +
                ", remoteAddr='" + remoteAddr + '\'' +
                ", requestUri='" + requestUri + '\'' +
                ", method='" + method + '\'' +
                ", params='" + params + '\'' +
                ", exception='" + exception + '\'' +
                ", type='" + type + '\'' +
                '}';
    }
}

3.定義日誌AOP切面類,經過logManager.insert(log)往數據庫寫入日誌。json

項目pom.xml中增長spring-boot-starter-aopsession

        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-aop</artifactId>  
        </dependency>  

具體的日誌切點類實現mybatis

 
 
package com.guahao.wcp.gops.home.aop;

import com.greenline.guser.biz.service.dto.UserInfoDTO;
import com.greenline.guser.client.utils.GuserCookieUtil;
import com.guahao.wcp.gops.home.annotation.SystemControllerLog;
import com.guahao.wcp.gops.home.service.DubboService;
import com.guahao.wcp.core.manager.operatelog.LogManager;
import com.guahao.wcp.core.dal.dataobject.OperateLogDO;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
* 系統日誌切點類
*
* @author jianggy
*/
@Aspect
@Component
public class SystemLogAspect {
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
// private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
private static final ThreadLocal<OperateLogDO> logThreadLocal = new NamedThreadLocal<OperateLogDO>("ThreadLocal log");
private static final ThreadLocal<UserInfoDTO> currentUserInfo = new NamedThreadLocal<UserInfoDTO>("ThreadLocal userInfo");

@Autowired(required = false)
private HttpServletRequest request;
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private LogManager logManager;
@Autowired
private DubboService dubboService;

/**
* Controller層切點 註解攔截
*/
@Pointcut("@annotation(com.guahao.wcp.gops.home.annotation.SystemControllerLog)")
public void controllerAspect() {
}

/**
* 方法規則攔截
*/
@Pointcut("execution(* com.guahao.wcp.gops.home.controller.*.*(..))")
public void controllerPointerCut() {
}

/**
* 前置通知 用於攔截Controller層記錄用戶的操做的開始時間
*
* @param joinPoint 切點
* @throws InterruptedException
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws InterruptedException {
// Date beginTime = new Date();
// beginTimeThreadLocal.set(beginTime);
//debug模式下 顯式打印開始時間用於調試
// if (logger.isDebugEnabled()) {
// logger.debug("開始計時: {} URI: {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
// .format(beginTime), request.getRequestURI());
// }
//讀取GuserCookie中的用戶信息
String loginId = GuserCookieUtil.getLoginId(request);
UserInfoDTO userInfo = dubboService.userInfoService.getUserInfoByLoginId(loginId).getDataResult();
currentUserInfo.set(userInfo);
}

/**
* 後置通知 用於攔截Controller層記錄用戶的操做
*
* @param joinPoint 切點
*/
@After("controllerAspect()")
public void doAfter(JoinPoint joinPoint) {
UserInfoDTO userInfo = currentUserInfo.get();
//登入login操做 前置通知時用戶未校驗 因此session中不存在用戶信息
if (userInfo == null) {
String loginId = GuserCookieUtil.getLoginId(request);
userInfo = dubboService.userInfoService.getUserInfoByLoginId(loginId).getDataResult();
if (userInfo == null) {
return;
}
}
Object[] args = joinPoint.getArgs();
System.out.println(args);

String desc = "";
String type = "info"; //日誌類型(info:入庫,error:錯誤)
String remoteAddr = request.getRemoteAddr();//請求的IP
String requestUri = request.getRequestURI();//請求的Uri
String method = request.getMethod(); //請求的方法類型(post/get)
Map<String, String[]> paramsMap = request.getParameterMap(); //請求提交的參數
try {
desc = getControllerMethodDescription(request,joinPoint);
} catch (Exception e) {
e.printStackTrace();
}
// debug模式下打印JVM信息。
// long beginTime = beginTimeThreadLocal.get().getTime();//獲得線程綁定的局部變量(開始時間)
// long endTime = System.currentTimeMillis(); //二、結束時間
// if (logger.isDebugEnabled()) {
// logger.debug("計時結束:{} URI: {} 耗時: {} 最大內存: {}m 已分配內存: {}m 已分配內存中的剩餘空間: {}m 最大可用內存: {}m",
// new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(endTime),
// request.getRequestURI(),
// DateUtils.formatDateTime(endTime - beginTime),
// Runtime.getRuntime().maxMemory() / 1024 / 1024,
// Runtime.getRuntime().totalMemory() / 1024 / 1024,
// Runtime.getRuntime().freeMemory() / 1024 / 1024,
// (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() + Runtime.getRuntime().freeMemory()) / 1024 / 1024);
// }

OperateLogDO log = new OperateLogDO();
log.setDesc(desc);
log.setType(type);
log.setRemoteAddr(remoteAddr);
log.setRequestUri(requestUri);
log.setMethod(method);
log.setMapToParams(paramsMap);
log.setUserName(userInfo.getName());
log.setUserId(userInfo.getLoginId());
// Date operateDate = beginTimeThreadLocal.get();
// log.setOperateDate(operateDate);
// log.setTimeout(DateUtils.formatDateTime(endTime - beginTime));

//1.直接執行保存操做
//this.logService.createSystemLog(log);

//2.優化:異步保存日誌
//new SaveLogThread(log, logService).start();

//3.再優化:經過線程池來執行日誌保存
threadPoolTaskExecutor.execute(new SaveLogThread(log,logManager));
logThreadLocal.set(log);
}

/**
* 異常通知
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "controllerAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
OperateLogDO log = logThreadLocal.get();
if (log != null) {
log.setType("error");
log.setException(e.toString());
new UpdateLogThread(log,logManager).start();
}
}

/**
* 獲取註解中對方法的描述信息 用於Controller層註解
*
* @param joinPoint 切點
* @return 方法描述
*/
public static String getControllerMethodDescription(HttpServletRequest request,JoinPoint joinPoint) throws IllegalAccessException, InstantiationException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SystemControllerLog controllerLog = method
.getAnnotation(SystemControllerLog.class);
String desc = controllerLog.description();
List<String> list = descFormat(desc);
for (String s : list) {
//根據request的參數名獲取到參數值,並對註解中的{}參數進行替換
String value=request.getParameter(s);
desc = desc.replace("{"+s+"}", value);
}
return desc;
}

/**
* 獲取日誌信息中的動態參數
* @param desc
* @return
*/
private static List<String> descFormat(String desc){
List<String> list = new ArrayList<String>();
Pattern pattern = Pattern.compile("\\{([^\\}]+)\\}");
Matcher matcher = pattern.matcher(desc);
while(matcher.find()){
String t = matcher.group(1);
list.add(t);
}
return list;
}
/**
* 保存日誌線程
*
* @author lin.r.x
*/
private static class SaveLogThread implements Runnable {
private OperateLogDO log;
private LogManager logManager;

public SaveLogThread(OperateLogDO log, LogManager logManager) {
this.log = log;
this.logManager = logManager;
}

@Override
public void run() {
logManager.insert(log);
}
}

/**
* 日誌更新線程
*
* @author lin.r.x
*/
private static class UpdateLogThread extends Thread {
private OperateLogDO log;
private LogManager logManager;

public UpdateLogThread(OperateLogDO log, LogManager logManager) {
super(UpdateLogThread.class.getSimpleName());
this.log = log;
this.logManager = logManager;
}

@Override
public void run() {
this.logManager.update(log);
}
}
}

 

4.實現AsyncConfigurer接口並重寫AsyncConfigurer方法,並返回一個ThreadPoolTaskExecutor,這樣咱們就獲得了一個基於線程池的TaskExecutor.app

在Executor配置類中增長@EnableAsync註解,開啓異步支持。異步

package com.guahao.wcp.gops.home.configuration;

import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;

/**
 * @program: wcp
 * @description: 配置類實現AsyncConfigurer接口並重寫AsyncConfigurer方法,並返回一個ThreadPoolTaskExecutor
 * @author: Cay.jiang
 * @create: 2018-03-12 17:27
 **/

//聲明這是一個配置類
@Configuration
//開啓註解:開啓異步支持
@EnableAsync
public class TaskExecutorConfigurer implements AsyncConfigurer {
    private static final Logger log = LoggerFactory.getLogger(TaskExecutorConfigurer.class);
    @Bean
    //配置類實現AsyncConfigurer接口並重寫AsyncConfigurer方法,並返回一個ThreadPoolTaskExecutor
    //這樣咱們就獲得了一個基於線程池的TaskExecutor
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //若是池中的實際線程數小於corePoolSize,不管是否其中有空閒的線程,都會給新的任務產生新的線程
        taskExecutor.setCorePoolSize(5);
        //鏈接池中保留的最大鏈接數。Default: 15 maxPoolSize
        taskExecutor.setMaxPoolSize(10);
        //線程池所使用的緩衝隊列
        taskExecutor.setQueueCapacity(25);
        //等待全部線程執行完
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        taskExecutor.initialize();
        return taskExecutor;
    }
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new WcpAsyncExceptionHandler();
    }
    /**
     * 自定義異常處理類
     * @author hry
     *
     */
    class WcpAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
        //手動處理捕獲的異常
        @Override
        public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
            System.out.println("-------------》》》捕獲到線程異常信息");
            log.info("Exception message - " + throwable.getMessage());
            log.info("Method name - " + method.getName());
            for (Object param : obj) {
                log.info("Parameter value - " + param);
            }
        }

    }
}

 

5.logManager調用日誌DAO操做,具體的mybatis實現就不寫了。ide

package com.guahao.wcp.core.manager.operatelog.impl;

import com.guahao.wcp.core.dal.dataobject.OperateLogDO;
import com.guahao.wcp.core.dal.mapper.OperateLogMapper;
import com.guahao.wcp.core.manager.operatelog.LogManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("logManager")
public class LogManagerImpl implements LogManager {
    

    @Autowired
    private OperateLogMapper operateLogDAO;
    
    @Override
    public int insert(OperateLogDO log) {

        System.out.println("新增操做日誌:"+log);
        return operateLogDAO.insert(log);
    }
    
    @Override
    public int update(OperateLogDO log) {
        //暫不實現
        //return this.logDao.updateByPrimaryKeySelective(log);
        System.out.println("更新操做日誌:"+log);
        return 1;
    }

}

 

6.使用範例ApplicationController方法中添加日誌註解spring-boot

    @RequestMapping(value = "/add.json", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
    @ResponseBody
    @SystemControllerLog (description = "【應用管理】新增應用{applicationName}")
    public BaseJson add(@ModelAttribute("application") ApplicationDO applicationDO, @ModelAttribute("team") TeamDO teamDO) {

.......
}

 

7.日誌數據入庫結果

 

 

 8.日誌結果展現

這個簡單的。

相關文章
相關標籤/搜索