为什么要使用统一日志管理
使用日志可以在系统出问题的时候,通过查找日志快速定位问题。日志系统可以用于监测系统是否正常运行,同时记录用户信息。如记录用户的请求参数、访问IP、请求响应时间等信息。使用AOP 做统一的日志管理,有利于实现核心业务也日志业务的解耦合。即使在日志系统出问题的时候也不影响系统的响应。在这篇文章中,主要实现一个记录用户访问某个接口的参数、IP、以及响应时间的日志。
如何实现
使用注解+AOP 的方式可以灵活的添加日志。定义好日志注解以后,在需要记录日志的地方添加注解即可,特别灵活简单。
导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
编写注释
注释也可以看成特殊的类,里面也可以定义参数
package cuit.jiefang.log;
import java.lang.annotation.*;
/**
* @author :jiefang
* @description:用来实现日志
* @date :2022/7/8 21:56
*/
//上面这三个注解是元注解
//表示注解的作用范为 METHOD 表示注解作用于方法上
@Target(ElementType.METHOD)
//表示注解在什么时候生效 RunTime 表示注解在运行时生效 spring 框架下的注解大多都是运行时生效
//Lombok的注解是在编译前生效,通过改变状态树的形式 为类自动添加一些方法
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
//变量用来记录日志记录那个模块
String module() default "";
//用来记录日志记录的是什么操作
String operation() default "";
}
编写切面类
切面类的主要作用是定义切入点与通知方法的关系。我们结合注释以后,切入点就是有注释标记的地方。通知方法使用环绕的通知方法最为灵活。
package cuit.jiefang.log;
/**
* @author :jiefang
* @description:
* @date :2022/7/8 21:58
*/
import com.alibaba.fastjson.JSON;
import cuit.jiefang.util.HttpContextUtils;
import cuit.jiefang.util.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* 日志切面
*
*/
@Aspect
@Component
@Slf4j //lombok提供的日志输出
public class LogAspect {
//切入点指的是哪里执行日志记录 这里使用注解 有注解的地方就记录日志
@Pointcut("@annotation(cuit.jiefang.log.LogAnnotation)")
public void logPointCut() {
}
//使用环绕通知 相比于前置后置更灵活
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
recordLog(point, time);
return result;
}
//具体需要记录的内容 可以灵活记录
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}", logAnnotation.module());
log.info("operation:{}", logAnnotation.operation());
//请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.info("request method:{}", className + "." + methodName + "()");
// //请求的参数
Object[] args = joinPoint.getArgs();
String params = JSON.toJSONString(args[0]);
log.info("params:{}", params);
//获取request 设置IP地址 这是两个工具类用来获取请求的IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
log.info("ip:{}", IpUtils.getIpAddr(request));
log.info("excute time : {} ms", time);
log.info("=====================log end================================");
}
}
工具类
用于获取用户请求的IP 地址的工具类
package cuit.jiefang.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* HttpServletRequest
*
*/
public class HttpContextUtils {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
package cuit.jiefang.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 获取Ip
*
*/
@Slf4j
public class IpUtils {
/**
* 获取IP地址
* <p>
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null, unknown = "unknown", seperator = ",";
int maxLength = 15;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
log.error("IpUtils ERROR ", e);
}
// 使用代理,则获取第一个IP地址
if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {
int idx = ip.indexOf(seperator);
if (idx > 0) {
ip = ip.substring(0, idx);
}
}
return ip;
}
/**
* 获取ip地址
*
* @return
*/
public static String getIpAddr() {
HttpServletRequest request = cuit.jiefang.util.HttpContextUtils.getHttpServletRequest();
return getIpAddr(request);
}
}
使用及测试
我们想记录用户创作文章的时候的信息,那么只需要在对应接口处添加注解即可
@LogAnnotation(module = "创作",operation = "写作")
public Article publish(ArticleParam articleParam) {
结果:
2022-07-09 17:36:32.952 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : =====================log start================================
2022-07-09 17:36:32.952 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : module:创作
2022-07-09 17:36:32.952 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : operation:写作
2022-07-09 17:36:32.952 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : request method:cuit.jiefang.service.impl.ArticleServiceImpl.publish()
2022-07-09 17:36:32.962 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : params:{"body":{"content":"test","contentHtml":"<p>test</p>\n"},"category":{"avatar":"/static/category/front.png","categoryName":"前端","description":"前端是什么,大前端","id":1},"summary":"1111","tags":[{"id":5}],"title":"11111"}
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : ip:0:0:0:0:0:0:0:1
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : excute time : 32 ms
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : =====================log end================================
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : =====================log start================================
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : module:文章
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : operation:创作文章
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : request method:cuit.jiefang.controller.ArticleController.publish()
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : params:{"body":{"content":"test","contentHtml":"<p>test</p>\n"},"category":{"avatar":"/static/category/front.png","categoryName":"前端","description":"前端是什么,大前端","id":1},"summary":"1111","tags":[{"id":5}],"title":"11111"}
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : ip:0:0:0:0:0:0:0:1
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : excute time : 49 ms
2022-07-09 17:36:32.968 INFO 14624 --- [nio-8088-exec-1] c.j.l.LogAspect : =====================log end================================