埋點實現以及全流程日誌記錄(基於SSM框架的AOP)


歡迎訪問筆者個人技術博客:http://rukihuang.xyz/

埋點實現以及全流程日誌記錄(基於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方法上,並將此接口作爲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

@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文件中配置可以生效,暫時還不明白時什麼原因。

不生效

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

生效

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean class="com.ruki.interceptor.TraceInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章