前言
平时做项目的时候,习惯于记录系统日志,出错去查看系统日志而对于用户具体做了什么操作无法知道,所以在这里我打算使用Spring时期所学AOP,整合到springboot中实现对用户操作信息的详细记录.
一、目录结构
二、 代码编写:
1. Pom.xml 添加必要依赖
<!--提供JdbcTemplate-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--数据库连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--引入druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.13</version>
</dependency>
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. yml 相关配置
server:
port: 8888
servlet:
context-path: /SpringAop
spring:
datasource:
druid:
# 数据库访问配置, 使用druid数据源
url: jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf8&useSSL=false
username: 你的账号
password: 你的密码
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
3. 自定义注解
定义一个方法级别的@Log注解,用于标注需要监听的方法
@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解类型保留时间的长短
public @interface Log {
String value() default ""; // value是注解描述的意思,应用: @Log("第一个方法执行")
}
4. 创建库表和实体
数据库名称为 springbootdb, 在该数据库中创建一张sys_log表,用于保存用户的操作日志,数据库服务采用MySQL 5.7:
create table sys_log (
ID int auto_increment primary key,
USERNAME varchar(20) null,
OPERATION varchar(20) null,
TIMES int(20) null,
METHOD varchar(200) null,
PARAMS varchar(500) null,
IP varchar(64) null,
CREATETIME date null
);
创建数据库时注意要设置编码格式
character set utf8
表对应的实体类:
@Data
public class SysLog implements Serializable {
private static final long serialVersionUID = -6309732882044872298L; // 序列化
private Integer id;
private String username;
private String operation;
private Integer times;
private String method;
private String params;
private String ip;
private Date createTime;
}
小贴士:引入 lombok 依赖即可使用@Data注解,不需要写getter,setter 方法,自动生成
5. 保存日志的方法
接口:
public interface SysLogDao {
void saveSysLog(SysLog sysLog);
}
实现类:
@Repository
public class SysLogDaoImpl implements SysLogDao {
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public void saveSysLog(SysLog sysLog) {
String sql = "insert into sys_log(username,operation,times,method,params,ip,createTime) values (?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,sysLog.getUsername(), sysLog.getOperation(),
sysLog.getTimes(),sysLog.getMethod(), sysLog.getParams(),sysLog.getIp(),
sysLog.getCreateTime());
}
}
由于没有整合mybatis,所以这里直接使用 spring 的 JdbcTemplate 完成数据库操作工作
6. 封装两个小工具 => HttpContextUtil 、IpUtil
- HttpContextUtil : 方便随时能取到当前请求的request对象
- IpUtil: 获取用户当前的 IP 地址
- HttpContextUtil
public class HttpContextUtil {
/*
* RequestContextHolder : 持有上下文的Request容器
* */
// 获取Request对象
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
// 获取Response对象
public static HttpServletResponse getHttpServletResponse() {
return ((ServletWebRequest)RequestContextHolder.getRequestAttributes()).getResponse();
}
}
- IpUtil
public class IpUtil {
/**
* 获取IP地址
*
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}
7. 切面和切点的代码编写(重点,注解详细)
@Aspect
@Component
public class logAspect {
@Autowired
private SysLogDao sysLogDao;
// 切点
@Pointcut("@annotation(com.xiao.springaop.annotation.Log)") // 所有带有 Log 注解的方法作为 织入点
public void pointcut(){}
@Around("pointcut()") // 增强处理方法 , 参数必须是 Proceedding 类型
public void around(ProceedingJoinPoint point) {
long beginTime = System.currentTimeMillis();
try {
// 执行方法,controller层的方法
point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// 执行时长
long time = System.currentTimeMillis() - beginTime;
// 保存日志
saveLog(point, time);
}
public void saveLog(ProceedingJoinPoint joinPoint, long time) {
// 获取joinPoint的信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取到 Method 对象,可以用于获取方法上面的注解
SysLog sysLog = new SysLog();
Log logAnnotation = method.getAnnotation(Log.class); // 获取Log注解对象
if (logAnnotation != null) {
// 注解上的value描述
sysLog.setOperation(logAnnotation.value());
}
// getTarget()该方法返回被织入增强处理的目标对象
String className = joinPoint.getTarget().getClass().getName(); // 获取类名
String methodName = signature.getName(); // 获取方法名
sysLog.setMethod(className + "." + methodName + "()");
// 请求方法参数名
Object[] args = joinPoint.getArgs();
// 获取方法的参数名
LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = localVariableTableParameterNameDiscoverer.getParameterNames(method);
// 拼接: 参数名:参数值
if (args != null && parameterNames != null) {
String params = "";
for (int i = 0 ; i < args.length; i++) {
params += " " + parameterNames[i] + ": " + args[i];
}
sysLog.setParams(params);
}
// 获取 resquest 对象
HttpServletRequest httpServletRequest = HttpContextUtil.getHttpServletRequest();
// 设置IP地址
sysLog.setIp(IpUtil.getIpAddress(httpServletRequest));
// 模拟用户名
sysLog.setUsername("mr.xiao");
// 方法所用时间
sysLog.setTimes((int) time);
// 日志创建时间
Date date = new Date();
sysLog.setCreateTime(date);
// 保存日志
sysLogDao.saveSysLog(sysLog);
}
}
8. Controller 测试:
测试地址:http://192.168.1.101:8888/SpringAop/
(博主的电脑本地IP地址,仅做参考)
@RestController
public class TestContoller {
@Log("方法一执行")
@GetMapping("/method_one")
public void methodOne(String name) {
}
@Log("方法二执行")
@GetMapping("/method_two")
public void methodTwo() throws InterruptedException {
Thread.sleep(2000);
}
@Log("方法三执行")
@GetMapping("/method_three")
public void methodThree(String name, String age) throws InterruptedException {
}
}
为了更好的看到 IPUtil 工具类IP获取的效果,我使用 linux 模拟另一台主机的访问,结果如下: