AOP与日志 | 小册免费学

363 阅读4分钟

AOP日志为什么很重要?

当系统规模比较大时,使用断点排查或者直接System.out.println会非常麻烦: 项目庞大,被拆分成了多个服务时,出于各方面的原因,本地启动的成本很高或者本地环境无法启动,只能走预发(临时上线)。

虽然确实可以远程断点调试,但也仅限于预发,线上环境是不可能断点调试的(将请求转发到本地) 线上数据和本地数据不一致,有时bug无法复现

即使可以本地调试,需要拉取最新的master代码,启动项目,构造请求数据,断点走读,一系列操作,太费事了。

妥当的解决方法:打日志-->看日志。登录服务器看log日志,搭建专门的ELK日志系统,可以在页面上选择对应的机器并配合Lucene语句等进行日志查看。

AOP

AOP完成需求需要完成的步骤:

  • SpringBoot项目需导入spring-boot-starter-aop依赖
  • 编写切面类
  • 把切面类交由Spring管理

编写切面类步骤

类上加上@Aspect注解,表明是一个切面类

需要把切面类交给Spring管理(我们要切的Controller/Service都是Spring容器的,切面要对它们起作用,就必须同样进入容器)

切面类内部编写切点表达式,如:@PointCut(“execution(* com.jt.controller..(..))”)表示对com.jt.controller下的所有方法进行切面增强。

切面类内部编写增强逻辑,@Before,@Around,表明这是一个增强,不同的注解增强方式不同。 接口请求耗时AOP demo

` @Aspect

@Slf4j

public class APITimeLogAspect {

// 编写切面类 编写了@EnableTimeLog注解 将切面类加载进spring容器 把注解放在启动类上
// 第二步 编写切点表达式

/**
 * 切点表达式 表示com.jt.projects下所有方法都需要增强(public)
 */
@Pointcut("execution(* com.jt.projects.aop.controllers.*.* (..))")
public void pointCut(){}

@Around("pointCut()")
public Object doAround(ProceedingJoinPoint joinPoint){
    Long startTime = System.currentTimeMillis();
    Object proceed = null;
    try {
        proceed = joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    log.info("-------耗时-------- {}",(System.currentTimeMillis() - startTime));
    return proceed;
}

} `` 把切面类加入Spring容器中: 可以直接在切面类上添加@Component 可以定义一个新的注解通过@Import注解把切面类加载进去 @Import注解:Spring定义的注解,由Spring读取并执行:把对应的Bean实例化并加载到容器中。 AOP打印请求相关日志: 一般来说在关键的代码中打印日志,当遇到没有打印日志的接口,此时我们定位问题就会非常吃力。 这种日志是局部的,特殊的。所以定义全局的、一般的日志记录很有必要。 我们比较关心的: 接口URL:请求哪个接口 类名方法:定位到哪个类哪个方法 请求参数:引发问题的参数是什么(方便复现问题) 远程地址 接口耗时 @Aspect @Slf4j public class ApiLogAspect { /** * 切点表达式 表示com.jt.projects下所有方法都需要增强(public) / @Pointcut("execution( com.jt.projects.aop.controllers.. (..))") public void pointCut(){}

@Autowired
HttpServletRequest request;

@Autowired
ObjectMapper objectMapper;

@Around("pointCut()")
public Object doAround(ProceedingJoinPoint joinPoint){
    LogEntity logEntity = new LogEntity();
    // 方法签名 可以理解为对方法信息的封装 包括类信息等
    Signature signature = joinPoint.getSignature();
    String classSimpleName = signature.getDeclaringType().getSimpleName();
    String className = signature.getDeclaringType().getName();
    String methodName = signature.getName();
    logEntity.setMethodName(
            String.format("类名为:%s,方法名为%s,方法全路径为%s",classSimpleName,methodName,(className +"#" + methodName)));
    logEntity.setUrl("接口URL:" + request.getRequestURI() + "请求类型:" + request.getMethod());
    // 一般都会有代理转发,真实的ip会放在X-Forwarded-For
    String xff = request.getHeader("X-Forwarded-For");
    if (xff == null) {
        xff = request.getRemoteAddr();
    } else {
        xff = xff.contains(",") ? xff.split(",")[0] : xff;
    }
    logEntity.setRemoteAddress("远程调用接口:"+ xff);

    String queryParam = "";
    Object[] objects = Arrays.stream(joinPoint.getArgs()).filter(arg -> !(arg instanceof ServletRequest ||
            arg instanceof ServletResponse ||
            arg instanceof MultipartFile)).toArray();
    queryParam = objectMapper.valueToTree(objects).toPrettyString();
    if ("GET".equals(request.getMethod())){
        queryParam = request.getQueryString();
    }

    logEntity.setRequestArgs(queryParam);

    Object proceed = null;
    try {
        Long startTime = System.currentTimeMillis();
        proceed = joinPoint.proceed();
        logEntity.setTimeConsuming(System.currentTimeMillis() - startTime);
    } catch (Throwable throwable) {
        throwable.printStackTrace();
        log.info("异常信息:{}", throwable.getMessage());
    }
    log.info("日志信息为:{}",logEntity);
    return proceed;
}

接口方法调用private方法不会走AOP。

优化 一般我们都用切点表达式来控制哪些类需要增强。但有时可能细粒度不够,或者不够灵活 可以配合注解使用。

/**

  • 切点表达式 类或方法上由ApiLog 且没有Ignore注解的 */ @Pointcut("( @annotation(com.jt.projects.aop.annations.ApiLog) " + "|| @within(com.jt.projects.aop.annations.ApiLog) ) " + "&& !@annotation(com.jt.projects.aop.annations.IgnoreApiLog)") public void pointCut(){}

@Aspect public class ApiUserLogAspect { @Autowired UserLogService userLogService; @Autowired ObjectMapper objectMapper; @Autowired HttpServletRequest request; /** * 设置切点表达式 只针对使用了@UserLog */ @Pointcut("@annotation(com.jt.projects.aop.annations.UserLog)") public void pointCut(){}

@AfterReturning("pointCut()")
public void afterReturning(JoinPoint joinpoint){
    saveSysUserLog((ProceedingJoinPoint) joinpoint);
}

private void saveSysUserLog(ProceedingJoinPoint joinpoint) {
    UserDTO userDTO = getUserDTO();
    MethodSignature signature = (MethodSignature) joinpoint.getSignature();
    Method method = signature.getMethod();
    UserLog annotation = method.getAnnotation(UserLog.class);
    if (annotation == null){
        return;
    }
    // 当方法中有UserLog注解时 记录进数据库 收集相关信息并保存
    UserLogDTO userLogDTO = buildUserLogDTO(userDTO,joinpoint,annotation);

    //存入数据库
    userLogService.addSysLog(userLogDTO);
}

private UserLogDTO buildUserLogDTO(UserDTO userDTO, ProceedingJoinPoint joinpoint, UserLog annotation) {
    UserLogDTO userLogDTO = new UserLogDTO();
    userLogDTO.setOperatorId(userDTO.getId());
    userLogDTO.setType(annotation.operationType().getValue());
    userLogDTO.setTitle(annotation.title());
    userLogDTO.setModuleCode(annotation.module().getModuleCode());
    userLogDTO.setContent(getContent(joinpoint));
    userLogDTO.setOperateTime(new Date());
    return userLogDTO;
}

private String getContent(ProceedingJoinPoint joinpoint) {
    if ("GET".equals(request.getMethod())){
        return request.getQueryString();
    }
    Object[] objects = Arrays.stream(joinpoint.getArgs()).filter(arg -> !(arg instanceof ServletRequest ||
            arg instanceof ServletResponse ||
            arg instanceof MultipartFile)).toArray();
    return objectMapper.valueToTree(objects).toPrettyString();
}

private UserDTO getUserDTO() {
    UserDTO userDTO = (UserDTO)ThreadLocalUtilV4.get("userDTO");
    return userDTO;
}

}

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情