操作日志
一、基础准备
(存储操作日志建议不要存储数据库,用户量和访问量大对性能影响很大,使用 ``logback-spring`把日志写进文件已经够用了,日志输出尽量详细点,直接下载日志文件就可以了)
使用的操作记录日志表的 建表SQL
DROP TABLE IF EXISTS `t_operation_log`;
CREATE TABLE `t_operation_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`operation_user_id` int(11) NULL DEFAULT NULL COMMENT '操作人ID',
`operation_username` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作人名称',
`operation_module` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作模块',
`operation_events` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '具体操作事件',
`operation_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作url',
`operation_data` varchar(3048) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作附带数据',
`operation_status` tinyint(1) NOT NULL COMMENT '操作是否正常,1正常操作, 0 操作异常',
`operation_ip` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作所在IP',
`operation_result` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作结果',
`add_time` datetime(0) NOT NULL COMMENT '操作时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 删除,0 未删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志表' ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1; -- 开启外键约束 --
在项目中编写其实体类
在这个实体类之中,写了私有化的构造函数,不允许 new 来创建对象。
可以 减少了数据冲突和不一致的可能性。 (目前就了解这么多,嘿嘿!)
@Data
@TableName("t_operation_log")
@ApiModel(value = "OperationLog对象", description = "操作日志表")
public class OperationLog implements Serializable,Cloneable {
private static final long serialVersionUID = 1L;
/**
* 实现 Cloneable 克隆拷贝
* 创建一个 默认 对象,用于作为克隆的源数据
*/
private static final OperationLog log = new OperationLog();
/**
* 获取克隆对象, 避免new的方式创建
* @return {@link OperationLog}
*/
public static OperationLog getInstance(){
try {
return (OperationLog) log.clone();
} catch (CloneNotSupportedException e) {
return new OperationLog();
}
}
/**
* 重写克隆方法
* @return {@link OperationLog}
*/
public OperationLog clone() throws CloneNotSupportedException {
return (OperationLog) super.clone();
}
/**
* 私有化构造函数,不允许 new
*/
private OperationLog(){
this.deleted = false;
}
@TableId(type = IdType.AUTO)
private Integer id;
@ApiModelProperty("操作人ID")
private Integer operationUserId;
@ApiModelProperty("操作人名称")
private String operationUsername;
@ApiModelProperty("操作模块")
private String operationModule;
@ApiModelProperty("具体操作事件")
private String operationEvents;
@ApiModelProperty("操作Url")
private String operationUrl;
@ApiModelProperty("操作附带数据")
private String operationData;
@ApiModelProperty("操作是否正常,1正常操作, 0 操作异常")
private Boolean operationStatus;
@ApiModelProperty("操作结果")
private String operationResult;
@ApiModelProperty("操作所在IP")
private String operationIp;
@ApiModelProperty("操作时间")
private LocalDateTime addTime;
@ApiModelProperty("1 删除,0 未删除")
private Boolean deleted;
}
二、创建日志注解
这里就简单创建了一个注解,注解的值的部分比较简单,在学习和复习的时候,可以学习 若以框架的注解,里面的内容比较丰富和完善。但是丰富完善的内容就需要在 切面类中,进行一些情况上面的判断。
@Target({ElementType.METHOD}) //注解在方法上
@Retention(RetentionPolicy.RUNTIME) //运行时 ,该注解生效
public @interface OperationLogDesc {
/**
* 操作模块
*/
String module();
/**
* 操作事件
*/
String events();
}
若依框架自定义日志注解
/**
* 自定义操作日志记录注解
*
* @author ruoyi
*
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
/**
* 模块
*/
public String title() default "";
/**
* 功能
*/
public BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
public OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
public boolean isSaveResponseData() default true;
/**
* 排除指定的请求参数
*/
public String[] excludeParamNames() default {};
}
三、使用注解
@RequestMapping("/info")
@OperationLogDesc(module = "测试——学生信息查询" , events = "学生信息-查询") //!!!!!!!!!!!!!!
public R info(String id) {
Students stu = studentService.getinfo(id);
return R.success(stu);
}
四、切面实现
因为我们要使用的是 aop 的切面类来实现相关功能,所以,要引入 aop 的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
下面就是最最最最最最最重要的部分了!!!!!!!!!
1、代码总览
@Aspect //声明是 aspect 切面类
@Component //声明是 spring 容器中的 bean
@Slf4j // 日志
public class LoggerAspect {
@Autowired
public OperationLogService operationLogService;
/**
* FastThreadLocal 依赖于 netty,如果不想用netty,可以使用jdk自带的 ThreadLocal
*/
final ThreadLocal<OperationLog> logThreadLocal = new ThreadLocal<>();
final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
/**
* 切点
* 所有 com.*.*.controller 包下的所有方法
*/
@Pointcut("execution(* com.*.*.controller.*.*(..))") // 切点 所有 com.*.*.controller 包下的所有方法
public void logPointcut() {
}
/**
* 请求前置通知
* 切点 执行前
*
* @param joinPoint 切点
*/
@Before("logPointcut()")
public void beforeLog(JoinPoint joinPoint) throws NoSuchMethodException {
// 获取请求参数
//将方法调用时传入的参数 joinPoint 转换为字符串形式
String params = Arrays.toString(joinPoint.getArgs());
// 鉴权会话获取 当前登录的用户信息,我用的是 shiro,根据情况改变
// Subject currentUser = SecurityUtils.getSubject();
// User user = (User)currentUser.getPrincipal();
// Integer userId = null;
// String userName = null;
// if(User!= null){
// userId = User.getId();
// userName = User.getUsername();
// }else{
// /*
// 因为登录接口没有登录会话状态,无法获取到用户信息,从 登录请求参数中获取 登录用户名
// @see 例如:登录请求肯定是 post请求,上方 "params" 参数已经获取到 请求参数信息,只要判断里面是否有用户名信息 验证是否为登录接口,然后字符串截取获取用户名。。这个方法是我能想到最快捷的
// 示例登录接口参数:我的登录 请求json [LoginDTO(username=1001010, password=132456,code='A5C5')]
// */
// if(params.contains("username=")){
// userName = params.substring(params.indexOf("username=") + 9, params.indexOf(", password="));
// // 登录参数密码 简单脱密一下
// params = params.replace("password=", "changshayueluqu_"); //将password= 替换为 changshayueluqu_
// }
// }
LocalDateTime now = LocalDateTime.now();
log.info("--------请求前置日志输出开始--------");
/**RequestContextHolder 用于获取当前请求的上下文信息
* getRequestAttributes()方法返回一个RequestAttributes对象,这里通过强制类型转换将其转换为ServletRequestAttributes。
* ServletRequestAttributes是RequestAttributes的一个实现类,专门用于处理基于 Servlet 的请求上下文。
*/
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
/**
* 由于getRequestAttributes()方法可能返回null(例如在非 Web 请求的上下文中调用)
* 这里使用Objects.requireNonNull()方法确保attributes不为null
* 如果attributes不为null,则调用其getRequest()方法获取HttpServletRequest对象
* 这个对象包含了与当前 HTTP 请求相关的所有信息。
*/
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
log.info("请求访问时间: {}", dateTimeFormatter.format(now));
// 获取请求url
String requestUrl = request.getRequestURL().toString();
log.info("请求url: {}", requestUrl);
// 获取method
log.info("请求方式: {}", request.getMethod());
log.info("请求参数列表: {}", params);
log.info("操作人ID: {}", 33333);
// 验证请求方法是否带有操作日志注解
/**
* joinPoint.getSignature() 获取与当前连接点(被拦截的方法调用)相关的签名信息。
* MethodSignature 是一个接口,它表示一个方法签名。
* 获取实际调用的 method 对象,该对象包括了 方法名、参数列表、返回类型等。
*/
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
/**
* method.getAnnotation(OperationLogDesc.class) 获取当前方法上带有 OperationLogDesc 注解的实例。
*/
OperationLogDesc operationLogDesc = method.getAnnotation(OperationLogDesc.class);
if (operationLogDesc != null) {
// 操作日志记录
OperationLog operationLog = OperationLog.getInstance();
operationLog.setAddTime(now);
operationLog.setOperationModule(operationLogDesc.module());
operationLog.setOperationEvents(operationLogDesc.events());
operationLog.setOperationData(params);
operationLog.setOperationUrl(requestUrl);
// 操作人ID
operationLog.setOperationUserId(666);
operationLog.setOperationUsername("666");
// IP地址
operationLog.setOperationIp(IpUtil.getIpAddr(request));
logThreadLocal.set(operationLog);
}
}
/**
* 请求后置通知,请求完成会进入到这个方法
*
* @param result 响应结果json
*/
@AfterReturning(value = "logPointcut()", returning = "result")
public void afterReturningLogger(Object result) {
// 程序运时间(毫秒)
log.info("请求结束时间: {}", dateTimeFormatter.format(LocalDateTime.now()));
log.info("--------后台管理请求后置日志输出完成--------");
// 保存操作日志
OperationLog operationLog = logThreadLocal.get();
if (operationLog != null) {
operationLog.setOperationStatus(true);
// 用的 是 阿里巴巴的 fastjson
operationLog.setOperationResult(JSONObject.toJSONString(result));
// 调用具体的 service 保存到数据库中
operationLogService.save(operationLog);
// 移除本地线程数据
logThreadLocal.remove();
}
}
/**
* 异常通知,请求异常会进入到这个方法
*/
@AfterThrowing(value = "logPointcut()", throwing = "throwable")
public void throwingLogger(Throwable throwable) {
log.error("ErrorMessage:请根据异常产生时间前往异常日志查看相关信息");
log.error("--------后台管理请求异常日志输出完成--------");
// 保存操作日志
OperationLog operationLog = logThreadLocal.get();
if (operationLog != null) {
operationLog.setOperationStatus(false);
String throwableStr = throwable.toString();
if(throwableStr.contains(":")){
throwableStr = throwableStr.substring(throwableStr.indexOf(":") + 1);
}
operationLog.setOperationResult(throwableStr);
// 调用具体的 service 保存到数据库中
operationLogService.save(operationLog);
// 移除本地线程数据
logThreadLocal.remove();
}
}
}
2、理解解析
首先就是定义一个类,表明他是切面类,加入到 spring 容器之中,加上日志注解,可以提供看到日志信息
@Aspect //声明是 aspect 切面类
@Component //声明是 spring 容器中的 bean
@Slf4j // 日志
public class LoggerAspect {
}
插入到数据库的操作,因为我使用的是 mybatisplus,也可以直接引用 自带的方法插入数据库,可以直接引入 mapper 层
@Autowired
public OperationLogService operationLogService;
使用 ThreadLocal<>() 来存储 OperationLog 对象,保证 日志对象的独立性
/**
* FastThreadLocal 依赖于 netty,如果不想用netty,可以使用jdk自带的 ThreadLocal
*/
final ThreadLocal<OperationLog> logThreadLocal = new ThreadLocal<>();
创建了一个时间模板对象
final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
定义切点,这里是所有的controller中的方法
/**
* 切点
* 所有 com.*.*.controller 包下的所有方法
*/
@Pointcut("execution(* com.*.*.controller.*.*(..))") // 切点 所有 com.*.*.controller 包下的所有方法
public void logPointcut() {
}
前置通知
请求的前置通知,就是在方法执行前,起作用
JoinPoint 就指代目标方法执行过程中的连接点,它提供了一系列方法来获取与当前被拦截方法相关的信息
/**
* 请求前置通知
* 切点 执行前
*
* @param joinPoint 切点
*/
@Before("logPointcut()")
public void beforeLog(JoinPoint joinPoint) throws NoSuchMethodException {
}
利用 joinPoint.getArgs() 获取得到目标方法的参数,并将其转换为字符串数组
// 获取请求参数
//将方法调用时传入的参数 joinPoint 转换为字符串形式
String params = Arrays.toString(joinPoint.getArgs());
获取当前登录用户信息的方法,也判断了未登录时的状况
获取当前时间
// 鉴权会话获取 当前登录的用户信息,我用的是 shiro,根据情况改变
// Subject currentUser = SecurityUtils.getSubject();
// User user = (User)currentUser.getPrincipal();
// Integer userId = null;
// String userName = null;
// if(User!= null){
// userId = User.getId();
// userName = User.getUsername();
// }else{
// /*
// 因为登录接口没有登录会话状态,无法获取到用户信息,从 登录请求参数中获取 登录用户名
// @see 例如:登录请求肯定是 post请求,上方 "params" 参数已经获取到 请求参数信息,只要判断里面是否有用户名信息 验证是否为登录接口,然后字符串截取获取用户名。。这个方法是我能想到最快捷的
// 示例登录接口参数:我的登录 请求json [LoginDTO(username=1001010, password=132456,code='A5C5')]
// */
// if(params.contains("username=")){
// userName = params.substring(params.indexOf("username=") + 9, params.indexOf(", password="));
// // 登录参数密码 简单脱密一下
// params = params.replace("password=", "changshayueluqu_"); //将password= 替换为 changshayueluqu_
// }
// }
LocalDateTime now = LocalDateTime.now();
log.info("--------请求前置日志输出开始--------");
获取 HTTP 请求的方法 (自己看看吧)
然后操作人id ,因为没有登录相关,自己给了一个
/**
* RequestContextHolder 用于获取当前请求的上下文信息
* getRequestAttributes()方法返回一个RequestAttributes对象,这里通过强制类型转换将其转换为ServletRequestAttributes。
* ServletRequestAttributes是RequestAttributes的一个实现类,专门用于处理基于 Servlet 的请求上下文。
*/
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
/**
* 由于getRequestAttributes()方法可能返回null(例如在非 Web 请求的上下文中调用)
* 这里使用Objects.requireNonNull()方法确保attributes不为null
* 如果attributes不为null,则调用其getRequest()方法获取HttpServletRequest对象
* 这个对象包含了与当前 HTTP 请求相关的所有信息。
*/
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
log.info("请求访问时间: {}", dateTimeFormatter.format(now));
// 获取请求url
String requestUrl = request.getRequestURL().toString();
log.info("请求url: {}", requestUrl);
// 获取method
log.info("请求方式: {}", request.getMethod());
log.info("请求参数列表: {}", params);
log.info("操作人ID: {}", 33333);
首先就是通过 joinPoint.getSignature() 得到方法签名信息 然后得到方法,再通过方法获取 注解,看看注解有无 自定义注解 OperationLogDesc
有的话,就进行操作信息的记录 ( IpUtil 是一个工具类,在本文最后的备注里)
并放进线程中 !!!!!!!!!
// 验证请求方法是否带有操作日志注解
/**
* joinPoint.getSignature() 获取与当前连接点(被拦截的方法调用)相关的签名信息。
* MethodSignature 是一个接口,它表示一个方法签名。
* 获取实际调用的 method 对象,该对象包括了 方法名、参数列表、返回类型等。
*/
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
/**
* method.getAnnotation(OperationLogDesc.class) 获取当前方法上带有 OperationLogDesc 注解的实例。
*/
OperationLogDesc operationLogDesc = method.getAnnotation(OperationLogDesc.class);
if (operationLogDesc != null) {
// 操作日志记录
OperationLog operationLog = OperationLog.getInstance();
operationLog.setAddTime(now);
operationLog.setOperationModule(operationLogDesc.module());
operationLog.setOperationEvents(operationLogDesc.events());
operationLog.setOperationData(params);
operationLog.setOperationUrl(requestUrl);
// 操作人ID
operationLog.setOperationUserId(666);
operationLog.setOperationUsername("666");
// IP地址
operationLog.setOperationIp(IpUtil.getIpAddr(request));
logThreadLocal.set(operationLog);
}
后置通知
请求方法执行后,执行此方法
/**
* 请求后置通知,请求完成会进入到这个方法
*
* @param result 响应结果json
*/
@AfterReturning(value = "logPointcut()", returning = "result")
public void afterReturningLogger(Object result) {
}
记录一个请求的结束时间
通过线程得到 operationLog 对象 保证对象的唯一性和统一 !!!!!
记录操作正常
操作结果 将 JSON 转换成字符串 (方便看)
保存到数据库之中
最后移除线程
// 程序运时间(毫秒)
log.info("请求结束时间: {}", dateTimeFormatter.format(LocalDateTime.now()));
log.info("--------后台管理请求后置日志输出完成--------");
// 保存操作日志
OperationLog operationLog = logThreadLocal.get();
if (operationLog != null) {
operationLog.setOperationStatus(true);
// 用的 是 阿里巴巴的 fastjson
operationLog.setOperationResult(JSONObject.toJSONString(result));
// 调用具体的 service 保存到数据库中
operationLogService.save(operationLog);
// 移除本地线程数据
logThreadLocal.remove();
}
异常通知
请求异常就会进入这个方法
/**
* 异常通知,请求异常会进入到这个方法
*/
@AfterThrowing(value = "logPointcut()", throwing = "throwable")
public void throwingLogger(Throwable throwable) {
}
通过先程得到 OperationLog 对象 保证唯一性
给定 失败的 (false)状态
处理异常信息,如果异常信息包括 :(冒号),只截取异常信息,不要异常类型
存数据库,移除线程
log.error("ErrorMessage:请根据异常产生时间前往异常日志查看相关信息");
log.error("--------后台管理请求异常日志输出完成--------");
// 保存操作日志
OperationLog operationLog = logThreadLocal.get();
if (operationLog != null) {
operationLog.setOperationStatus(false);
String throwableStr = throwable.toString();
if(throwableStr.contains(":")){
throwableStr = throwableStr.substring(throwableStr.indexOf(":") + 1);
}
operationLog.setOperationResult(throwableStr);
// 调用具体的 service 保存到数据库中
operationLogService.save(operationLog);
// 移除本地线程数据
logThreadLocal.remove();
}
备注
所需依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83_noneautotype</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
IpUtil
public class IpUtil {
private static final Logger log = LoggerFactory.getLogger(IpUtil.class);
private IpUtil() {
}
public static String getIpAddr(HttpServletRequest request) {
String ipAddress;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if ("127.0.0.1".equals(ipAddress)) {
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException var4) {
log.error(var4.getMessage(), var4);
}
ipAddress = ((InetAddress)Objects.requireNonNull(inet)).getHostAddress();
}
}
if (ipAddress != null && ipAddress.length() > 15 && ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
} catch (Exception var5) {
ipAddress = "";
}
return ipAddress;
}
public static String getIpAddr() {
return getV4OrV6IP();
}
public static String getV4OrV6IP() {
String ip = null;
String test = "http://test.ipw.cn";
StringBuilder inputLine = new StringBuilder();
BufferedReader in = null;
try {
URL url = new URL(test);
HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection();
in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8));
String read;
while((read = in.readLine()) != null) {
inputLine.append(read);
}
ip = inputLine.toString();
} catch (Exception var16) {
log.error("获取网络IP地址异常,这是具体原因: ", var16);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException var15) {
var15.printStackTrace();
}
}
}
if (ip == null) {
ip = "127.0.0.1";
log.info("获取网络IP地址异常, 赋值默认ip: 【{}】", ip);
}
return ip;
}
}
ip问题
数据库记录为 0:0:0:0:0:0:0:1
原因
0:0:0:0:0:0:0:1是属于ipv6,但是本机又没有设置ipv6,后来我又进行另一台电脑做测试,发现这种情况只有在服务器和客户端都在同一台电脑上才会出现(例如用localhost访问的时候才会出现),原来是hosts配置文件的问题 windows的hosts文件路径:C:\Windows\System32\drivers\etc\hosts linux的host路径:/etc/hosts
解决措施
注释掉文件中的 # ::1 localhost 这一行即可解决问题。不过不起作用。 最有效的方式就是改变请求的ip,不要使用localhost:8080 使用127.0.0.1:8080或者ip:8080。百分百管用
标签:null,log,记录,操作,日志,operationLog,public,请求
From: https://www.cnblogs.com/FangZongRi/p/18655632