在Spring框架中,使用AOP配合自定义注解可以方便的实现用户操作的监控。首先搭建一个基本的Spring Boot Web环境开启Spring Boot,然后引入必要依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- oracle驱动 -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>6.0</version>
</dependency>
<!-- druid数据源驱动 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.6</version>
</dependency>
自定义注解
定义一个方法级别的@Log
注解,用于标注需要监控的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
创建库表和实体
在数据库中创建一张sys_log表,用于保存用户的操作日志,数据库采用oracle 11g:
CREATE TABLE "SCOTT"."SYS_LOG" (
"ID" NUMBER(20) NOT NULL ,
"USERNAME" VARCHAR2(50 BYTE) NULL ,
"OPERATION" VARCHAR2(50 BYTE) NULL ,
"TIME" NUMBER(11) NULL ,
"METHOD" VARCHAR2(200 BYTE) NULL ,
"PARAMS" VARCHAR2(500 BYTE) NULL ,
"IP" VARCHAR2(64 BYTE) NULL ,
"CREATE_TIME" DATE NULL
);
COMMENT ON COLUMN "SCOTT"."SYS_LOG"."USERNAME" IS '用户名';
COMMENT ON COLUMN "SCOTT"."SYS_LOG"."OPERATION" IS '用户操作';
COMMENT ON COLUMN "SCOTT"."SYS_LOG"."TIME" IS '响应时间';
COMMENT ON COLUMN "SCOTT"."SYS_LOG"."METHOD" IS '请求方法';
COMMENT ON COLUMN "SCOTT"."SYS_LOG"."PARAMS" IS '请求参数';
COMMENT ON COLUMN "SCOTT"."SYS_LOG"."IP" IS 'IP地址';
COMMENT ON COLUMN "SCOTT"."SYS_LOG"."CREATE_TIME" IS '创建时间';
CREATE SEQUENCE seq_sys_log START WITH 1 INCREMENT BY 1;
库表对应的实体:
public class SysLog implements Serializable{
private static final long serialVersionUID = -6309732882044872298L;
private Integer id;
private String username;
private String operation;
private Integer time;
private String method;
private String params;
private String ip;
private Date createTime;
// get,set略
}
保存日志的方法
为了方便,这里直接使用Spring JdbcTemplate来操作数据库。定义一个SysLogDao接口,包含一个保存操作日志的抽象方法:
public interface SysLogDao {
void saveSysLog(SysLog syslog);
}
其实现方法:
@Repository
public class SysLogDaoImp implements SysLogDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void saveSysLog(SysLog syslog) {
StringBuffer sql = new StringBuffer("insert into sys_log ");
sql.append("(id,username,operation,time,method,params,ip,create_time) ");
sql.append("values(seq_sys_log.nextval,:username,:operation,:time,:method,");
sql.append(":params,:ip,:createTime)");
NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(this.jdbcTemplate.getDataSource());
npjt.update(sql.toString(), new BeanPropertySqlParameterSource(syslog));
}
}
这里采用MySQL数据库,由于MySQL没有自增序列,采用自建自增序列表:
#序列表
CREATE TABLE sequence (
seq_name varchar(50) NOT NULL, -- 序列名称
current_val int(11) NOT NULL, -- 当前值
increment_val int(11) NOT NULL, -- 跨度
PRIMARY KEY (seq_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
set global log_bin_trust_function_creators=TRUE;
#获取当前值的函数
CREATE FUNCTION currval (v_seq_name VARCHAR(50)) RETURNS INTEGER
BEGIN
DECLARE current INTEGER;
SET current = 0;
SELECT
current_val INTO current
FROM
sequence
WHERE
seq_name = v_seq_name;
RETURN current;
END;
#获取下一个值的函数
CREATE FUNCTION nextval (v_seq_name VARCHAR(50)) RETURNS INTEGER
BEGIN
DECLARE current INTEGER;
SET current = 0;
SELECT
current_val + increment_val INTO current
FROM
sequence
WHERE
seq_name = v_seq_name FOR UPDATE;
UPDATE sequence
SET current_val = current
WHERE
seq_name = v_seq_name;
RETURN current;
END;
INSERT INTO sequence (`seq_name`, `current_val`, `increment_val`) VALUES ('seq_sys_log', '1', '1');
@Override
public void saveSysLog(SysLog syslog) {
String sql_seq = "SELECT nextval('seq_sys_log')";
Integer seq = this.jdbcTemplate.queryForList(sql_seq,Integer.class).get(0);
StringBuffer sql = new StringBuffer("insert into sys_log ");
sql.append("(id,username,operation,time,method,params,ip,create_time) ");
sql.append("values(:seq,:username,:operation,:time,:method,");
sql.append(":params,:ip,:createTime)");
MapSqlParameterSource map = new MapSqlParameterSource();
map.addValue("seq", seq);
map.addValue("username", syslog.getUsername());
map.addValue("operation", syslog.getOperation());
map.addValue("time", syslog.getTime());
map.addValue("method", syslog.getMethod());
map.addValue("params", syslog.getParams());
map.addValue("ip", syslog.getIp());
map.addValue("createTime", syslog.getCreateTime());
NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(this.jdbcTemplate.getDataSource());
npjt.update(sql.toString(), map);
}
切面和切点
定义一个LogAspect类,使用@Aspect
标注让其成为一个切面,切点为使用@Log
注解标注的方法,使用@Around
环绕通知:
@Aspect
@Component
public class LogAspect {
@Autowired
private SysLogDao sysLogDao;
@Pointcut("@annotation(com.springboot.annotation.Log)")
public void pointcut() { }
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) {
Object result = null;
long beginTime = System.currentTimeMillis();
try {
// 执行方法
result = point.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
// 保存日志
saveLog(point, time);
return result;
}
private void saveLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLog sysLog = new SysLog();
Log logAnnotation = method.getAnnotation(Log.class);
if (logAnnotation != null) {
// 注解上的描述
sysLog.setOperation(logAnnotation.value());
}
// 请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
// 请求的方法参数值
Object[] args = joinPoint.getArgs();
// 请求的方法参数名称
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
String params = "";
for (int i = 0; i < args.length; i++) {
params += " " + paramNames[i] + ": " + args[i];
}
sysLog.setParams(params);
}
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 设置IP地址
sysLog.setIp(IPUtils.getIpAddr(request));
// 模拟一个用户名
sysLog.setUsername("mrbird");
sysLog.setTime((int) time);
sysLog.setCreateTime(new Date());
// 保存系统日志
sysLogDao.saveSysLog(sysLog);
}
}
测试
TestController:
@RestController
public class TestController {
@Log("执行方法一")
@GetMapping("/one")
public void methodOne(String name) { }
@Log("执行方法二")
@GetMapping("/two")
public void methodTwo() throws InterruptedException {
Thread.sleep(2000);
}
@Log("执行方法三")
@GetMapping("/three")
public void methodThree(String name, String age) { }
}
最终项目目录如下图所示:
启动项目,分别访问:
- http://localhost:8080/web/one?name=KangKang
- http://localhost:8080/web/two
- http://localhost:8080/web/three?name=Mike&age=25
查询数据库:
SQL> select * from sys_log order by id;
ID USERNAME OPERATION TIME METHOD PARAMS IP CREATE_TIME
---------- ---------- ---------- ---------- ------------------------------ ------------------------------ ---------- --------------
11 mrbird 执行方法一 6 com.springboot.controller.Test name: KangKang 127.0.0.1 08-12月-17
Controller.methodOne()
12 mrbird 执行方法二 2000 com.springboot.controller.Test 127.0.0.1 08-12月-17
Controller.methodTwo()
13 mrbird 执行方法三 0 com.springboot.controller.Test name: Mike age: 25 127.0.0.1 08-12月-17
Controller.methodThree()