通过aop切面获取接口日志

前言:我们项目中可能有这种需求,每个人请求了哪些接口?做了什么事情?参数是什么?重要的接口我们需要记录操作日志以便查找。操作日志和系统日志不一样,操作日志必须要做到简单易懂。所以如何让操作日志不跟业务逻辑耦合,如何让操作日志的内容易于理解,如何让操作日志的接入更加简单?我们不可能在每个接口中去一一处理,可以借助Spring提供的AOP能力+自定义注解轻松应对。

一:AOP相关术语

通知(Advice)

通知描述了切面要完成的工作以及何时执行。比如我们的日志切面需要记录每个接口调用时长,就需要在接口调用前后分别记录当前时间,再取差值。

前置通知(Before):在目标方法调用前调用通知功能;

后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;

返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;

异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;

环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。

切点(Pointcut)

切点定义了通知功能被应用的范围。比如日志切面的应用范围就是所有接口,即所有controller层的接口方法。

切面(Aspect)

切面是通知和切点的结合,定义了何时、何地应用通知功能。

引入(Introduction)

在无需修改现有类的情况下,向现有的类添加新方法或属性。

织入(Weaving)

把切面应用到目标对象并创建新的代理对象的过程。

连接点(JoinPoint)

通知功能被应用的时机。比如接口方法被调用的时候就是日志切面的连接点。

二:AOP相关注解

Spring中使用注解创建切面

@Aspect:用于定义切面

@Before:通知方法会在目标方法调用之前执行

@After:通知方法会在目标方法返回或抛出异常后执行

@AfterReturning:通知方法会在目标方法返回后执行

@AfterThrowing:通知方法会在目标方法抛出异常后执行

@Around:通知方法会将目标方法封装起来

@Pointcut:定义切点表达式

切点表达式:指定了通知被应用的范围,表达式格式:

execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数)

//com.hs.demo.controller包中所有类的public方法都应用切面里的通知

execution(public* com.hs.demo.controller.*.*(..))

//com.hs.demo.service包及其子包下所有类中的所有方法都应用切面里的通知

execution(* com.hs.demo.service..*.*(..))

//com.hs.demo.service.EmployeeService类中的所有方法都应用切面里的通知

execution(* com.hs.demo.service.EmployeeService.*(..))

(1)@POINTCUT定义切入点,有以下2种方式:

方式一:设置为注解@LogFilter标记的方法,有标记注解的方法触发该AOP,没有标记就没有。

@Aspect

@Component

publicclassLogFilter1Aspect{

@Pointcut(value = "@annotation(com.hs.aop.annotation.LogFilter)")

publicvoidpointCut(){

}

}

自定义注解LogFilter:

@Target(ElementType.METHOD)

@Retention(value = RetentionPolicy.RUNTIME)

public@interfaceLogFilter1 {

}

对应的Controller方法如下,手动添加@LogFilter注解:

@RestController

publicclassAopController{

@RequestMapping("/aop")

@LogFilter

publicStringaop(){

System.out.println("这是执行方法");

return"success";

}

}

方式二:采用表达式批量添加切入点,如下方法,表示AopController下的所有public方法都添加LogFilter1切面。

@Pointcut(value = "execution(public * com.train.aop.controller.AopController.*(..))")

publicvoidpointCut(){

}

(2)@Around环绕通知

@Around集成了@Before、@AfterReturing、@AfterThrowing、@After四大通知。需要注意的是,他和其他四大通知注解最大的不同是需要手动进行接口内方法的反射后才能执行接口中的方法,换言之,@Around其实就是一个动态代理。

/**

* 环绕通知是spring框架为我们提供的一种可以在代码中手动控制增强部分什么时候执行的方式。

*

*/

publicvoidaroundPringLog(ProceedingJoinPoint pjp)

  {

//拿到目标方法的方法签名

Signaturesignature=pjp.getSignature();

//获取方法名

Stringname=signature.getName();

try{

//@Before

System.out.println("【环绕前置通知】【"+name+"方法开始】");

//这句相当于method.invoke(obj,args),通过反射来执行接口中的方法

proceed = pjp.proceed();

//@AfterReturning

System.out.println("【环绕返回通知】【"+name+"方法返回,返回值:"+proceed+"】");

}catch(Exception e) {

//@AfterThrowing

System.out.println("【环绕异常通知】【"+name+"方法异常,异常信息:"+e+"】");

}

finally{

//@After

System.out.println("【环绕后置通知】【"+name+"方法结束】");

}

}

proceed = pjp.proceed(args)这条语句其实就是method.invoke,以前手写版的动态代理,也是method.invoke执行了,jdk才会利用反射进行动态代理的操作,在Spring的环绕通知里面,只有这条语句执行了,spring才会去切入到目标方法中。

为什么说环绕通知就是一个动态代理呢?

proceed = pjp.proceed(args)这条语句就是动态代理的开始,当我们把这条语句用try-catch包围起来的时候,在这条语句前面写的信息,就相当于前置通知,在它后面写的就相当于返回通知,在catch里面写的就相当于异常通知,在finally里写的就相当于后置通知。


三:实现

(注意接口地址改变RequestFilter 中urlPatterns 也要改变 ,否则无法获取前台的request)

package com.unisound.iot.smart.operlog;

import javax.servlet.*;

import javax.servlet.annotation.WebFilter;

import javax.servlet.http.HttpServletRequest;

import java.io.IOException;

import java.util.Objects;

/**

* request body 作为流式只能被读取一次,如果行多次阅读,必须将其保存下来

*

* 目前暂时不做body的读取,所以暂时去掉次过滤器

*/

//@WebFilter(filterName = "requestFilter", urlPatterns = "/*")

//这个是控制接口的,如果接口有改变urlPatterns 也要改变

@WebFilter(filterName ="requestFilter", urlPatterns ="/saas/*")

public class RequestFilter implements Filter {

    @Override

    public void init(FilterConfig filterConfig) throws ServletException {

}

    @Override

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request1 =(HttpServletRequest) request;

        if (Objects.isNull(request1.getHeader("Content-Type")) ||request1.getHeader("Content-Type").contains("multipart/form-data;")) {

            chain.doFilter(request, response);

        } else {

            chain.doFilter(new InputStreamReadRepeatableRequestWrapper(request1), response);

        }

}

    @Override

    public void destroy() {

}

}


package com.unisound.iot.smart.operlog;

import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.JSONObject;

import com.unisound.iot.smart.dao.model.operlog.OperationLog;

import com.unisound.iot.smart.dao.model.saas.LoginUserVO;

import com.unisound.iot.smart.entity.ym.MemberVo;

import com.unisound.iot.smart.service.operlog.OperationLogService;

import com.unisound.iot.smart.utils.IPUtil;

import com.unisound.iot.smart.utils.UserUtils;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.AfterReturning;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.aspectj.lang.reflect.MethodSignature;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.web.context.request.RequestAttributes;

import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;

import java.lang.reflect.Method;

import java.util.HashMap;

import java.util.Map;

/**

* 切面处理类,操作日志异常日志记录处理

*

* @author wu

* @date 2019/03/21

*/

@Slf4j

@Aspect

@Component

public class OperLogAspect {

    @Autowired

    private OperationLogService operationLogService;

    /**

    * 设置操作日志切入点 记录操作日志 在注解的位置切入代码

    */

    @Pointcut("@annotation(com.unisound.iot.smart.operlog.OperLog)")

    public void operLogPoinCut() {

}

    /**

    * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行

    *

    * @param joinPoint 切入点

    * @param keys      返回结果

    */

    @AfterReturning(value ="operLogPoinCut()", returning ="keys")

    public void saveOperLog(JoinPoint joinPoint, Object keys) {

        // 获取RequestAttributes

        RequestAttributes requestAttributes =RequestContextHolder.getRequestAttributes();

        // 从获取RequestAttributes中获取HttpServletRequest的信息

        HttpServletRequest request =(HttpServletRequest) requestAttributes

                .resolveReference(RequestAttributes.REFERENCE_REQUEST);

        OperationLog operlog =new OperationLog();

        try {

            // 从切面织入点处通过反射机制获取织入点处的方法

            MethodSignature signature =(MethodSignature) joinPoint.getSignature();

            // 获取切入点所在的方法

            Method method =signature.getMethod();

            // 获取操作

            OperLog opLog =method.getAnnotation(OperLog.class);

            if (opLog !=null) {

                String operModul =opLog.operModul();

                String operType =opLog.operType();

                String operDesc =opLog.operDesc();

                operlog.setOperModul(operModul); // 操作模块

                operlog.setOperType(operType); // 操作类型

                operlog.setOperDesc(operDesc); // 操作描述

            }

            LoginUserVO loginUserVO =UserUtils.getCurrentUser(request);

            Long userId =null;

            String userName =null;

            Integer custId=null;

            if (loginUserVO !=null) {

                userId =Long.valueOf(loginUserVO.getLoginUserId());

                userName =loginUserVO.getLoginName();

                //custId = loginUserVO.getCustId();

            }

            // 获取请求的类名

            String className =joinPoint.getTarget().getClass().getName();

            // 获取请求的方法名

            String methodName =method.getName();

            methodName =className +"." +methodName;

            String RequParam =null;

            // 请求的参数

            Map<String, Object> rtnMap = converMap(request.getParameterMap());

            // 将参数所在的数组转换成json

            String params =null;

            JSONObject jsonObject =new JSONObject();

            if (rtnMap !=null &&rtnMap.size() >0) {

                //params = JSONObject.object(rtnMap);

//RequParam += "params=" + params;

                JSONObject jo =new JSONObject(rtnMap);

                jsonObject.put("params", jo);

            }

            //添加body

            /* 为了避免低效【过滤器过滤每个url,保存body,暂时去掉】

              开启方式:1.打开此段注释 2.打开RequestFilter中的@WebFilter 注释即可*/

            try {

                //获取前台传过来的request,如果接口地址有改变 打开RequestFilter中的urlPatterns 也要改变,否则request为空

                Map bodyMap =RequestHelper.getBodyString(request);

                String body =null;

                if (bodyMap !=null) {

                    //body = JSON.toJSONString(bodyMap);

//RequParam += "body=" + body;

                    JSONObject jo =new JSONObject(bodyMap);

                    jsonObject.put("body", jo);

                }

            } catch (Exception e) {

                log.error("读取 request body 出错! 请查看RequestFilter拦截地址是否包含!{}", e, e.getMessage());

            }

            operlog.setRequParam(jsonObject.toJSONString()); // 请求参数

            operlog.setRespParam(JSON.toJSONString(keys)); // 返回结果

            operlog.setUserId(userId); // 请求用户ID

            operlog.setUserName(userName); // 请求用户名称

            operlog.setOperIp(IPUtil.realIP(request)); // 请求IP

            operlog.setOperUri(request.getRequestURI()); // 请求URI

//operlog.setCustId(custId);

            operationLogService.insert(operlog);

        } catch (Exception e) {

            e.printStackTrace();

        }

}

    /**

    * 转换request 请求参数

    *

    * @param paramMap request获取的参数数组

    */

    public Map<String, Object> converMap(Map<String, String[]> paramMap) {

        Map<String, Object> rtnMap =new HashMap<String, Object>();

        for (String key :paramMap.keySet()) {

            rtnMap.put(key, paramMap.get(key)[0]);

        }

        return rtnMap;

    }

}

创作不易,如果这篇文章对你有用,请点个赞谢谢♪(・ω・)ノ!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容