一、背景简介
- 系统上线后业务人员反馈不错,但是领导想要看看某些接口的调用日志信息,这样一来就尴尬了,系统根本没做日志的采集功能;这可把我愁坏了,如果直接在原有的业务代码逻辑中去显示的编写针对于该接口的日志保存逻辑,一个还好,后期有两个、三个…N个呢?这谁顶得住啊…0.0,而且这样也会产生一些代码的冗余以及增加系统的耦合度,对于追求完美且“懒惰”的我来说,这是万万不能接受的。所以,我就在想有没有一种方法可以不在具体的业务中编写任何的代码,系统就可以帮我们采集我们需要的日志呢?答案当然是肯定的,欲知详情,且听老衲慢慢道来。
- 经过我对Spring的研究和半天的辛苦编码,我终于完美的解决了系统日志的记录,具体方案为:自定义注解+AOP切面编程。既然是AOP,那就一定要先介绍一下AOP相关的知识了。
二、SpringAop使用详解
1.我整理了一张图(借鉴),让大家能更好的梳理SpringAOP的使用。
- @Aspect注解:声明这是一个切面类,作用在类上。
- @Pointcut注解:声明切点/切面,拦截每一个该切点/切面匹配到的方法,根据我们具体写的通知方法,在方法的执行之前和执行之后处理自定义的业务逻辑。
- @Before(前置通知):在目标方法被调用之前调用通知功能,通知方法会在目标方法调用之前执行。
- @After(后置通知):在目标方法执行完成之后调用通知,此时不会关心方法的输出是什么,通知方法会在目标方法返回或抛出异常后调用。
- @AfterReturning(返回通知):在目标方法成功执行之后调用通知,通知方法会在目标方法返回后调用。
- @AfterThrowing(异常通知):在目标方法抛出异常后或前置通知抛出异常后调用,通知方法会在目标表方法抛出异常后执行。
- @Around(环绕通知):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为,通知方法会将目标方法包裹起来。
- 是不是觉得通知方法执行顺序有点抽象(我也觉得0.0),那我用我们最擅长的方式来帮助大家理解一下☺。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;
try {
//@Before执行
result = method.invoke(target, args);
//@After执行
return result;
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
//@AfterThrowing执行
throw targetException;
} finally {
//@AfterReturning执行
}
}
三、AOP应用示例
- 自定义注解
/**
* 主要标注日志的具体用处也就是具体操作
* @author Jack.Cheng
* @date 2020/1/7 10:05
**/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface SysOperaLog {
//描述,默认为空
String desc() default "" ;
//日志级别,默认为一般 级别:5-严重、4-警告、3-敏感、2-重要、1-一般
int level() default 1;
}
- AOP切面定义
/**
* 系统日志切面
* @author Jack.Cheng
* @date 2020/1/7 10:22
**/
@Slf4j
@Aspect
@Component
public class SysLogAspect {
/**
* 用于保存当前线程调用接口的日志信息,贯穿AOP各个通知方法的生命周期
*/
private static final ThreadLocal<SysLog> LOCAL_SYS_LOG = new ThreadLocal<>();
/**
* SpringIoc容器,使用Spring的事件监听机制,发布日志消息事件,异步处理日志信息
* 家庭条件好的,这里可以用MQ队列来实现日志的异步消费 0.0
*/
@Autowired
private ApplicationContext applicationContext;
/**
* 定义AOP切点信息,拦截所有带有@SysOperaLog注解的方法
*/
@Pointcut("@annotation(com.asiainfo.gridtask.aspect.annotation.SysOperaLog)")
public void sysLogAspect() { }
/**
* 前置通知,在目标方法被调用之前调用通知功能,通知方法会在目标方法调用之前执行。
* @param joinPoint 切点信息
*/
@Before(value = "sysLogAspect()")
public void recordLog(JoinPoint joinPoint){
SysLog sysLog = new SysLog();
try {
// 记录日志处理
long beginTime = Instant.now().toEpochMilli();
HttpServletRequest request = getRequest();
sysLog.setLogId(LogUtil.randomLogId())
.setCreateTime(TimeUtils.getCurrentDateString(TimeUtils.DATE_TIME_FORMAT))
.setIp(CommonUtil.getIpAddr(request))
.setLogDate(TimeUtils.getCurrentDateString(TimeUtils.DATE_DAY_FORMAT));
// 获取方法的@SysOperaLog注解信息,初始化封装日志信息
SysOperaLogInfo sysOperateLogInfo = LogUtil.getSysOperateLogInfo(joinPoint);
if(null != sysOperateLogInfo){
sysLog.setDescription(sysOperateLogInfo.getDesc())
.setOperateLevel(sysOperateLogInfo.getLevel())
.setParams(sysOperateLogInfo.getParams())
.setRequestMethod(sysOperateLogInfo.getRequestMethod());
}
long endTime = Instant.now().toEpochMilli();
sysLog.setConsumingTime(endTime - beginTime);
log.info("接口开始调用执行,当前logInfo:{}",sysLog.toString());
}catch (Exception e){
log.error("AOP日志处理失败,ERROR:",e);
}finally {
// 使用ThreadLocal保存日志初始化参数,方法执行完或抛出异常后继续封装日志信息
LOCAL_SYS_LOG.set(sysLog);
}
}
/**
* 返回通知,在目标方法成功执行之后调用通知,通知方法会在目标方法返回后调用。
* @param result 接口的执行结果,与注解的result对应
*/
@AfterReturning(returning = "result", pointcut = "sysLogAspect()")
public void doAfterReturning(Object result) {
try {
//得到当前线程的log对象
SysLog sysLog = LOCAL_SYS_LOG.get();
if(null != sysLog){
ResultObject resultInfo = Convert.convert(ResultObject.class, result);
if (resultInfo.isResultStatus()) {
sysLog.setStatus(LogConstant.SUCCESS);
} else {
sysLog.setStatus(LogConstant.FAILED);
String exDesc = resultInfo.getResult() == null ? "" : resultInfo.getResult().toString();
sysLog.setExDesc(exDesc);
}
setGridUserInfo(sysLog);
applicationContext.publishEvent(new SysLogEvent(sysLog));
log.info("接口执行完成,当前logInfo:{}",sysLog.toString());
}
}finally {
LOCAL_SYS_LOG.remove();
}
}
/**
* 异常通知,在目标方法抛出异常后或前置通知抛出异常后调用,通知方法会在目标表方法抛出异常后执行。
* @param e 接口抛出的异常信息
*/
@AfterThrowing(pointcut = "sysLogAspect()", throwing = "e")
public void doAfterThrowable(Throwable e) {
try {
SysLog sysLog = LOCAL_SYS_LOG.get();
if(null != sysLog){
sysLog.setExDesc("系统内部异常");
setGridUserInfo(sysLog);
applicationContext.publishEvent(new SysLogEvent(sysLog));
log.info("接口执行异常,当前logInfo:{}",sysLog.toString());
}
}finally {
LOCAL_SYS_LOG.remove();
}
}
private HttpServletRequest getRequest(){
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
private void setGridUserInfo(SysLog sysLog){
HttpServletRequest request = getRequest();
HttpSession session = request.getSession();
String staffCode = Objects.nonNull(session.getAttribute(LoginConstant.LOGIN_PARAM_STAFF_CODE)) ?
(String) session.getAttribute(LoginConstant.LOGIN_PARAM_STAFF_CODE) : null;
String gridId = Objects.nonNull(session.getAttribute(LoginConstant.LOGIN_PARAM_ORG_ID)) ?
(String) session.getAttribute(LoginConstant.LOGIN_PARAM_ORG_ID) : null;
int gridLevel = Objects.nonNull(session.getAttribute(LoginConstant.LOGIN_PARAM_ORG_LEVEL)) ?
(Integer) session.getAttribute(LoginConstant.LOGIN_PARAM_ORG_LEVEL) : 0;
String phoneNo = Objects.nonNull(session.getAttribute(LoginConstant.LOGIN_PARAM_TOKEN)) ?
(String) session.getAttribute(LoginConstant.LOGIN_PARAM_TOKEN) : null;
sysLog.setStaffCode(staffCode)
.setGridId(gridId)
.setGridLevel(gridLevel)
.setPhoneNo(phoneNo);
}
}
- 测试Restful接口
@GetMapping("testAop")
@SysOperaLog(desc = "【AOP注解测试接口】",level = 2)
public ResultObject testMyAop(String msg){
log.info("【AOP注解测试接口】,msg:{}",msg);
return ResultObject.ok(msg);
}
- 测试结果
- 在此系统的整个AOP环境已经搭建完成了,我们也知道了SpringAOP的使用方法。只需要一个注解就搞定了日志的记录,是不是很舒服,很方便,你是不是也想尝试一下呢?
- 会用了,这就满足了吗?当然不是,对于一个优秀的程序员来说,知道了工具的使用方法,那一定更想知道其使用原理,那就敬请期待Spring AOP源码分析(二)【组件注册】。
- 源码地址:https://github.com/juncehng/aop
- 限于博主水平有限,如有错误,还请大家批评指正☺