AOP+多線程異步保存業務操作日誌你會嗎?

前言

許多後臺管理系統中需要記錄用戶的每一步操作,比如:用戶的登錄、修改訂單等,一般情況下我們會在每個業務操作對應的Service中加入日誌然後保存到數據庫。這樣就會在業務層中增加許多跟業務無關的操作日誌保存代碼,這種情況可以使用切面在方法執行的前後動態將操作日誌保存。

多線程異步保存日誌的實現步驟

1.自定義註解

註解主要用來標註哪些方法需要對操作日誌進行保存

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebLog {
    //  操作描述
    String value() default "";
    // true 表示持久化日誌,false表示不持久化
    boolean persistent() default true;
}

2.定義日誌處理接口和默認實現

2.1將日誌的操作抽象到接口中

public interface WebLogHandler<T> {
    /**
     * 處理日誌
     * @param t 日誌對象
     * @param isPersistent 是否需要持久化 true表示需要持久化、false表示不需要
     * @throws LogPersistenceException
     */
    void processLog(T t,boolean isPersistent) throws LogPersistenceException;

    /**
     * 持久化日誌
     * @param t 持久化日誌對象到數據庫
     * @throws LogPersistenceException
     */
    void persistenceLog(T t) throws LogPersistenceException;

    /**
     * 是否需要持久化日誌
     * @param isPersistent true表示需要持久化日誌
     * @return boolean true表示持久化日誌,false表示不持久化日誌
     */
    boolean customerWantsPersistenceLog(boolean isPersistent);
}

2.2 使用抽象類實現日誌操作接口

抽象類中使用了模板方法實現,將持久化方法定義爲抽象方法由具體的子類去實現

public abstract class AbstractWebLogHandler<T> implements WebLogHandler<T>{

    protected ThreadPoolTaskExecutor threadPoolTaskExecutor;

    public AbstractWebLogHandler(ThreadPoolTaskExecutor threadPoolTaskExecutor) {
        this.threadPoolTaskExecutor=threadPoolTaskExecutor;

    }

    /**
     * 處理日誌
     * @param t 日誌對象
     * @param isPersistent 是否持久化
     * @throws LogPersistenceException
     */
    @Override
    public void processLog(T t,boolean isPersistent) throws LogPersistenceException {
        if (customerWantsPersistenceLog(isPersistent)) {
            persistenceLog(t);
        }
    }

    /**
     * 日誌持久化抽象方法,由子類實現
     * @param t 持久化日誌
     * @throws LogPersistenceException
     */
    @Override
    public abstract void persistenceLog(T t) throws LogPersistenceException;

    /**
     * 是否需要持久化日誌
     * @return boolean true持久化日誌,false不持久化日誌
     */
    @Override
    public boolean customerWantsPersistenceLog(boolean isPersistent) {
        return isPersistent;
    }
}

2.3 定義日誌Bean對象

用來暫時保存日誌信息

public class WebLogBean implements Serializable {
    /**
     * 瀏覽器信息
      */
    private String browserInfo;
    /**
     * 請求URL
      */
    private String requestURL;
    /**
     *  請求類型(get、post、put、delete等)
     */
    private String httpMethod;
    /**
     * 客戶端請求IP
     */
    private String requestIP;
    /**
     * 請求參數
     */
    private String requestParam;
    /**
     * 請求參數
     */
    private Object[] requestParams;
    /**
     * 操作的類和方法
     */
    private String operateClassMethod;
    /**
     * 耗時
     */
    private long consumeTime;
    /***
     * 響應結果
     */
    private String responseResult;
    /**
     * 操作描述
     */
    private String operateDesc;
    /**
     * 異常信息
     */
    private String exceptionMsg;
    /**
     * 操作時間
     */
    private Date operateTime;

    @Override
    public String toString() {
        return String.format("WebLogBean[browserInfo='%s',requestURL='%s',httpMethod='%s'," +
                "requestIP='%s',requestParams='%s',operateClassMethod='%s',consumeTime=%d," +
                "responseResult='%s',operateDesc='%s',exceptionMsg='%s',operateTime=%d",
                browserInfo,requestURL,httpMethod,requestIP,requestParams,operateClassMethod,consumeTime,
                responseResult,operateDesc,exceptionMsg,operateTime);
    }
}

2.4 定義Service接口

public interface OperationLogService<T> {
	/**
	 * 保存操作日誌
	 * @param webLogBean
	 */
	void saveOperationLog(WebLogBean webLogBean);
}

2.5 使用線程調用Service進行日誌持久化

public class OperationLogThread<T> implements Runnable {
	private static Logger logger= LoggerFactory.getLogger(OperationLogThread.class);
	private volatile OperationLogService operationLogService;
	private volatile WebLogBean webLogBean;

	public OperationLogThread(OperationLogService operationLogService,
	                           WebLogBean webLogBean) {
		this.operationLogService=operationLogService;
		this.webLogBean=webLogBean;
	}

	@Override
	public void run() {
		try {
			if (logger.isInfoEnabled()) {
				logger.info("thread name " + Thread.currentThread().getName() + " start save operateLog " + JSON.toJSONString(webLogBean));
			}
			this.operationLogService.saveOperationLog(webLogBean);
			if (logger.isInfoEnabled()) {
				logger.info("thread name " + Thread.currentThread().getName() + "save operateLog success ");
			}
		} catch (Exception e) {
			logger.error("thread name "+Thread.currentThread().getName()+" save operateLog error",e);
		}
	}
}

2.5 定義日誌處理默認實現類

定義默認的日誌操作實現類使用線程

@Component
public class DefaultWebLogHandler extends AbstractWebLogHandler<WebLogBean> {

    @Autowired
    OperationLogService operationLogService;

    public DefaultWebLogHandler(ThreadPoolTaskExecutor threadPoolTaskExecutor) {
        super(threadPoolTaskExecutor);
    }

    @Override
    public void persistenceLog(WebLogBean webLogBean) throws LogPersistenceException {
        this.threadPoolTaskExecutor.execute(new OperationLogThread<>(operationLogService,webLogBean));
    }
}

定義日誌切面

在切面中獲取到帶有@WebLog註解標註方法的請求參數並調用WebLogHandler接口的processLog方法處理日誌@Pointcut("@annotation(com.example.log.annotation.WebLog)") 註解表示切面應用的範圍爲註解@WebLog

@Aspect
@Order(10)
@Component
public class WebLogAspect {
    private static final Logger logger=LoggerFactory.getLogger(WebLogAspect.class);
    private ThreadLocal<Long> startTimeThreadLocal=new ThreadLocal<>();
    private ThreadLocal<WebLogBean> webLogBeanThreadLocal=new ThreadLocal<>();
    private ThreadLocal<Boolean> isPersistentThreadLocal=new ThreadLocal<>();
    public static final String TYPE_NAME_SERVLET="org.springframework.security.web.servletapi.HttpServlet3RequestFactory$Servlet3SecurityContextHolderAwareRequestWrapper";
    public static final String UNDERTOW_SERVLET_TYPE_NAME="io.undertow.servlet.spec.HttpServletRequestImpl";
    public static final String APACHE_REQUEST_FACADE="org.apache.catalina.connector.RequestFacade";
    public static final String MOCK_HTTP_SERVLET_REQUEST="org.springframework.mock.web.MockHttpServletRequest";
    /**
     * 日誌處理類
     */
    @Autowired
    private WebLogHandler webLogHandler;
	
	// 切面的範圍爲前面定義的註解
    @Pointcut("@annotation(com.example.log.annotation.WebLog)")
    public void start() {}

    @Before("start()")
    public void before(JoinPoint joinPoint) {
        // 接收到請求,記錄請求內容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        WebLogBean webLogBean=new WebLogBean();
        webLogBean.setBrowserInfo(request.getHeader("User-Agent"));
        webLogBean.setRequestURL(request.getRequestURL().toString());
        webLogBean.setHttpMethod(request.getMethod());
        webLogBean.setRequestIP(request.getRemoteAddr());
        Object[] args=joinPoint.getArgs();
        List<Object> paramList=new ArrayList<>();
        if (args != null && args.length > 0) {
            for (int i=0; i<args.length ;i++) {
                if (args[i] != null) {
                    String typeName = args[i].getClass().getTypeName();
                    if (logger.isInfoEnabled()) {
                        logger.info(" request Parameter TypeName is "+typeName);
                    }
                    if (TYPE_NAME_SERVLET.equals(typeName) || UNDERTOW_SERVLET_TYPE_NAME.equals(typeName) ||
                            APACHE_REQUEST_FACADE.equals(typeName) || MOCK_HTTP_SERVLET_REQUEST.equals(typeName)) {
                        continue;
                    }
                    paramList.add(args[i]);
                }
            }
            String jsonParam=JSON.toJSONString(paramList, SerializerFeature.WriteNullListAsEmpty);
            webLogBean.setRequestParam(jsonParam);
        }
        webLogBean.setOperateClassMethod(joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
        MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature();
        Method method=methodSignature.getMethod();
        WebLog webLog=method.getAnnotation(WebLog.class);
        webLogBean.setOperateDesc(webLog.value());
        if (logger.isInfoEnabled()) {
            logger.info("請求參數:["+webLogBean.toString()+"]");
        }
        startTimeThreadLocal.set(System.currentTimeMillis());
        isPersistentThreadLocal.set(webLog.persistent());
        webLogBeanThreadLocal.set(webLogBean);
    }

    @AfterReturning(pointcut ="start()",returning = "object")
    public void afterReturning(Object object) {
        if (webLogBeanThreadLocal.get()!= null) {
            WebLogBean webLogBean = webLogBeanThreadLocal.get();
            if (startTimeThreadLocal.get() != null) {
                long startTime = startTimeThreadLocal.get();
                webLogBean.setConsumeTime(System.currentTimeMillis() - startTime);
            }
            webLogBean.setResponseResult(JSONObject.toJSONString(object));
            webLogBean.setOperateTime(new Date());
            if (logger.isInfoEnabled()) {
                logger.info("請求處理結束,參數:["+JSON.toJSONString(webLogBean)+"]");
            }
            Boolean persistentFlag=null;
            if (isPersistentThreadLocal.get()!= null) {
                persistentFlag=isPersistentThreadLocal.get();
            }
            webLogHandler.processLog(webLogBean,persistentFlag);
        }
        startTimeThreadLocal.remove();
        webLogBeanThreadLocal.remove();
        isPersistentThreadLocal.remove();
    }

    @AfterThrowing(pointcut = "start()",throwing = "throwable")
    public void afterThrowing(Throwable throwable) {
        logger.error("業務處理髮生異常",throwable);
        if (logger.isInfoEnabled()) {
            logger.info("業務處理髮生異常"+throwable.getMessage());
        }
        if (webLogBeanThreadLocal.get() != null) {
            WebLogBean webLogBean = webLogBeanThreadLocal.get();
            if (startTimeThreadLocal.get()!=null) {
                long startTime = startTimeThreadLocal.get();
                webLogBean.setConsumeTime(System.currentTimeMillis() - startTime);
            }
            webLogBean.setExceptionMsg(throwable.getMessage());
            webLogBean.setOperateTime(new Date());
            if (logger.isInfoEnabled()) {
                logger.info("請求處理異常,參數:["+JSON.toJSONString(webLogBean)+"]");
            }
            boolean persistentFlag=false;
            if (isPersistentThreadLocal.get()!= null) {
                persistentFlag=isPersistentThreadLocal.get();
            }
            webLogHandler.processLog(webLogBean,persistentFlag);
        }
        startTimeThreadLocal.remove();
        webLogBeanThreadLocal.remove();
        isPersistentThreadLocal.remove();
    }
}

配置線程池

@Configuration
@PropertySource(value={"classpath:example-log.properties"})
public class LogConfiguration {

	@Value("${example.log.thread.corePoolSize}")
	private int corePoolSize;

	@Value("${example.log.thread.maxPoolSize}")
	private int maxPoolSize;

	@Bean
	public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
		ThreadPoolTaskExecutor threadPoolTaskExecutor=new ThreadPoolTaskExecutor();
		threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
		threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
		threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
		return threadPoolTaskExecutor;
	}
}

3. 將上述的實現作爲jar包依賴引入到業務系統中使用

<dependency>
            <groupId>common-log</groupId>
            <artifactId>example-log</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

3.1 定義業務系統中的日誌接口

public interface OperateLogService extends OperationLogService<OperateLog> {
    /**
     *
     * @param operateLog
     */
    void createOperateLog(OperateLog operateLog);
}

3.2 定義業務日誌Service實現類保存日誌到數據庫

@Primary表示如果一個接口有多個實現類,優先使用@Primary標註的類

@Primary
@Service
public class OperateLogServiceImpl implements OperateLogService {
    @Autowired
    OperateLogDao operateLogDao;
   
    @Override
    public void saveOperationLog(WebLogBean webLogBean) {
        OperateLog operateLog=new OperateLog();
        BeanUtils.copyProperties(webLogBean,operateLog);
        SessionInfo sessionInfo= SessionInfoThreadLocal.get();
        if(sessionInfo != null) {
            operateLog.setOperator(sessionInfo.getUsername());
            operateLog.setGroupIds(sessionInfo.getGroupIds());
        }
        operateLogDao.insert(operateLog);
    }
}

4. 使用註解標註需要持久化日誌操作

使用@webLog註解在用戶注方法上進行標註

    @WebLog("用戶註冊")
    @PostMapping("/registration")
    public WebResponseResult registerUser(@RequestBody UserCreateParamVo userCreateParamVo,
                                          @RequestParam(required = false) String source) throws Exception {
        UserVo user=new UserVo();
        BeanUtils.copyProperties(userCreateParamVo,user);
        userService.registUser(user,source);
        return WebResponseResult.success();
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章