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;
}
}