在Spring框架中,使用AOP配合自定义注解可以方便的实现用户操作的监控。首先搭建一个基本的Spring Boot Web环境开启Spring Boot,然后引入必要依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <!-- aop依赖 -->
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-aop</artifactId>
  9. </dependency>
  10. <!-- oracle驱动 -->
  11. <dependency>
  12. <groupId>com.oracle</groupId>
  13. <artifactId>ojdbc6</artifactId>
  14. <version>6.0</version>
  15. </dependency>
  16. <!-- druid数据源驱动 -->
  17. <dependency>
  18. <groupId>com.alibaba</groupId>
  19. <artifactId>druid-spring-boot-starter</artifactId>
  20. <version>1.1.6</version>
  21. </dependency>

自定义注解

定义一个方法级别的@Log注解,用于标注需要监控的方法:

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface Log {
  4. String value() default "";
  5. }

创建库表和实体

在数据库中创建一张sys_log表,用于保存用户的操作日志,数据库采用oracle 11g:

  1. CREATE TABLE "SCOTT"."SYS_LOG" (
  2. "ID" NUMBER(20) NOT NULL ,
  3. "USERNAME" VARCHAR2(50 BYTE) NULL ,
  4. "OPERATION" VARCHAR2(50 BYTE) NULL ,
  5. "TIME" NUMBER(11) NULL ,
  6. "METHOD" VARCHAR2(200 BYTE) NULL ,
  7. "PARAMS" VARCHAR2(500 BYTE) NULL ,
  8. "IP" VARCHAR2(64 BYTE) NULL ,
  9. "CREATE_TIME" DATE NULL
  10. );
  11. COMMENT ON COLUMN "SCOTT"."SYS_LOG"."USERNAME" IS '用户名';
  12. COMMENT ON COLUMN "SCOTT"."SYS_LOG"."OPERATION" IS '用户操作';
  13. COMMENT ON COLUMN "SCOTT"."SYS_LOG"."TIME" IS '响应时间';
  14. COMMENT ON COLUMN "SCOTT"."SYS_LOG"."METHOD" IS '请求方法';
  15. COMMENT ON COLUMN "SCOTT"."SYS_LOG"."PARAMS" IS '请求参数';
  16. COMMENT ON COLUMN "SCOTT"."SYS_LOG"."IP" IS 'IP地址';
  17. COMMENT ON COLUMN "SCOTT"."SYS_LOG"."CREATE_TIME" IS '创建时间';
  18. CREATE SEQUENCE seq_sys_log START WITH 1 INCREMENT BY 1;

库表对应的实体:

  1. public class SysLog implements Serializable{
  2. private static final long serialVersionUID = -6309732882044872298L;
  3. private Integer id;
  4. private String username;
  5. private String operation;
  6. private Integer time;
  7. private String method;
  8. private String params;
  9. private String ip;
  10. private Date createTime;
  11. // get,set略
  12. }

保存日志的方法

为了方便,这里直接使用Spring JdbcTemplate来操作数据库。定义一个SysLogDao接口,包含一个保存操作日志的抽象方法:

  1. public interface SysLogDao {
  2. void saveSysLog(SysLog syslog);
  3. }

其实现方法:

  1. @Repository
  2. public class SysLogDaoImp implements SysLogDao {
  3. @Autowired
  4. private JdbcTemplate jdbcTemplate;
  5. @Override
  6. public void saveSysLog(SysLog syslog) {
  7. StringBuffer sql = new StringBuffer("insert into sys_log ");
  8. sql.append("(id,username,operation,time,method,params,ip,create_time) ");
  9. sql.append("values(seq_sys_log.nextval,:username,:operation,:time,:method,");
  10. sql.append(":params,:ip,:createTime)");
  11. NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(this.jdbcTemplate.getDataSource());
  12. npjt.update(sql.toString(), new BeanPropertySqlParameterSource(syslog));
  13. }
  14. }

这里采用MySQL数据库,由于MySQL没有自增序列,采用自建自增序列表:

  1. #序列表
  2. CREATE TABLE sequence (
  3. seq_name varchar(50) NOT NULL, -- 序列名称
  4. current_val int(11) NOT NULL, -- 当前值
  5. increment_val int(11) NOT NULL, -- 跨度
  6. PRIMARY KEY (seq_name)
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  8. set global log_bin_trust_function_creators=TRUE;
  9. #获取当前值的函数
  10. CREATE FUNCTION currval (v_seq_name VARCHAR(50)) RETURNS INTEGER
  11. BEGIN
  12. DECLARE current INTEGER;
  13. SET current = 0;
  14. SELECT
  15. current_val INTO current
  16. FROM
  17. sequence
  18. WHERE
  19. seq_name = v_seq_name;
  20. RETURN current;
  21. END;
  22. #获取下一个值的函数
  23. CREATE FUNCTION nextval (v_seq_name VARCHAR(50)) RETURNS INTEGER
  24. BEGIN
  25. DECLARE current INTEGER;
  26. SET current = 0;
  27. SELECT
  28. current_val + increment_val INTO current
  29. FROM
  30. sequence
  31. WHERE
  32. seq_name = v_seq_name FOR UPDATE;
  33. UPDATE sequence
  34. SET current_val = current
  35. WHERE
  36. seq_name = v_seq_name;
  37. RETURN current;
  38. END;
  39. INSERT INTO sequence (`seq_name`, `current_val`, `increment_val`) VALUES ('seq_sys_log', '1', '1');
  1. @Override
  2. public void saveSysLog(SysLog syslog) {
  3. String sql_seq = "SELECT nextval('seq_sys_log')";
  4. Integer seq = this.jdbcTemplate.queryForList(sql_seq,Integer.class).get(0);
  5. StringBuffer sql = new StringBuffer("insert into sys_log ");
  6. sql.append("(id,username,operation,time,method,params,ip,create_time) ");
  7. sql.append("values(:seq,:username,:operation,:time,:method,");
  8. sql.append(":params,:ip,:createTime)");
  9. MapSqlParameterSource map = new MapSqlParameterSource();
  10. map.addValue("seq", seq);
  11. map.addValue("username", syslog.getUsername());
  12. map.addValue("operation", syslog.getOperation());
  13. map.addValue("time", syslog.getTime());
  14. map.addValue("method", syslog.getMethod());
  15. map.addValue("params", syslog.getParams());
  16. map.addValue("ip", syslog.getIp());
  17. map.addValue("createTime", syslog.getCreateTime());
  18. NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(this.jdbcTemplate.getDataSource());
  19. npjt.update(sql.toString(), map);
  20. }

切面和切点

定义一个LogAspect类,使用@Aspect标注让其成为一个切面,切点为使用@Log注解标注的方法,使用@Around环绕通知:

  1. @Aspect
  2. @Component
  3. public class LogAspect {
  4. @Autowired
  5. private SysLogDao sysLogDao;
  6. @Pointcut("@annotation(com.springboot.annotation.Log)")
  7. public void pointcut() { }
  8. @Around("pointcut()")
  9. public Object around(ProceedingJoinPoint point) {
  10. Object result = null;
  11. long beginTime = System.currentTimeMillis();
  12. try {
  13. // 执行方法
  14. result = point.proceed();
  15. } catch (Throwable e) {
  16. e.printStackTrace();
  17. }
  18. // 执行时长(毫秒)
  19. long time = System.currentTimeMillis() - beginTime;
  20. // 保存日志
  21. saveLog(point, time);
  22. return result;
  23. }
  24. private void saveLog(ProceedingJoinPoint joinPoint, long time) {
  25. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  26. Method method = signature.getMethod();
  27. SysLog sysLog = new SysLog();
  28. Log logAnnotation = method.getAnnotation(Log.class);
  29. if (logAnnotation != null) {
  30. // 注解上的描述
  31. sysLog.setOperation(logAnnotation.value());
  32. }
  33. // 请求的方法名
  34. String className = joinPoint.getTarget().getClass().getName();
  35. String methodName = signature.getName();
  36. sysLog.setMethod(className + "." + methodName + "()");
  37. // 请求的方法参数值
  38. Object[] args = joinPoint.getArgs();
  39. // 请求的方法参数名称
  40. LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
  41. String[] paramNames = u.getParameterNames(method);
  42. if (args != null && paramNames != null) {
  43. String params = "";
  44. for (int i = 0; i < args.length; i++) {
  45. params += " " + paramNames[i] + ": " + args[i];
  46. }
  47. sysLog.setParams(params);
  48. }
  49. // 获取request
  50. HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
  51. // 设置IP地址
  52. sysLog.setIp(IPUtils.getIpAddr(request));
  53. // 模拟一个用户名
  54. sysLog.setUsername("mrbird");
  55. sysLog.setTime((int) time);
  56. sysLog.setCreateTime(new Date());
  57. // 保存系统日志
  58. sysLogDao.saveSysLog(sysLog);
  59. }
  60. }

测试

TestController:

  1. @RestController
  2. public class TestController {
  3. @Log("执行方法一")
  4. @GetMapping("/one")
  5. public void methodOne(String name) { }
  6. @Log("执行方法二")
  7. @GetMapping("/two")
  8. public void methodTwo() throws InterruptedException {
  9. Thread.sleep(2000);
  10. }
  11. @Log("执行方法三")
  12. @GetMapping("/three")
  13. public void methodThree(String name, String age) { }
  14. }

最终项目目录如下图所示:

Spring Boot AOP记录用户操作日志 - 图1

启动项目,分别访问:

查询数据库:

  1. SQL> select * from sys_log order by id;
  2. ID USERNAME OPERATION TIME METHOD PARAMS IP CREATE_TIME
  3. ---------- ---------- ---------- ---------- ------------------------------ ------------------------------ ---------- --------------
  4. 11 mrbird 执行方法一 6 com.springboot.controller.Test name: KangKang 127.0.0.1 08-12月-17
  5. Controller.methodOne()
  6. 12 mrbird 执行方法二 2000 com.springboot.controller.Test 127.0.0.1 08-12月-17
  7. Controller.methodTwo()
  8. 13 mrbird 执行方法三 0 com.springboot.controller.Test name: Mike age: 25 127.0.0.1 08-12月-17
  9. Controller.methodThree()