1. 需求
我们经常会有这样的需求,需要对关键的业务功能做操作日志记录,也就是用户在指定的时间操作了哪个功能,操作前后的数据记录,必要的时候可以一键回退,今天我就为大家实现这个的功能,让大家可以直接拿来用。
1.1 日志分类
- 系统日志:指的是程序执行过程中的关键步骤记录,根据实际输出的debug、info、warn、error等级别的执行记录信息,主要用来记录核心参数以及返回值,同时为了在出现问题的时候能够快速排查问题
- 操作日志:它是用户实际业务操作行为的记录,主要是为了分析用户的行为偏好,一方面用来对业务进行分析,开发人员也可以针对访问的频率去提高特定接口的性能。
2. 设计思路
首先我们得要分析具体要实现这个功能,都需要记录哪些信息,然后怎么要去实现这个功能?
具体功能点:
- 记录用户的业务操作行为,具体字段有:操作人、操作时间、操作功能、日志类型、操作内容描述、操作内容、操作前内容。
- 需要对外提供页面和管理功能,以便查询追溯和数据回滚。
我们首先要想到,要实现这个功能,最好的办法就是需要和业务逻辑进行解耦,因为它是一种业务辅助功能,同时是对所有业务的一种横向操作,那我们使用什么技术,是不是就呼之欲出了?最容易想到的就是AOP切面+自定义注解。
2.1 实现步骤
1、首先定义操作日志注解,注解内定义一些属性,如操作功能名称、描述等;
2、将自定义注解标记在需要进行业务操作记录的方法上,一般查询不需要。
3、定义切入点,编写切面:切入点就是标记业务操作日志注解的目标方法;切面就是保存业务操作日志信息。
对了,在这里我想给大家说一下咱们经常用到的几个技术的区别,过滤器,拦截器,SpringAOP,这个也是我经常的困惑。
2.2 SpringAOP、过滤器、拦截器对比
在匹配中同一目标时,过滤器、拦截器、SpringAOP的执行优先级是:过滤器>拦截器>SpringAOP,执行顺序是先进后出,主要有如下区别:
- 过滤器(Filter)
过滤器,就是起到过滤筛选作用的一种事物,只不过这里的过滤器过滤的对象是客户端访问的web资源。也可以理解为一种预处理手段,对资源进行拦截后,将其中我们认为的杂质(用户自己定义的)过滤,符合条件的放行,不符合的则拦截下来。
过滤器常见的使用场景:统一设置编码,过滤敏感字符,登录校验,URL级别的访问权限控制,数据压缩。
2.拦截器(Interceptor)
拦截器是springmvc提供的,类似于过滤器。主要用于拦截用户请求并作相应的处理。
拦截器的使用场景: 日志记录,权限校验,登录校验,性能检测,经常会用在网关处。
3:AOP
AOP拦截的是类的元数据(包、类、方法名、参数等),AOP针对具体的代码,能够实现更加复杂的业务逻辑。
使用的场景:日志记录,性能统计,安全控制,事务处理,异常处理。
3. 具体实现
3.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 数据库表设计
create table if not exists bus_log
(
id bigint auto_increment comment '自增id'
primary key,
bus_name varchar(100) null comment '业务名称',
bus_descrip varchar(255) null comment '业务操作描述',
oper_person varchar(100) null comment '操作人',
oper_time datetime null comment '操作时间',
ip_from varchar(50) null comment '操作来源ip',
param_file varchar(255) null comment '操作参数报文文件'
)
comment '业务操作日志' default charset ='utf8';
3.3 代码实现
- 定义日志注解
/**
* 业务日志注解
* 可以作用在控制器或其他业务类上,用于描述当前类的功能;
* 也可以用于方法上,用于描述当前方法的作用;
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BusLog {
/**
* 功能名称
* @return
*/
String name() default "";
/**
* 功能描述
* @return
*/
String descrip() default "";
}
- 编写切面
主要步骤是:在环绕通知内执行过目标方法后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述, 把方法的参数报文写入到文件中,最后保存业务操作日志信息;
@Component
@Aspect
@Slf4j
public class BusLogAop implements Ordered {
@Autowired
private BusLogDao busLogDao;
/**
* 定义BusLogAop的切入点为标记@BusLog注解的方法
*/
@Pointcut(value = "@annotation(com.wuk.BusLog)")
public void pointcut() {
}
/**
* 业务操作环绕通知
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
log.info("----BusAop 环绕通知 start");
//执行目标方法
Object result = null;
try {
result = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//目标方法执行完成后,获取目标类、目标方法上的业务日志注解上的功能名称和功能描述
Object target = proceedingJoinPoint.getTarget();
Object[] args = proceedingJoinPoint.getArgs();
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
BusLog anno1 = target.getClass().getAnnotation(BusLog.class);
BusLog anno2 = signature.getMethod().getAnnotation(BusLog.class);
BusLogBean busLogBean = new BusLogBean();
String logName = anno1.name();
String logDescrip = anno2.descrip();
busLogBean.setBusName(logName);
busLogBean.setBusDescrip(logDescrip);
busLogBean.setOperPerson("wuk");
busLogBean.setOperTime(new Date());
JsonMapper jsonMapper = new JsonMapper();
String json = null;
try {
json = jsonMapper.writeValueAsString(args);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
//把参数报文写入到文件中
OutputStream outputStream = null;
try {
String paramFilePath = System.getProperty("user.dir") + File.separator + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN) + ".log";
outputStream = new FileOutputStream(paramFilePath);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
busLogBean.setParamFile(paramFilePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//保存业务操作日志信息
this.busLogDao.insert(busLogBean);
log.info("----BusAop 环绕通知 end");
return result;
}
@Override
public int getOrder() {
return 1;
}
}
- 业务接口添加注解
@RestController
@Slf4j
@RequestMapping("/person")
public class PersonController {
@Autowired
private IPersonService personService;
@PostMapping
@BusLog(name="添加人员信息",descrip = "添加人员信息")
public Person add(@RequestBody Person person) {
Person result = this.personService.registe(person);
log.info("//增加person执行完成");
return result;
}
@PutMapping
@BusLog(name="修改人员信息",descrip = "修改人员信息")
public void edit(@RequestBody Person person) {
this.personService.update(person);
}
@DeleteMapping
@BusLog(name="删除人员信息", descrip = "删除人员信息")
public void delete(@PathVariable(name = "id") Integer id) {
this.personService.delete(id);
}
}