運用切面實現使用者行為日誌的新增

愛學習的小猴 發表於 2020-11-21
  1. Dao介面實現

  • 業務描述與設計實現

資料層基於業務層的持久化請求,將業務層提交的使用者行為日誌資訊寫入到資料庫。

  • 關鍵程式碼設計與實現

在SysLogDao介面中新增用於實現日誌資訊持久化的方法。關鍵程式碼如下:

void saveObject(SysLog entity)

第二步:在SysLogServiceImpl類中新增,儲存日誌的方法實現。關鍵程式碼如下:

         @Override
        public void saveObject(SysLog entity) {
          sysLogDao.insertObject(entity);
}
  1. 日誌切面Aspect實現

  • 業務描述與設計實現

在日誌切面中,抓取使用者行為資訊,並將其封裝到日誌物件然後傳遞到業務,通過業務層物件對日誌日誌資訊做進一步處理。此部分內容後續結合AOP進行實現(暫時先了解,不做具體實現)。

  • 關鍵程式碼設計與實現

定義日誌切面類物件,通過環繞通知處理日誌記錄操作。關鍵程式碼如下:

package com.cy.pj.common.aspect;

import com.cy.pj.common.annotation.RequiredLog;
import com.cy.pj.common.util.IPUtils;
import com.cy.pj.sys.pojo.SysLog;
import com.cy.pj.sys.pojo.SysUser;
import com.cy.pj.sys.service.SysLogService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.SecurityUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

@Aspect
@Component
public class SysLogAspect {
       @Autowired
       private SysLogService sysLogService;
       @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
       public void doLog(){}

       @Around("doLog()")
       public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
              long t1=System.currentTimeMillis();
              Object result=joinPoint.proceed();
              long t2=System.currentTimeMillis();
              doSaveLog(joinPoint,(t2-t1));
              return result;//目標方法的執行結果
       }
       /**記錄使用者正常行為日誌:
        * username  (登入使用者)
        * ip (通過工具類獲取)
        * operation (一般是通過註解指定或定義)
        * method (目標型別的類全名+方法名)
        * params(執行目標方法時傳入的引數)
        * time (執行目標方法時的耗時時長)
        * createdtime (日誌的記錄時間)
        * */
       private void doSaveLog(ProceedingJoinPoint jointPoint,long time) throws NoSuchMethodException, JsonProcessingException {
          //想要獲取目標物件的操作內容,得先獲取目標物件的的型別,基於型別獲取目標方法,
           //1.獲取使用者行為日誌,IPUtils是一個獲取ip地址工具類,需要外界匯入
          String ip= IPUtils.getIpAddr();
          //2.獲取目標物件型別(為什麼要獲取此型別呢?要基於此型別獲取目標方法)
          Class<?> targetCls=jointPoint.getTarget().getClass();
          System.out.println("targetCls="+targetCls.getName());
          //3獲取目標方法,獲取目標方法會攜帶方法名和引數型別,所以我們要先獲取一個方法簽名
           //3.1獲取方法簽名(儲存了方法資訊的一個物件),通過此簽名獲取目標方法的名字name以及引數型別ParameterTypes
          MethodSignature ms=(MethodSignature) jointPoint.getSignature();
          //3.2DeclaredMethod獲取私有的方法
          Method targetMethod= targetCls.getDeclaredMethod(ms.getName(),ms.getParameterTypes());
          //4.獲取目標方法上的requiredLog註解
          RequiredLog requiredLog=targetMethod.getAnnotation(RequiredLog.class);

          //5.獲取註解中operation屬性的值
          String operation=requiredLog.operation();;//操作名
           //6.目標方法(目標型別的類全名+方法名)
          String method=targetCls.getName()+"."+targetMethod.getName();
           //7.(執行目標方法時傳入的實際引數值),拿出來的引數是一個陣列,需要轉換成字串,
           // 如果傳入的是pojo,會自動轉成json字串格式
          String params= new ObjectMapper().writeValueAsString(jointPoint.getArgs());

          //8.封裝使用者行為日誌
          SysLog log=new SysLog();
          SysUser user= (SysUser)SecurityUtils.getSubject().getPrincipal();
          log.setUsername(user.getUsername());
          log.setIp(ip);
          log.setOperation(operation);
          log.setMethod(method);
          log.setParams(params);
          log.setTime(time);
          log.setCreatedTime(new Date());
          //3.儲存使用者行為日誌
          sysLogService.saveObject(log);
       }
}

方法中用到的ip地址獲取需要提供一個如下的工具類:(不用自己實現,直接用)

package com.cy.pj.common.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

public class IPUtils {
	private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
	public static String getIpAddr() {
		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
			String ip = null;
			try {
				ip = request.getHeader("x-forwarded-for");
				if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
					ip = request.getHeader("Proxy-Client-IP");
				}
			if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
					ip = request.getHeader("WL-Proxy-Client-IP");
				}
				if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
					ip = request.getHeader("HTTP_CLIENT_IP");
				}
				if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
					ip = request.getHeader("HTTP_X_FORWARDED_FOR");
				}
				if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
					ip = request.getRemoteAddr();
				}
			} catch (Exception e) {
				logger.error("IPUtils ERROR ", e);
			}
			return ip;
		}

}

定義註解介面的實現,通過此註解作為切面的切入點

package com.cy.pj.common.annotation;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
      String operation() default "";
}

例如在實現類中加註解實現:

@RequiredLog(operation = "禁用啟用")//此註解描述的方法為日誌切入點方法
    @RequiresPermissions("sys:user:update")//註解中的字串為一個許可權標識
    @Override
    public int validById(Integer id, Integer valid) {
        //1.引數校驗
        //2.修改使用者狀態
        int rows=sysUserDao.validById(id,valid,"admin");//這裡的admin暫時理解為登入使用者
        if(rows==0)
            throw new ServiceException("記錄有可能已經不存在");
        return rows;
    }