在项目里我们常常有一种需求,要在系统中记录用户的操作情况,需要记录用户请求的路径,表单参数等信息。针对这类需求,倘若对所有的请求都记录操作日志,当在访问量大并且系统功能点多的情况下日志量可能也会非常可观,然而其中一些可有可无的操作并不是我们非常需要关注的,这种情况下我们可以根据需要,选择性的对部分重要业务的操作进行记录,同时我希望这样的操作日志可以由框架统一来处理,不需要在开发业务模块的过程中过多的关注记录日志的过程,所以这里我们可以通过自定义注解,以及AOP的方式来将日志记录在数据库中或者文件当中。这里我们的业务非常的简单就记录到数据库中,方便展示。
1.首先自定义注解
/**
* 定义操作日志注解
* @author admin
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SysOperationLog {
String value() default "";
}
这里@Target(ElementType.METHOD)
标记表示我们此处的自定义注解应用在方法上。当然,还有其他的类型:@Target(ElementType.TYPE)
//类、接口、枚举、注解@Target(ElementType.METHOD)
//方法@Target(ElementType.FIELD)
//字段、枚举的常量@Target(ElementType.PARAMETER)
//方法参数@Target(ElementType.CONSTRUCTOR)
//构造函数@Target(ElementType.LOCAL_VARIABLE)
//局部变量@Target(ElementType.ANNOTATION_TYPE)
//注解@Target(ElementType.PACKAGE)
//包
2.定义切面,实现自定义日志
@Aspect
@Order(5)
@Component
public class SysOperationLogAspect {
@Autowired
private SysLogService sysLogService;
public static final String ACCOUNT = "匿名用户";
public static final String LOGOUT_URI = "/logout";
public static final String SHORT_PKG = "c.s.s.c";
//保存线程共享变量,开始执行时间
ThreadLocal<Long> startTime = new ThreadLocal<>();
//日志ID
ThreadLocal<String> logId = new ThreadLocal<>();
//定义一个切入点,匹配带有@SysOperationLog注解的方法
@Pointcut("@annotation(com.story.storyvueweb.config.log.SysOperationLog)")
private void storySysLog() {
}
/**
* 环绕通知
* @param joinPoint
*/
@Around("storySysLog()")
public Object advice(ProceedingJoinPoint joinPoint) {
Object o = null;
try {
o = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// 返回请求的结果
return o;
}
/**
* 在请求的方法执行前执行
* @param joinPoint
* @throws Throwable
*/
@Before("storySysLog()&&@annotation(SysOperationLog)")
public void doBefore(JoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记下请求起始时间
startTime.set(System.currentTimeMillis());
//生成logid
logId.set(UUID.randomUUID().toString());
SpiritLoginUser loginUser = LoginUserUtils.loginUser();
final String method = request.getMethod(); //请求方法
final String url = request.getRequestURL().toString();
final String uri = request.getRequestURI();
final String ip = IPUtils.getIpAddr(request);
final String account = loginUser != null ? loginUser.getAccount() : "匿名用户";
final String clazz = joinPoint.getSignature().getDeclaringTypeName()
.replaceAll("com.story.storyvueweb.controller", SHORT_PKG);
final String methodName = joinPoint.getSignature().getName();
final Object[] args = joinPoint.getArgs();
final String params = Arrays.toString(args);
sysLogService.recordLog(new SysLogPO(logId.get(),account, ip, method, url, uri, clazz, methodName,
DateUtils.currentDate(), null, params));
}
/**
* 在方法执行后环绕结束后执行
*/
@After("storySysLog()")
public void doAfter() {
// 计算出本次请求用时,更新到日志中
sysLogService.recordLog(logId.get(), System.currentTimeMillis() - startTime.get());
}
}
@Around
环绕通知中的ProceedingJoinPoint
表示连接点对象,可以通过反射执行目标对象连接点,也就是请求访问的方法。@Before
中的JoinPoint
用以获取连接点运行时的参数,签名对象,上下文等信息。
这里我们已经在切面中将需要的参数放到了一个封装好的对象中,后面主要通过SysLogService来做,它的接口定义很简单:
public interface SysLogService extends StoryAbstractService<SysLogReqDTO, SysLogRespDTO,Long> {
/**
* 记录日志
* @param entity
*/
public void recordLog(SysLogPO entity);
/**
* 更新日志中耗时
* @param id
* @param spendTime 持续时间
*/
public void recordLog(String id, Long spendTime);
}
日志我这里放在了数据库中,当然也可以选择放在文件中,我们只需要修改实现类就可以了。
这里也简单的看下我们的SysLog对象定义:
@Data
@Entity
@Table(name="t_sys_log")
public class SysLogPO {
private String id;
private String account;
private String ip;
private String requestMethod;
private String url;
private String uri;
private String clazz;
private String methodName;
private Date visitTime;
private Long spendTime;
private String params;
public SysLogPO(String id, String account, String ip, String requestMethod, String url, String uri, String clazz,String methodName, Date visitTime, Long spendTime, String params) {
super();
this.id = id;
this.account = account;
this.ip = ip;
this.requestMethod = requestMethod;
this.url = url;
this.uri = uri;
this.clazz = clazz;
this.methodName = methodName;
this.visitTime = visitTime;
this.spendTime = spendTime;
this.params = params;
}
public SysLogPO() {
super();
}
}
3.在方法中使用注解
@RestController
@RequestMapping("/sysmgr/user")
public class UserController {
@SysOperationLog
@PostMapping(value = "/update_user")
public BaseResp<UserRespDTO> updateUser(@RequestBody UserReqDTO userVO) {
BaseResp<UserRespDTO> result = new BaseResp<>();
return result;
}
}
我们只需要在方法声明上增加一个注解@SysOperationLog
,就可以在被请求时记录下请求的参数以及耗时。
最后我们可以通过查询,呈现在页面上的效果就是这样: