埋点实现以及全流程日志记录(基于SSM的AOP)

1. 需求

  • 由于项目需要,mentor给我布置了一个埋点的开发任务,主要内容如下

    • 需求1:记录用户的关键操作,并将用户id访问时间访问接口访问的关键内容记录下来,存到oracle数据库中
    • 需求2:记录一次访问的全流程,controller -> service -> dao,将该流程中执行的方法利用logger打印至控制台,方便日后debug

2. 实现思路

  • 实现思路基于小杨vita的这一篇博客,原文链接:在Java项目中使用traceId跟踪请求全流程日志
  • 需求1:基于AOP切面的思想,自定义一个注解MyLog,并将注解放置在Controller接口方法。并将此接口作为PointCut,在前置通知中,记录关键信息。
  • 需求2:同样是基于切面思想,自定义一个拦截器,在访问前拦截每一个请求,给每个请求生成一个traceId,并在ThreadLocal中放置一个traceId副本;自定义一个注解TraceLog,放置在controllerservicedao的<font color="red">方法</font>上,并将此接口作为PointCut,在环绕通知中,通过logger在控制台输出信息。

3. 代码实现

3.1 需求1

3.1.1 自定义注解 MyLog

import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.METHOD}) //Annotation所修饰的对象范围
@Retention(RetentionPolicy.RUNTIME) //生命周期
@Documented //产生doc文档时会记录
public @interface MyLog {

}
  • @Target注解作用目标:

    • @Target(ElementType.TYPE)——接口、类、枚举、注解
    • @Target(ElementType.FIELD)——字段、枚举的常量
    • @Target(ElementType.METHOD)——方法
    • @Target(ElementType.PARAMETER)——方法参数
    • @Target(ElementType.CONSTRUCTOR) ——构造函数
    • @Target(ElementType.LOCAL_VARIABLE)——局部变量
    • @Target(ElementType.ANNOTATION_TYPE)——注解
    • @Target(ElementType.PACKAGE)——包
  • @Retention生命周期:

    • RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
    • RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
    • RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

3.1.2 切面类 AopLog

package com.cmcczj.api.bid.controller;

import com.cmcczj.api.bid.model.SysLogInfo;
import com.cmcczj.api.bid.service.IAopLogService;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;



@Component
@Aspect
public class AopLog {

    private static Logger logger = Logger.getLogger(AopLog.class);

    @Autowired
    private IAopLogService aopLogService;

    @Autowired
    private HttpServletRequest request;

    @Pointcut("@annotation(com.ruki.annotation.MyLog)")
    public void aopLog(){}

    @Before("aopLog()")
    public void doBefore(JoinPoint jp) {
        System.out.println("前置通知");
        String params = request.getParameter("userId") == null ? "" : request.getParameter("userId");
        String requestURI = request.getRequestURI();
        String params = request.getParameter("data") == null ? "" : request.getParameter("data");

        SysLogInfo info = new SysLogInfo();//参数实体类
        info.setParam(params);
        info.setRequest_method(requestURI);

        try {
            aopLogService.saveLog(info);
        } catch (Exception e) {
            logger.info(e.getMessage());
        }
    }
}

3.1.3 IAopLogService/AopLogServiceImpl

  • IAopLogService
@Service
public interface IAopLogService {
    void saveLog(SysLogInfo sysLogInfo) throws Exception;
}
  • AopLogServiceImpl
@Service
public class AopLogServiceImpl implements IAopLogService {

    @Autowired
    private IAopLogDao aopLogDao;

    @Override
    public void saveLog(SysLogInfo sysLogInfo){
//        System.out.println("进入dao了");
        try {
            aopLogDao.saveLog(sysLogInfo);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.1.4 IAopLogDao

public interface IAopLogDao {
    void saveLog(SysLogInfo sysLogInfo) throws Exception;
}

3.1.5 SysLogInfo

public class SysLogInfo {
    private String userId;
    private String request_method;
    private Integer data_locale;
    private String data_time;
    //getter setter
}

3.1.6 IAopLogDao.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruki.dao.IAopLogDao">

    <insert id="saveLog" parameterType="com.ruki.model.SysLogInfo">
        INSERT INTO
            HH_SYSLOG(
                USERID,
                REQUEST_TIME,
                REQUEST_METHOD
                DARA
                )
            VALUES (
                #{userId},
                to_char(sysdate,'yyyy-mm-dd hh24:mi:ss'),
                #{request_method},
                #{data}
                )
    </insert>

</mapper>

3.2 需求2

3.2.1 自定义注解 MyTraceLog

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyTraceLog {
}

3.2.2 切面类 TraceLog

@Component
@Aspect
public class TraceLog {
    private static Logger traceLogger = Logger.getLogger(TraceLog.class);

    @Pointcut("@annotation(com.ruki.annotation.MyTraceLog)")
    public void traceLog(){}

    @Around("traceLog()")
    public Object doAround(ProceedingJoinPoint jp) {
        String traceId = RequestContext.getTraceId();
        String methodName = jp.getSignature().getName();
        String className = jp.getTarget().getClass().getName();

        Object object = null;
        try {
            traceLogger.info("traceId={"+traceId+"}, className={"+className+"},methodName={"+methodName+"},开始执行");
            object = jp.proceed();
            traceLogger.info("traceId={"+traceId+"}, className={"+className+"},methodName={"+methodName+"},执行结束");
        } catch (Throwable throwable) {
            traceLogger.error("traceId={"+traceId+"}, className={"+className+"},methodName={"+methodName+"},执行异常");
        }

        return object;
    }
}

3.2.3 拦截器 TraceInterceptor

public class TraceInterceptor extends HandlerInterceptorAdapter {
    private static final Logger LOGGER = Logger.getLogger(TraceInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LOGGER.info("trace进入拦截器内");
        String traceId = request.getHeader(Constants.LOG_TRACE_ID);//获得traceId
        if (traceId == null || traceId == "") {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("首次分配traceId");
            }
            traceId = TraceLogUtils.getTraceId();//生成traceId
        }
        RequestContext.addTraceId(traceId);//往ThreadLocal添加生成的traceId
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        RequestContext.removeTraceId();//访问完成后清理ThreadLocal中的traceId
        LOGGER.info("清理本次请求的trace信息完成");
        super.afterCompletion(request, response, handler, ex);
    }
}

3.2.4 生成 traceId TraceLogUtils

public class TraceLogUtils {
    public static String getTraceId() {
        return UUID.randomUUID().toString();
    }
}

3.2.5 常量 Constants

public class Constants {

    public static final String LOG_TRACE_ID = "trace_id";

}

3.2.6 请求上下文 RequestContext

  • traceIdThreadLocal也放一份
public class RequestContext {
    private final static ThreadLocal<String> traceIdThreadLocal = new ThreadLocal<>();

    public static void addTraceId(String traceId) {
        traceIdThreadLocal.set(traceId);
    }

    public static String getTraceId() {
        return traceIdThreadLocal.get();
    }

    public static void removeTraceId() {
        traceIdThreadLocal.remove();
    }
}

3.3 Controller

@RestController
@RequestMapping("/hello")
public class HelloWorld {

    @Autowired
    private IHelloService helloService;

    @MyLog
    @MyTraceLog
    @RequestMapping(value = { "/say" }, method = { RequestMethod.GET })
    public String sayHello(){
        System.out.println("hello world");
        helloService.sayHello();
        return "hello world!";
    }
}

4. 遇到的问题

  • 在实现需求2的时编写了一个拦截器TraceInterceptor,使用注解方式注册拦截器未生效,在xml文件中配置可以生效,暂时还不明白是什么原因。

<font color="red">不生效</font>

@Configuration
public class InceptorRegisConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TraceInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }
}

<font color="red">生效</font>

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean class="com.ruki.interceptor.TraceInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>
Last modification:December 20th, 2019 at 11:02 am