文章目录
- 前言
- 1. 添加依赖
- 2. 创建自定义注解@LogAnnotation
- 3. 创建日志记录类型
- 3. 编写切面逻辑
- 4. 完善切面层,获取详细的请求信息
- 4.1 获取自定义注解上的属性值
- 4.2 通过Cookies获取用户信息
- 4.3 获取执行时间
- 4.4 日志实体类以及对应数据库类型
- 5.最后实现的结果
前言
在一个项目中,想要能够记录用户敏感操作的功能。例如用户登录操作,删除某个模块的内容,系统能够将系统日志自动加入到数据库中。日志内容主要包括了,操作用户的id,用户的姓名、用户ip来源、操作内容是什么,执行了什么URL,执行耗时等等。
其实日志记录,简单的实现实际上还是在用户执行敏感操作的时候,新增一个日志Controller,像其他业务一样实现数据库的增删改查。但是这种笨方法将会大大增加代码量。同时还要改动原项目代码,在执行写入数据库的控制类后还要写入数据库的日志类,如果不仔细审查会容易出错。
但是有没有好一点的方法呢?我们可以使用面向切面编程(Aspect Oriented Programming,AOP)解决这类问题。
先不扯这些原理机制,我们要实现的目标就是,用一种东西或者某种机制,在系统执行某个控制类(Controller)方法的之前(预先通知)或者之后(事后通知),它能够将执行的这个方法参数,用户信息,ip,执行了什么操作等等都记录下来,打印到系统后台或者写到数据库,具有高级权限的人员,如管理员可以从后台看到这些执行操作的信息。
怎么实现呢?如果我们使用了AOP机制,那么在需要日志记录的方法上添加注释即可。
例如给登录控制类上添加日志记录和打印,我们只需在LoginController
添加一个自定义注释@LogAnnotation
,并注明该注解下的两个属性的message,operation值即可:其他代码完全不用动。
@LogAnnotation(message = "用户登录", operation = LogType.LOGIN)
//上面的注释就已经完成了AOP机制
@PostMapping("/login")
public ApiResult login(@RequestBody Login login) {
....
你的登录业务代码
....
}
这实际上正是AOP的特性之一,在不改变源代码的前提下,给系统增加某些共有功能,例如日志记录,性能统计,安全控制,事务处理,异常处理等系统级维护层次,这样开发的好处就是,共有模块的代码和你的业务代码分离,降低代码耦合度。
本次项目就是采用AOP机制,实现日志记录,由于项目中没有使用任何安全框架,所以日志记录获取用户的登录信息(登录名,用户姓名等)采用了“获取cookies值”方法实现。
1. 添加依赖
<!--spring切面aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- json解析依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
2. 创建自定义注解@LogAnnotation
由于是创建自定义注解,所以新建class的时候选择的是Annotation
型。
package com.feng.generation_design.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface LogAnnotation {
String message(); // 日志内容
String operation(); // 日志类型
}
3. 创建日志记录类型
该类型很容易理解,如果你想要在用户执行某个添加功能时,启动日志记录,那么就在对应的“添加”控制类上,设定LogType的operation属性为 “ADD”;
package com.feng.generation_design.entity;
public class LogType {
//添加型日志
public static final String ADD = "ADD";
//删除型日志
public static final String DELETE = "DELETE";
//更新类型的日志记录
public static final String UPDATE = "UPDATE";
//查询类型的日志记录
public static final String QUERY = "QUERY";
//登录型日志
public static final String LOGIN = "LOGIN";
//退出登录型的日志记录
public static final String LOGOUT = "LOGOUT";
}
接下去就是创建具体的切面逻辑。
3. 编写切面逻辑
新建一个java.class,名为SystemLogAspect
,它的具体结构如下:
public class SystemLogAspect {
private static Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
//定义切点@PointCut
//在注解位置切入代码,也就是你的自定义注解所在的位置
@Pointcut("@annotation(com.xxx.LogAnnotation)")
public void logPoinCut() {
}
//前置通知
//在执行方法之前打印获取的参数内容
@Before("logPoinCut()")
public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
//将日志实现服务注入到该类中
// @Autowired
// OperateLogServiceimpl operateLogService;
//在这里编写的日志记录代码。
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("URL: {}", request.getRequestURL().toString());
logger.info("HTTP请求类型: {}", request.getMethod());
logger.info("执行方法: {}", joinPoint);
logger.info("传递参数: {}", Arrays.toString(joinPoint.getArgs()));
logger.info("IP地址: {}: " + request.getRemoteAddr());
}
现在可以重启项目,然后可以在某个控制类上加上注解,
@LogAnnotation(message = "用户登录", operation = LogType.LOGIN)
在前台触发加上注解的控制类,看系统后台是否能够正确打印日志信息。成功启动项目,触发日志:
2023-06-07 16:05:05.604 INFO 2892 --- [nio-8080-exec-4] c.f.g.aspect.SystemLogAspect : URL: http://localhost:8080/institute/DeleteCurriculumById/10010
2023-06-07 16:05:05.604 INFO 2892 --- [nio-8080-exec-4] c.f.g.aspect.SystemLogAspect : HTTP请求类型: DELETE
2023-06-07 16:05:05.604 INFO 2892 --- [nio-8080-exec-4] c.f.g.aspect.SystemLogAspect : 执行方法: execution(ApiResult com.feng.generation_design.controller.CurriculumController.DeleteCurriculumById(Integer))
2023-06-07 16:05:05.604 INFO 2892 --- [nio-8080-exec-4] c.f.g.aspect.SystemLogAspect : 传递参数: [10010]
现在就是将你想记录的日志内容,填充到你的before方法中。
4. 完善切面层,获取详细的请求信息
这部分就是在上面的结构基础上,完善我们要记录的日志信息。下面所有的代码都是写到
首先,我们之前提到过,我们自定义的注解,其实有两个参数,分别是operation
和method
属性,我们给不同的控制类设置了不同的属性值,我们怎么在上面的SystemLogAspect
中获取呢?
4.1 获取自定义注解上的属性值
获取method属性
//获取切入点属性
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取操作
LogAnnotation myLog = method.getAnnotation(LogAnnotation.class);
if (myLog != null) {
String value = myLog.message();
System.out.println("获取到的method属性:" + operatingLog.getMessage());
}
4.2 通过Cookies获取用户信息
如果在前端使用了cookies保存用户的一些登录信息。如用户名,id,等信息,那么只需借助声明
HttpServletRequest request = attributes.getRequest();
辅助获得用户数据。
首先,不妨先打印出你的Cookies里面到底有什么。(Cookie采用的都是key-value存储数据的)
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
System.out.println(cookie.getName() + ": " + cookie.getValue());
}
里面的中文信息会有乱码,
对有中文的cookie属性设置如下:
request.setCharacterEncoding("UTF-8");
//上面这个写到最前面
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
System.out.println(cookie.getName() + ": " + cookie.getValue());
//对有中文乱码进行编码设置。
if (cookie.getName().equals("cname")) {
operatingLog.setUserName(URLDecoder.decode(cookie.getValue(), "utf-8"));
}
//对有中文乱码进行编码设置。
if (cookie.getName().equals("cid")) {
operatingLog.setUserId(Integer.parseInt(URLDecoder.decode(cookie.getValue(), "utf-8")));
}
}
4.3 获取执行时间
记录用户执行当前操作的时间,数据库记录时间实际上是varchar类型,实体类也是String类型,所以,在后台直接获得指定格式的时间,转成字符串.。
//获取执行时间
Date day = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
// System.out.println("格式化输出:" + sdf.format(day));
既然能够获取到用户信息,以及执行的控制类信息,那就创建一个日志实体类,将上面收集的信息打包,创建Service层,ServiceImpl类、Mapper层,把上面的数据写入数据库。这部分的内和增删改查业务已经一样了。所以不再赘述。
4.4 日志实体类以及对应数据库类型
@Data
public class OperatingLog implements Serializable {
private Integer logId; //消息id
private Integer userId; //操作用户Id
private String message; //操作内容
private String url; //操作地址
private String ip; //请求Ip
private String date; //日志发生时间
private Long totalTime; //总耗时
private String userName; //操作用户名
private String type; //请求类型
private String params; //传递参数值
}
对应的数据库如下:
operateLogService.add(operatingLog);