栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【Spring Boot AOP记录用户操作日志并保存到clickhouse与多数据源的问题】

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【Spring Boot AOP记录用户操作日志并保存到clickhouse与多数据源的问题】

Spring Boot AOP记录用户操作日志并保存到clickhouse与多数据源的问题
  • 前言
  • 一、在Spring框架中,使用AOP配合自定义注解可以方便的实现用户操作的监控。首先搭建一个基本的Spring Boot Web环境开启Spring Boot,然后引入必要依赖:
  • 二、ApiOperation注解
  • 三、创建库表与实体
    • 1.在clickhouse中创建一张t_log表,用于保存用户的操作日志:
    • 2.库表对应的实体
  • 四、保存日志的方法
    • 1、 mapper
    • 2、 使用mybatisplus来操作clickhouse数据库。定义一个IOpenAppIdLogService接口,包含一个保存操作日志的抽象方法:
    • 3、 接口的实现类
    • 4、切面和切点
  • 四、测试
  • 总结


前言

提示:Spring Boot AOP记录用户操作日志并保存到clickhouse中:

记录用户操作日志功能,是使用AOP配合@ApiOperation注解的方式实现用户操作的监控。用户访问指定包包下面的Controller,从请求的参数列表中提取出用户的appId,方法参数。然后在Request中提取出请求的url,请求的方式,ip。将appId,操作内容,耗时,操作的方法,请求的url,请求的方式,操作的时间以及系统返回给用户的应答码等保存到clickhouse中表中。以此完成记录用户操作日志的功能。


提示:以下是本篇文章正文内容

一、在Spring框架中,使用AOP配合自定义注解可以方便的实现用户操作的监控。首先搭建一个基本的Spring Boot Web环境开启Spring Boot,然后引入必要依赖:

pom.xml中引入依赖:

  
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
        
         
            ru.yandex.clickhouse
            clickhouse-jdbc
            0.2.4
        

 
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.2
        
        
 
       
            com.jthinking.common
            ip-info
            2.1.4
        
二、ApiOperation注解

@ApiOperation注解,用于标注需要监控的方法:

    @ApiIgnore
    @RequestMapping(path = "health", method = RequestMethod.GET)
    @ResponseBody
    @ApiOperation(value = "我要监控这个接口", notes = "")
    public RestResp test() {
        return RestResp.ok();
    }
三、创建库表与实体 1.在clickhouse中创建一张t_log表,用于保存用户的操作日志:

代码如下(示例):

CREATE TABLE 库名.t_log
(

    `username` String COMMENT '用户名',

    `operation` String COMMENT '操作内容',

    `time` Decimal(11,
 0) COMMENT '耗时',

    `method` String COMMENT '操作方法',

    `params` String COMMENT '方法参数',

    `request_url` String COMMENT '请求URL',

    `request_method` String COMMENT '请求方式',

    `ip` String COMMENT '操作者IP',

    `create_time` DateTime COMMENT '创建时间',

    `location` String COMMENT '操作地点',

    `response_code` Nullable(Int32) COMMENT '应答码',

    `type` Int32 COMMENT '0系统内部操作日志,
rnrn1系统外部请求日志'
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(create_time)
PRIMARY KEY app_id
ORDER BY (app_id,
 create_time)
TTL create_time + toIntervalDay(365)
SETTINGS index_granularity = 8192
2.库表对应的实体

代码如下(示例):

@Data
@Document
@TableName("t_log")
public class AppIdLog {
    
    @TableField("username")
    private String userName;

    
    @TableField("operation")
    private String operation;

    
    @TableField("time")
    private Long time;

    
    @TableField("method")
    private String method;

    
    @TableField("params")
    private String params;

    
    @TableField("ip")
    private String ip;

    
    @TableField("create_time")
    private Date createTime;

    
    @TableField("location")
    private String location;

    
    @TableField("type")
    private Long type;

    
    @TableField("request_url")
    private String requestUrl;

    
    @TableField("request_method")
    private String requestMethod;

    
    @TableField("response_code")
    private Integer responseCode;
}


四、保存日志的方法

提示:下面的@DS("clickhouse"),@DSTransactional()一定要加上否则的话访问的就是MySQL,而不是clickhouse!!!!!!

1、 mapper
@DS("clickhouse")
public interface AppIdLogMapper extends BaseMapper {
}
2、 使用mybatisplus来操作clickhouse数据库。定义一个IOpenAppIdLogService接口,包含一个保存操作日志的抽象方法:
@DS("clickhouse")
public interface IAppIdLogService extends IService {


    
    @Async(FebsConstant.ASYNC_POOL)
    void saveLog(ProceedingJoinPoint point, Method targetMethod, String ip, String operation, long start, int type, String url, String method);
}
3、 接口的实现类
@Slf4j
@Service
@RequiredArgsConstructor
//如果有多数据源这里需要@DS注解。这样就是显示的指明操作的clickhouse数据库了,而不会去访问MySQL
@DSTransactional()
public class AppIdLogServiceImpl extends ServiceImpl implements IAppIdLogService {


    private final ObjectMapper objectMapper;


    private final OpenAppIdLogMapper openAppIdLogMapper;

    
    @Override
    public void saveLog(ProceedingJoinPoint point, Method targetMethod, String ip, String operation, long start, int type, String url, String method) {
        AppIdLog openappidlog = new AppIdLog();
        openappidlog.setIp(ip);
        //耗时
        openappidlog.setTime(System.currentTimeMillis() - start);
        openappidlog.setOperation(operation);
        String className = point.getTarget().getClass().getName();
        String methodName = targetMethod.getName();
        openappidlog.setMethod(className + "." + methodName + "()");
        Object[] args = point.getArgs();
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = u.getParameterNames(targetMethod);
        if (args != null && paramNames != null) {
            StringBuilder params = new StringBuilder();
            params = handleParams(params, args, Arrays.asList(paramNames),openappidlog);
            openappidlog.setParams(params.toString());
        }

        //获取返回的数据,并且提前其中的code码
        RestResp restResp = null;
        try {
            restResp = (RestResp) point.proceed();
        } catch (Throwable throwable) {
            openappidlog.setResponseCode(null);
            log.error("不能转换为RestResp类型");
            throwable.printStackTrace();
        }

        if (restResp != null) {
            openappidlog.setResponseCode(restResp.getCode());
        }
        //操作的时间
        openappidlog.setCreateTime(new Date());
        //根据ip获取地点
        openappidlog.setLocation( IPInfoUtils.getIpInfo(ip));
        openappidlog.setType((long) type);
        openappidlog.setRequestUrl(url);
        openappidlog.setRequestMethod(method);

        try {
            this.save(openappidlog);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("目标用户日志添加到表中失败:{},用户日志信息:{}", e, openappidlog);
        }

    }

    @SuppressWarnings("all")
    private StringBuilder handleParams(StringBuilder params, Object[] args, List paramNames, OpenAppIdLog openappidlog) {
        try {
            for (int i = 0; i < args.length; i++) {

                if (args[i] instanceof Map) {
                    Set set = ((Map) args[i]).keySet();
                    List list = new ArrayList<>();
                    List paramList = new ArrayList<>();
                    for (Object key : set) {
                        list.add(((Map) args[i]).get(key));
                        paramList.add(key);
                    }
                    return handleParams(params, list.toArray(), paramList, openappidlog);
                } else {
                    if (args[i] instanceof Serializable) {
                        try {
                            Class aClass = args[i].getClass();
                            aClass.getDeclaredMethod("toString", new Class[]{});
                            // 如果不抛出 NoSuchMethodException 异常则存在 toString 方法 ,安全的 writeValueAsString ,否则 走 Object的 toString方法
                            params.append(" ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i]));
                        } catch (NoSuchMethodException e) {
                            params.append(" ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i].toString()));
                        }
                    } else if (args[i] instanceof MultipartFile) {
                        MultipartFile file = (MultipartFile) args[i];
                        params.append(" ").append(paramNames.get(i)).append(": ").append(file.getName());
                    }
                      //如果请求参数是DTO的子类
                      else if (args[i] instanceof DTO) {
                        Class aClass = args[i].getClass();
                        aClass.getSuperclass().getDeclaredMethod("toString", new Class[]{});
                        aClass.getDeclaredMethod("toString", new Class[]{});

                       
                   //获取请求参数的父类,并且获取其中的username属性值
                        Class superclass = aClass.getSuperclass();
                       
                        Field[] declaredFields = superclass.getDeclaredFields();
                        for (Field field:declaredFields){
                            field.setAccessible(true);
                            if (field.getName().contains("username")){
                                String username=(String)field.get(args[i]);
                                openappidlog.setuserName(username);
                            }
                        }
                        params.append(" ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i]));
                    } else {
                        params.append(" ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i]));
                    }
                }
            }
        } catch (Exception ignore) {
            params.append("参数解析失败: args:" + args + " -- paramNames:" + paramNames);
            log.error("参数解析失败:{}", ignore);
            ignore.printStackTrace();
        }
        return params;
    }


}

 
4、切面和切点 
@Aspect
@Slf4j
@Component
public class ControllerEndpointAspect extends BaseAspectSupport {
    @Autowired
    private IAppIdLogService itLogService;

    @Pointcut("execution(* xxx.xxx.xxx.controller..*.*(..)) && @annotation(io.swagger.annotations.ApiOperation)")
    public void pointcut() {
    }
    
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point){
        Object result = null;
        Method targetMethod = resolveMethod(point);
        ApiOperation annotation = targetMethod.getAnnotation(ApiOperation.class);
        String operation = annotation.value();
        long start = System.currentTimeMillis();
        try {
            // 启动目标方法执行
            result = point.proceed();
        } catch (Throwable e) {
            log.error("proceed方法启动目标方法执行失败:{}", e);
        }
        if (StringUtils.isNotBlank(operation)) {
        //获取请求的url,方式,ip数据
            HttpServletRequest request = getHttpServletRequest();
            String ip = getHttpServletRequestIpAddress();
            String url = request.getRequestURI();
            String method = request.getMethod();
            itLogService.saveLog(point, targetMethod, ip, operation, start, 1, url, method);
        }
        return result;
    }



    
   public static HttpServletRequest getHttpServletRequest() {
        return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    }


    
    public static String getHttpServletRequestIpAddress() {
        HttpServletRequest request = getHttpServletRequest();
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOW.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOW.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOW.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}
四、测试

访问下列方法

    @ApiIgnore
    @RequestMapping(path = "health", method = RequestMethod.GET)
    @ResponseBody
    @ApiOperation(value = "我要监控这个接口", notes = "")
    public RestResp test() {
        return RestResp.ok();
    }

在clickhouse对应的表中就可以查看到具体的数据

总结

总结:
总的来说还是比较简单的,具体的可以参考https://mrbird.cc/Spring-Boot-AOP%20log.html。注意的是在配置文件中如:application.yaml配置了多数据源的情况下如下面的情况:

 datasource:
        base:
          username: xxx
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://${mysql.url}:3308/server?allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8
        clickhouse:
          username: xxx
          password: xxxxxxx
          driver-class-name: ru.yandex.clickhouse.ClickHouseDriver
          url: jdbc:clickhouse://192.168.1.xxx:8123/jjj?socket_timeout=300000
    druid:
        min-evictable-idle-time-millis: 60000

上面同时配置了clickhouse与MySQL的数据源,这种情况的话就需要在mapper类上显示的打上@DS注解比如下面这种:

@DS("clickhouse")//这里与配置文件中配置的数据源名称一致
public interface OpenAppIdLogMapper extends BaseMapper {
}
...
//Service接口
@DS("clickhouse")
public interface IAppIdLogService extends IService {}
...
//接口实现类
@DSTransactional()
public class AppIdLogServiceImpl extends ServiceImpl implements IAppIdLogService {}




@DS("base")//因为默认是访问MySQL数据库可以不配置,但是如果出现了访问库出错的情况下,还是需要显示的配置的并且操作MySQL数据库,只需要在mapper上标记一下注解@DS
public interface AppidMapper extends BaseMapper {

}

当然不只是局限于clickhouse与MySQL,还有clickhouse,mongodb,MySQL三者同时使用也是上面的这样解决的。
希望对您有帮助!!!!!

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号