基于springAop的日志采集-进阶

hl.wang

发布于 2021.06.08 12:49 阅读 2335 评论 0

提出问题

       在之前文章中我们使用spring的Aop实现了操作日志的采集功能,但是当出现大量操作时,会出现数据混乱的问题,并且我们之前使用了map存储数据不够简洁。

 

 

 

分析问题

    针对于高并发下的操作日志的采集,我们将操作信息存储于一个entity中使代码更加简洁易懂,并且利用@Around环绕采集所需信息解决高并发中采集数据混乱的问题。

 

 

 

 

解决问题

   首先我们定义所需要的entity

@Data
@EqualsAndHashCode(callSuper = false)
public class UserLogInfo implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 用户id
     */
    private Long usloUserId;
    /**
     * 用户姓名
     */
    private String usloUserName;
    /**
     * 访问Ip
     */
    private String usloVisitAddress;
    /**
     * 服务器IP
     */
    private String usloServerAddress;
    /**
     * 机器名
     */
    private String usloHostName;
    /**
     * 项目id
     */
    private Long usloItemId;
    /**
     * 项目名称
     */
    private String usloItemName;
    /**
     * 请求url
     */
    private String usloUrl;
    /**
     * 请求端口
     */
    private String usloPor;
    /**
     * 请求类型(1.1post 2.get3. put4.delete)
     */
    private Integer usloType;
    /**
     * 方法描述
     */
    private String usloDescription;
    /**
     *  请求标识(1.http 2. https  3.tcp  4.udp)
     */
    private Integer usloIdentification;
    /**
     * 请求域名
     */
    private String usloDomain;
    /**
     * 请求时长
     */
    private Long usloRequestTime;
    /**
     * 请求状态
     */
    private Integer usloActionCode;
    /**
     * 错误类型
     */
    private String usloErrorName;
    /**
     * 错误内容
     */
    private String usloErrorContent;
    /**
     * 备注
     */
    private String usloRemark;
    /**
     * 访问开始时间
     */
    private Date usloDbStartTime;
    /**
     * 访问结束时间
     */
    private Date usloDbEndTime;
    /**
     *  创建时间
     */
    private Date usloDbCreateTime;
    /**
     * Date数据最后一次修改时间
     */
    private Date usloDbUpdateTime;
    /**
     * 请求方法名
     */
    private String usloMethodName;
    /**
     * 请求信息
     */
    private String usloRequestData;
    /**
     * 请求返回的数据
     */
    private String usloResponseData;
}

   

 

 

下面我们建立一个切面来拦截这个注解进行处理,首先我们定义拦截点

@Pointcut("@annotation(com.jtexplorer.aop.annotation.MyLog)")
private void sendLog() {
}

 

 

 

由于我们要解决在高并发下的日志采集数据正确性问题,因此我们本次只使用@Around注解,在@Around注解环绕前我们对一些初始值进行赋值,在环绕后我们的本次请求的结果进行进行处理计算。

@Around("sendLog()")
public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable {
    UserLogInfo userLog = new UserLogInfo();
    Object result = null;
    before(joinPoint,userLog);
    try{
        result = joinPoint.proceed();
        after(joinPoint,userLog);
        afterReturning(joinPoint,userLog);
        if (result != null) {
            //提交日志需要
            userLog.setUsloResponseData(result.toString());
        }
    }catch (Exception e){
        after(joinPoint,userLog);
        afterThrowing(joinPoint,e,userLog);
        JsonResult jsonResult = new JsonResult();
        jsonResult.setSuccess(false);
        jsonResult.setFailReason("系统错误");
        return jsonResult;
    }
    return result;
}

 

 

 

 

根据我们的业务需求在方法在方法执行前,我们首先设置一些默认值,我们在@Around注解环绕前执行before方法为我们的info填充信息

public void before(JoinPoint joinPoint, UserLogInfo userLogInfo) {
    //初始化
    try {
        userLogInfo.setUsloDbStartTime(TimeTools.transformDateFormatStringToDate(null,"yyyy-MM-dd HH:mm:ss"));
        startTime = System.currentTimeMillis();
        userLogInfo.setUsloServerAddress(IPUtils.getOutIPV4());
    } catch (ParseException e) {
        e.printStackTrace();
    }
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    MyLog myLog = signature.getMethod().getAnnotation(MyLog.class);
    if (myLog != null) {
        if (myLog.isLog()) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            //封装域名、url
            userLogInfo.setUsloDomain(request.getServerName());
            userLogInfo.setUsloUrl(request.getServletPath());
            //请求类型
            if(request.getMethod().equals("POST")){
                userLogInfo.setUsloType(1);
            }else if(request.getMethod().equals("GET")){
                userLogInfo.setUsloType(2);
            }else if(request.getMethod().equals("PUT")){
                userLogInfo.setUsloType(3);
            }else if(request.getMethod().equals("DELETE")){
                userLogInfo.setUsloType(4);
            }
            String projectName = PropertiesUtil.GetValueByKey(Thread.currentThread().getContextClassLoader().getResource(FILE_PATH).getPath(), "sendLogProjectName");
            if(StringUtil.isNotEmpty(projectName)){
                userLogInfo.setUsloItemName(projectName);
            }
            String projectId = PropertiesUtil.GetValueByKey(Thread.currentThread().getContextClassLoader().getResource(FILE_PATH).getPath(), "sendLogProjectId");
            if(StringUtil.isNotEmpty(projectId)){
                userLogInfo.setUsloItemId(Long.getLong(projectId));
            }
            //方法描述
            try {
                //获得http还是https
                String networkProtocol = request.getScheme();
                //获得端口号
                int port = request.getServerPort();
                //获得服务器ip和机器名
                InetAddress ia = InetAddress.getLocalHost();
                String hotsAddress = ia.getHostAddress();
                String hostName = ia.getHostName();
                userLogInfo.setUsloPor(String.valueOf(port));
                //服务器ip
                userLogInfo.setUsloServerAddress(hotsAddress);
                //机器名称
                userLogInfo.setUsloHostName(hostName);
                if (networkProtocol.equals("http")) {
                    userLogInfo.setUsloIdentification(1);
                } else if (networkProtocol.equals("https")) {
                    userLogInfo.setUsloIdentification(2);
                }
                String methodDescription = getControllerMethodDescription(joinPoint);
                //方法描述
                userLogInfo.setUsloDescription(methodDescription);
                //访问者ip
                String requestIp = IPUtil.getIpFromRequest(request);
                userLogInfo.setUsloVisitAddress(requestIp);
            } catch (Exception e) {
                e.printStackTrace();
            }
            //拼接请求参数
            StringBuffer params = new StringBuffer();
            params.append("{");
            Enumeration<?> enumeration = request.getParameterNames();
            while (enumeration.hasMoreElements()) {
                String paramName = enumeration.nextElement().toString();
                params.append("\"");
                params.append(paramName + "\":\"" + request.getParameter(paramName) + "\",");
            }
            params.append("}");
            //请求数据
            userLogInfo.setUsloRequestData(params.toString());
            //请求方法
            userLogInfo.setUsloMethodName(joinPoint.getSignature().getName());
        }
    }
}

 

/**
* 获得注解中的描述信息
*
* @param joinPoint
* @return
* @throws ClassNotFoundException
*/
public static String getControllerMethodDescription(JoinPoint joinPoint) throws ClassNotFoundException {
    String targetName = joinPoint.getTarget().getClass().getName();
    String methodName = joinPoint.getSignature().getName();
    Object[] arguments = joinPoint.getArgs();
    Class targetClass = Class.forName(targetName);
    Method[] methods = targetClass.getMethods();
    String description = "";
    for (Method method : methods) {
        if (method.getName().equals(methodName)) {
            Class[] clazzs = method.getParameterTypes();
            if (clazzs.length == arguments.length) {
                description = method.getAnnotation(MyLog.class).remark();
                break;
            }
        }
    }
    return description;
}

 

 

 

 

在before方法执行完成之后,我们对本次请求进行处理,拿到本次请求的结果之后根据请求结果来进行相关业务所需数据的计算与填充我们进入到after方法和afterReturning方法

 

public void after(JoinPoint joinPoint, UserLogInfo userLog) {
    try {
        userLog.setUsloDbEndTime(TimeTools.transformDateFormatStringToDate(null,"yyyy-MM-dd HH:mm:ss"));
    } catch (ParseException e) {
        e.printStackTrace();
    }
    endTime = System.currentTimeMillis();
    //响应所耗费时间
    userLog.setUsloRequestTime(endTime-startTime);
}

 

public void afterReturning(JoinPoint joinPoint,UserLogInfo userLog) {
    try {
        HttpServletResponse httpServletResponse = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        int status = httpServletResponse.getStatus();
        //状态码 200 500
        userLog.setUsloActionCode(status);
        if (userLog.getUsloActionCode()==200) {
            userLog.setUsloErrorContent("");
        }
        String url = PropertiesUtil.GetValueByKey(Thread.currentThread().getContextClassLoader().getResource(FILE_PATH).getPath(), "sendLogIP");
        sendLogByThreadPool(url,userLog);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 

 

 

 

 

由于我们的请求不一定全是正常返回有的会产生异常,我们需要对产生异常的请求也进行捕捉并计算填充相关的信息,因此我们还需要编写异常捕捉的方法afterThrowing

 

public void afterThrowing(JoinPoint joinPoint, Throwable throwable, UserLogInfo userLogInfo) {
    throwable.printStackTrace();
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    throwable.printStackTrace(pw);
    String[] errors = sw.toString().split("\r\n");
    int i = 0;
    StringBuffer sb = new StringBuffer();
    for(String error : errors){
        if(i<10){
            sb.append(errors[i] + "\r\n");
        }else{
            break;
        }
        if(i == 0){
            userLogInfo.setUsloErrorName(errors[i]);
        }
        i++;
    }
    userLogInfo.setUsloErrorContent(sb.toString());
    userLogInfo.setUsloActionCode(500);
    String url = PropertiesUtil.GetValueByKey(Thread.currentThread().getContextClassLoader().getResource(FILE_PATH).getPath(), "sendLogIP");
    sendLogByThreadPool(url,userLogInfo);
}

 

 

 

 

 

 最后我们只剩下@AfterReturning注解与@AfterThrowing注解,其实从名字上我们也可以看出来这两个注解的方法是用来干什么的,@AfterReturning就是在正常响应之后会进入到这个注解之中根据我们的业务需求主要用来记录我们的响应状态码

 

@AfterReturning("sendLog()")
public void afterReturning(JoinPoint joinPoint) {
    try {
        HttpServletResponse httpServletResponse = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        int status = httpServletResponse.getStatus();
        //状态码 200 500
        requestMap.put("usloAction", String.valueOf(status));
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Employee employee = SessionUtil.getLoginEmp(httpServletRequest.getSession());
        if (employee != null) {
            requestMap.put("usloUserId", employee.getEmpId().toString());
            requestMap.put("usloUserName", employee.getEmpName());
        }
        if (requestMap.get("usloAction").equals("200")) {
            requestMap.put("usloErrorContent", "");
        }
        String url = PropertiesUtil.GetValueByKey(Thread.currentThread().getContextClassLoader().getResource(FILE_PATH).getPath(), "sendLogIP");
        sendLogByThreadPool(url);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 

 

 

 

 

@AfterThrowing注解则是当代码出现错误的时候会被拦截到改注解之中,用于处理当后台代码报错时我们应存储什么操作日志,根据实际的业务需求进行变动在这里我们只需要记录状态码和错误原因

 

@AfterThrowing(value = "execution(* com.*.controller..*(..))", throwing = "throwable")
public void afterThrowing(JoinPoint joinPoint, Throwable throwable) {
    throwable.printStackTrace();
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    throwable.printStackTrace(pw);
    String[] errors = sw.toString().split("\r\n");
    int i = 0;
    StringBuffer sb = new StringBuffer();
    for(String error : errors){
        if(i<10){
            sb.append(errors[i] + "\r\n");
        }else{
            break;
        }
        if(i == 0){
            requestMap.put("usloErrorName",errors[i]);
        }
        i++;
    }
    requestMap.put("usloErrorContent", sb.toString());
    requestMap.put("usloAction", "500");
    String url = PropertiesUtil.GetValueByKey(Thread.currentThread().getContextClassLoader().getResource(FILE_PATH).getPath(), "sendLogIP");
    sendLogByThreadPool(url);
}

 

至此我们通过spirngAop在高并发的情况下采集操作日志的功能就完成了。

 

 

 

总结

      本文主要使用了spring的aop编写自定义注解来实现了高并发情况下用户操作日志的收集工作。aop中注解的执行顺序为@before->@around->@after->@afterReturning当出现异常时执行顺序为:@before->@around->@after->@afterThrowing。before我们一般用于初始值的赋值,具体的业务逻辑我们一般编写在后面的过程之中。但是在高并发的情况下这样的执行顺序会出现数据错误的问题,因此我们只使用@Around注解来采集高并发情况下的日志信息,在环绕前我们使用before方法对信息初始信息进行赋值,环绕后我们使用after和afterReturning方法对请求结果进行处理计算,若请求出现异常则使用after和afterThrowning方法对请求结果进行处理计算。若要参考非高并发下的日志采集请查看文章:基于springAOP的日志采集实现_孔雀英才-软件开发训练营 (codingcamp.cn)