埋点实现以及全流程日志记录(基于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
,放置在controller
、service
、dao
的<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
- 将
traceId
在ThreadLocal
也放一份
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>