[编码实践]SpringBoot实战:利用Spring AOP实现操作日志审计管理

[编码实践]SpringBoot实战:利用Spring AOP实现操作日志审计管理

设计原则和思路:

  • 元注解方式结合AOP,灵活记录操作日志
  • 能够记录详细错误日志为运营以及审计提供支持
  • 日志记录尽可能减少性能影响
  • 操作描述参数支持动态获取,其他参数自动记录。

1.定义日志记录元注解,

根据业务情况,要求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 paramMap) {
        if (paramMap == null) {
            return;
        }
        StringBuilder params = new StringBuilder();
        for (Map.Entry param : ((Map) 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)往数据库写入日志。

项目pom.xml中增加spring-boot-starter-aop

        <dependency>  
            <groupId>org.springframework.bootgroupId>  
            <artifactId>spring-boot-starter-aopartifactId>  
        dependency>  

具体的日志切点类实现

 
       
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 beginTimeThreadLocal = new NamedThreadLocal("ThreadLocal beginTime");
private static final ThreadLocal logThreadLocal = new NamedThreadLocal("ThreadLocal log");
private static final ThreadLocal currentUserInfo = new NamedThreadLocal("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 paramsMap = request.getParameterMap(); //请求提交的参数
try {
desc = getControllerMethodDescription(request,joinPoint);
} catch (Exception e) {
e.printStackTrace();
}
// debug模式下打印JVM信息。
// long beginTime = beginTimeThreadLocal.get().getTime();//得到线程绑定的局部变量(开始时间)
// long endTime = System.currentTimeMillis(); //2、结束时间
// 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 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 descFormat(String desc){
List list = new ArrayList();
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.

在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实现就不写了。

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方法中添加日志注解

    @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.日志结果展示

这个简单的。

posted on 2018-03-15 16:23 蒋刚毅 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/cay83/p/8574277.html

你可能感兴趣的:([编码实践]SpringBoot实战:利用Spring AOP实现操作日志审计管理)