在实际的项目中, 特别是管理系统中, 对于那些重要的操作我们通常都会记录操作日志. 比如对数据库的 CRUD 操作, 我们都会对每一次重要的操作进行记录, 通常的做法是向数据库指定的日志表中插入一条记录. 这里就产生了一个问题, 难道要我们每次在 CRUD 的时候都手动的插入日志记录吗? 这肯定是不合适的, 这样的操作无疑是加大了开发量, 而且不易维护, 所以实际项目中总是利用 AOP(Aspect Oriented Programming) 即面向切面编程这一技术来记录系统中的操作日志.
日志分类
这里我把日志按照面向的对象不同分为两类:
CRUD
面向不同对象的日志, 我们采用不同的策略去记录. 很容易看出, 对于面向用户的日志具有很强的灵活性, 需要开发者控制用户的哪些操作需要向数据库记录日志, 所以这一类保存在数据库的日志我们在使用 AOP 记录时用自定义注解的方式去匹配; 而面向开发者的日志我们则使用表达式去匹配就可以了 (这里有可能叙述的有点模糊, 看了下面去案例将会很清晰), 下面分别介绍两种日志的实现.
实现 AOP 记录面向用户的日志
接下来分步骤介绍 Spring boot 中怎样实现通过 AOP 记录操作日志.
添加依赖
在 pom.xml 文件中添加如下依赖:
- <!-- aop 依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-aop</artifactId>
- </dependency>
修改配置文件
在项目的 application.properties 文件中添加下面一句配置:
spring.aop.auto=true
这里特别说明下, 这句话不加其实也可以, 因为默认就是 true , 只要我们在 pom.xml 中添加了依赖就可以了, 这里提出来是让大家知道有这个有这个配置.
自定义注解
上边介绍过了了, 因为这类日志比较灵活, 所以我们需要自定义一个注解, 使用的时候在需要记录日志的方法上添加这个注解就可以了, 首先在启动类的同级包下边新建一个 config 包, 在这个报下边新建 new 一个名为 Log 的 Annotation 文件, 文件内容如下:
- package com.web.springbootaoplog.config;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- /**
- * @author Promise
- * @createTime 2018 年 12 月 18 日 下午 9:26:25
- * @description 定义一个方法级别的 @log 注解
- */
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Log {
- String value() default "";
- }
这里用到的是 Java 元注解的相关知识, 不清楚相关概念的朋友可以去这篇博客 get 一下 [ 传送门 https://josh-persistence.iteye.com/blog/2226493 ] .
准备数据库日志表以及实体类, sql 接口, xml 文件
既然是向数据库中插入记录, 那么前提是需要创建一张记录日志的表, 下面给出我的表 sql , 由于是写样例, 我这里这张表设计的很简单, 大家可以自行设计.
- CREATE TABLE `sys_log` (
- `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
- `user_id` int(11) NOT NULL COMMENT '操作员 id',
- `user_action` varchar(255) NOT NULL COMMENT '用户操作',
- `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COMMENT='日志记录表';
通过上篇博客介绍的 MBG 生成相应的实体类, sql 接口文件, 以及 xml 文件, 这里不再概述, 不清楚的朋友请移步 [传送门]
当然还需要创建 service 接口文件以及接口实现类, 这里直接给出代码:
ISysLogServcie.java
- package com.Web.springbootaoplog.service;
- import com.Web.springbootaoplog.entity.SysLog;
- /**
- * @author Promise
- * @createTime 2018 年 12 月 18 日 下午 9:29:48
- * @description 日志接口
- */
- public interface ISysLogService {
- /**
- * 插入日志
- * @param entity
- * @return
- */
- int insertLog(SysLog entity);
- }
SysLogServiceImpl.java
- package com.Web.springbootaoplog.service.impl;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import com.Web.springbootaoplog.config.Log;
- import com.Web.springbootaoplog.dao.SysLogMapper;
- import com.Web.springbootaoplog.entity.SysLog;
- import com.Web.springbootaoplog.service.ISysLogService;
- /**
- * @author Promise
- * @createTime 2018 年 12 月 18 日 下午 9:30:57
- * @description
- */
- @Service("sysLogService")
- public class SysLogServiceImpl implements ISysLogService{
- @Autowired
- private SysLogMapper sysLogMapper;
- @Override
- public int insertLog(SysLog entity) {
- // TODO Auto-generated method stub
- return sysLogMapper.insert(entity);
- }
- }
AOP 的切面和切点
准备上边的相关文件后, 下面来介绍重点 - 创建 AOP 切面实现类, 同样我们这里将该类放在 config 包下, 命名为 LogAsPect.java , 内容如下:
- package com.Web.springbootaoplog.config;
- import java.lang.reflect.Method;
- import java.util.Arrays;
- import java.util.Date;
- import org.aspectj.lang.JoinPoint;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Before;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.hibernate.validator.internal.util.logging.LoggerFactory;
- import org.slf4j.Logger;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
- import org.springframework.stereotype.Component;
- import com.Web.springbootaoplog.entity.SysLog;
- import com.Web.springbootaoplog.service.ISysLogService;
- /**
- * @author Promise
- * @createTime 2018 年 12 月 18 日 下午 9:33:28
- * @description 切面日志配置
- */
- @Aspect
- @Component
- public class LogAsPect {
- private final static Logger log = org.slf4j.LoggerFactory.getLogger(LogAsPect.class);
- @Autowired
- private ISysLogService sysLogService;
- // 表示匹配带有自定义注解的方法
- @Pointcut("@annotation(com.web.springbootaoplog.config.Log)")
- public void pointcut() {}
- @Around("pointcut()")
- public Object around(ProceedingJoinPoint point) {
- Object result =null;
- long beginTime = System.currentTimeMillis();
- try {
- log.info("我在目标方法之前执行!");
- result = point.proceed();
- long endTime = System.currentTimeMillis();
- insertLog(point,endTime-beginTime);
- } catch (Throwable e) {
- // TODO Auto-generated catch block
- }
- return result;
- }
- private void insertLog(ProceedingJoinPoint point ,long time) {
- MethodSignature signature = (MethodSignature)point.getSignature();
- Method method = signature.getMethod();
- SysLog sys_log = new SysLog();
- Log userAction = method.getAnnotation(Log.class);
- if (userAction != null) {
- // 注解上的描述
- sys_log.setUserAction(userAction.value());
- }
- // 请求的类名
- String className = point.getTarget().getClass().getName();
- // 请求的方法名
- String methodName = signature.getName();
- // 请求的方法参数值
- String args = Arrays.toString(point.getArgs());
- // 从 session 中获取当前登陆人 id
- // Long useride = (Long)SecurityUtils.getSubject().getSession().getAttribute("userid");
- Long userid = 1L;// 应该从 session 中获取当前登录人的 id, 这里简单模拟下
- sys_log.setUserId(userid);
- sys_log.setCreateTime(new java.sql.Timestamp(new Date().getTime()));
- log.info("当前登陆人:{}, 类名:{}, 方法名:{}, 参数:{}, 执行时间:{}",userid, className, methodName, args, time);
- sysLogService.insertLog(sys_log);
- }
- }
这里简单介绍下关于 AOP 的几个重要注解:
@Aspect : 这个注解表示将当前类视为一个切面类
@Component : 表示将当前类交由 Spring 管理.
@Pointcut : 切点表达式, 定义我们的匹配规则, 上边我们使用
@Pointcut("@annotation(com.web.springbootaoplog.config.Log)")
表示匹配带有我们自定义注解的方法.
@Around : 环绕通知, 可以在目标方法执行前后执行一些操作, 以及目标方法抛出异常时执行的操作.
我们用到的注解就这几个, 当然还有其他的注解, 这里我就不一一介绍了, 想要深入了解 AOP 相关知识的朋友可以移步官方文档 [ 传送门 ]
下面看一段关键的代码:
- log.info("我在目标方法之前执行!");
- result = point.proceed();
- long endTime = System.currentTimeMillis();
- insertLog(point,endTime-beginTime);
其中 result = point.proceed(); 这句话表示执行目标方法, 可以看出我们在这段代码执行之前打印了一句日志, 并在执行之后调用了 insertLog() 插入日志的方法, 并且在方法中我们可以拿到目标方法所在的类名, 方法名, 参数等重要的信息.
测试控制器
在 controller 包下新建一个 HomeCOntroller.java (名字大家随意), 内容如下:
- package com.Web.springbootaoplog.controller;
- import java.util.HashMap;
- import java.util.Map;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.Web.bind.annotation.RequestBody;
- import org.springframework.Web.bind.annotation.RequestMapping;
- import org.springframework.Web.bind.annotation.RequestMethod;
- import org.springframework.Web.bind.annotation.ResponseBody;
- import com.Web.springbootaoplog.config.Log;
- import com.Web.springbootaoplog.entity.SysLog;
- import com.Web.springbootaoplog.service.ISysLogService;
- /**
- * @author Promise
- * @createTime 2019 年 1 月 2 日 下午 10:35:30
- * @description 测试 controller
- */
- @Controller
- public class HomeController {
- private final static Logger log = org.slf4j.LoggerFactory.getLogger(HomeController.class);
- @Autowired
- private ISysLogService logService;
- @RequestMapping("/aop")
- @ResponseBody
- @Log("测试 aoplog")
- public Object aop(String name, String nick) {
- Map<String, Object> map =new HashMap<>();
- log.info("我被执行了!");
- map.put("res", "ok");
- return map;
- }
- }
定义一个测试方法, 带有两个参数, 并且为该方法添加了我们自定义的 @Log 注解, 启动项目, 浏览器访问 localhost:8080/aop?name=xfr&nick=eran , 这时候查看 eclipse 控制台的部分输出信息如下:
2019-01-24 22:02:17.682 INFO 3832 --- [nio-8080-exec-1] c.Web.springbootaoplog.config.LogAsPect : 我在目标方法之前执行!
2019-01-24 22:02:17.688 INFO 3832 --- [nio-8080-exec-1] c.w.s.controller.HomeController : 我被执行了!
2019-01-24 22:02:17.689 INFO 3832 --- [nio-8080-exec-1] c.Web.springbootaoplog.config.LogAsPect : 当前登陆人: 1, 类名: com.Web.springbootaoplog.controller.HomeController, 方法名: aop, 参数:[xfr, eran], 执行时间: 6
可以看到我们成功在目标方法执行前后插入了一些逻辑代码, 现在再看数据库里边的数据:
成功记录了一条数据.
实现 AOP 记录面向开发者的日志
首先这里我列举一个使用该方式的应用场景, 在项目中出现了 bug , 我们想要知道前台的请求是否进入了我们控制器中, 以及参数的获取情况, 下面开始介绍实现步骤.
其实原理跟上边是一样的, 只是切点的匹配规则变了而已, 而且不用将日志记录到数据库, 打印出来即可.
首先在 LogAsPect.java 中定义一个新的切点表达式, 如下:
- @Pointcut("execution(public * com.web.springbootaoplog.controller..*.*(..))")
- public void pointcutController() {
- }
@Pointcut("execution(public * com.web.springbootaoplog.controller..*.*(..))") 表示匹配 com.Web.springbootaoplog.controller 包及其子包下的所有公有方法.
关于这个表达式详细的使用方法可以移步这里,[ 传送门 https://www.cnblogs.com/liaojie970/p/7883687.html ]
再添加匹配到方法时我们要做的操作:
- @Before("pointcutController()")
- public void around2(JoinPoint point) {
- // 获取目标方法
- String methodNam = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
- // 获取方法参数
- String params = Arrays.toString(point.getArgs());
- log.info("get in {} params :{}",methodNam,params);
- }
@Before : 表示目标方法执行之前执行以下方法体的内容.
再在控制器中添加一个测试方法:
- @RequestMapping("/testaop3")
- @ResponseBody
- public Object testAop3(String name, String nick) {
- Map<String, Object> map = new HashMap<>();
- map.put("res", "ok");
- return map;
- }
可以看到这个方法我们并没有加上 @Log 注解, 重启项目, 浏览器访问 localhost:8080/testaop3?name=xfr&nick=eran, 这时候查看 eclipse 控制台的部分输出信息如下:
2019-01-24 23:19:49.108 INFO 884 --- [nio-8080-exec-1] c.Web.springbootaoplog.config.LogAsPect : get in com.Web.springbootaoplog.controller.HomeController.testAop3 params :[xfr, eran]
打印出了关键日志, 这样我们就能知道是不是进入了该方法, 参数获取是否正确等关键信息.
这里有的朋友或许会有疑问这样会不会与添加了 @Log 的方法重复了呢, 的确会, 所以在项目中我通常都将 @Log 注解用在了 Service 层的方法上, 这样也更加合理.
来源: http://www.tuicool.com/articles/7BFnEfi