前言
許多後臺管理系統中需要記錄用戶的每一步操作,比如:用戶的登錄、修改訂單等,一般情況下我們會在每個業務操作對應的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();
}