前言
许多后台管理系统中需要记录用户的每一步操作,比如:用户的登录、修改订单等,一般情况下我们会在每个业务操作对应的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();
}