采用spring AOP记录日志

采用spring AOP记录日志

前言

项目开发过程中,查看日志有利于更好的排查问题和收集访问数据,对产品有更大的帮助。

单回顾一下aop:

AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。

一 、AOP的基本概念

  • Aspect(切面):通常是一个类,里面可以定义切入点和通知

  • JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用

  • Advice(通知):AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around

  • Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式

  • AOP代理:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类

下面我们简单使用下

二、增加pom jar包

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
     </dependency>
<! --  获取浏览器的基本信息  -->
 <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.20</version>
 </dependency>

三、编写LogAspect类

@Aspect
@Component
@Slf4j
public class LogAspect {
	//mq记录日志,非必要
    @Autowired
    private AmqpTemplate rabbitTemplate;

    /**
     * 进入方法时间戳
     */
    private Long startTime;
    /**
     * 方法结束时间戳(计时)
     */
    private Long endTime;

    public LogAspect() {
    }


    /**
     * 定义请求日志切入点,其切入点表达式有多种匹配方式,这里是指定路径
     */
    @Pointcut("execution(public * cn.fengpt..*.web.*.*(..))")
    public void webLogPointcut() {
    }



    /**
     * 在执行方法前后调用Advice,这是最常用的方法,相当于@Before和@AfterReturning全部做的事儿
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("webLogPointcut()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if(attributes != null ){
            HttpServletRequest request = attributes.getRequest();
            if(request != null ){
                //获取请求头中的User-Agent
                UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
                //打印请求的内容
                String ip = getRemoteIpByServletRequest(request, false);
                String url=request.getRequestURL().toString();
                startTime = System.currentTimeMillis();
                log.info("请求Url : {}" , url);
                log.info("请求方式 : {}" , request.getMethod());
                log.info("请求ip : {}" ,ip);
                log.info("请求方法 : {}" , pjp.getSignature().getDeclaringTypeName() , "." , pjp.getSignature().getName());
                log.info("请求参数 : {}" , Arrays.toString(pjp.getArgs()));
                // 系统信息
                log.info("浏览器:{}", userAgent.getBrowser().toString());
                log.info("浏览器版本:{}",userAgent.getBrowserVersion());
                log.info("操作系统: {}", userAgent.getOperatingSystem().toString());
                // pjp.proceed():当我们执行完切面代码之后,还有继续处理业务相关的代码。proceed()方法会继续执行业务代码,并且其返回值,就是业务处理完成之后的返回值。
                Object ret = pjp.proceed();
                log.info("请求结束时间:"+ LocalDateTime.now());
                log.info("请求耗时:{}" , (System.currentTimeMillis() - startTime));
                // 处理完请求,返回内容
                log.info("请求返回 : {}" , ret);
                LogDTO logDTO=new LogDTO();
                logDTO.setIp(ip);
                logDTO.setBrowerVersion(userAgent.getBrowserVersion().getVersion());
                logDTO.setOperatingSystem(userAgent.getOperatingSystem().toString());
                logDTO.setBrowser(userAgent.getBrowser().toString());
                logDTO.setUrl(url);
                logDTO.setMethod(request.getMethod());
                //JSON.toJSONString(logDTO)
			//这里可以采用mq记录相关日志
                rabbitTemplate.convertAndSend("log-message", JSON.toJSONString(logDTO));
                return ret;
            }
        }
        Object ret = pjp.proceed();
        return ret;
    }

    /**
     * 获取真实ip
     *
     * @param request       HttpServletRequest
     * @param acceptInnerIp 是否可以返回内网ip
     * @return 真实ip
     */
    public static String getRemoteIpByServletRequest(HttpServletRequest request, boolean acceptInnerIp) {
        String ip = request.getHeader("x-forwarded-for");
        log.info("x-forwarded-for:{}" , ip);
        log.info("Proxy-Client-IP:{}" , request.getHeader("Proxy-Client-IP"));
        log.info("WL-Proxy-Client-IP:{}" , request.getHeader("WL-Proxy-Client-IP"));
        log.info("HTTP_CLIENT_IP:{}" , request.getHeader("HTTP_CLIENT_IP"));
        log.info("HTTP_X_FORWARDED_FOR:{}" , request.getHeader("HTTP_X_FORWARDED_FOR"));
        log.info("X-Real-IP:{}" , request.getHeader("X-Real-IP"));

        if (StringUtils.isNotBlank(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (isIpValid(ip)) {
            return ip;
        }
        ip = request.getHeader("Proxy-Client-IP");
        if (isIpValid(ip)) {
            return ip;
        }
        ip = request.getHeader("WL-Proxy-Client-IP");
        if (isIpValid(ip)) {
            return ip;
        }
        ip = request.getHeader("HTTP_CLIENT_IP");
        if (isIpValid(ip)) {
            return ip;
        }
        ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        if (isIpValid(ip)) {
            return ip;
        }
        ip = request.getHeader("X-Real-IP");
        if (isIpValid(ip)) {
            return ip;
        }
        ip = request.getRemoteAddr();
        return ip;
    }

    /**
     * 判断是否有效
     * @param ip ip
     * @param acceptInnerIp 是否接受内网ip
     * @return
     */
    private static boolean isIpValid(String ip, boolean acceptInnerIp) {
        return acceptInnerIp ? isIpValid(ip) : isIpValidAndNotPrivate(ip);
    }

    /**
     * 仅仅判断ip是否有效
     * @param ip
     * @return
     */
    private static boolean isIpValid(String ip) {
        if (StringUtils.isBlank(ip)) {
            return false;
        }
        String[] split = ip.split("\\.");
        if (split.length != 4) {
            return false;
        }
        try {
            long first = Long.valueOf(split[0]);
            long second = Long.valueOf(split[1]);
            long third = Long.valueOf(split[2]);
            long fourth = Long.valueOf(split[3]);
            return first < 256 && first > 0
                    && second < 256 && second >= 0
                    && third < 256 && third >= 0
                    && fourth < 256 && fourth >= 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * 判断ip是否有效,并且不是内网ip
     * @param ip
     * @return
     */
    private static boolean isIpValidAndNotPrivate(String ip) {
        if (StringUtils.isBlank(ip)) {
            return false;
        }
        String[] split = ip.split("\\.");
        try {
            long first = Long.valueOf(split[0]);
            long second = Long.valueOf(split[1]);
            long third = Long.valueOf(split[2]);
            long fourth = Long.valueOf(split[3]);
            if (first < 256 && first > 0
                    && second < 256 && second >= 0
                    && third < 256 && third >= 0
                    && fourth < 256 && fourth >= 0) {
                if (first == 10) {
                    return false;
                }
                if (first == 172 && (second >= 16 && second <= 31)) {
                    return false;
                }
                if (first == 192 && second == 168) {
                    return false;
                }
                return true;
            }
            return false;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /*
    *//**
     * 前置通知:
     * 1. 在执行目标方法之前执行,比如请求接口之前的登录验证;
     * 2. 在前置通知中设置请求日志信息,如开始时间,请求参数,注解内容等
     *
     * @param joinPoint
     * @throws Throwable
     *//*
    @Before("webLogPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //获取请求头中的User-Agent
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        //打印请求的内容
        startTime = System.currentTimeMillis();
        log.info("请求开始时间:{}" + LocalDateTime.now());
        log.info("请求Url : {}" + request.getRequestURL().toString());
        log.info("请求方式 : {}" + request.getMethod());
        log.info("请求ip : {}" + request.getRemoteAddr());
        log.info("请求方法 : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("请求参数 : {}" + Arrays.toString(joinPoint.getArgs()));
        // 系统信息
        log.info("浏览器:{}", userAgent.getBrowser().toString());
        log.info("浏览器版本:{}", userAgent.getBrowserVersion());
        log.info("操作系统: {}", userAgent.getOperatingSystem());
    }

    *//**
     * 返回通知:
     * 1. 在目标方法正常结束之后执行
     * 1. 在返回通知中补充请求日志信息,如返回时间,方法耗时,返回值,并且保存日志信息
     *
     * @param ret
     * @throws Throwable
     *//*
    @AfterReturning(returning = "ret", pointcut = "webLogPointcut()")
    public void doAfterReturning(Object ret) throws Throwable {
        endTime = System.currentTimeMillis();
        log.info("请求结束时间:{}" + LocalDateTime.now());
        log.info("请求耗时:{}" + (endTime - startTime));
        // 处理完请求,返回内容
        log.info("请求返回 : {}" + ret);
    }
*/
    /**
     * 异常通知:
     * 1. 在目标方法非正常结束,发生异常或者抛出异常时执行
     * 1. 在异常通知中设置异常信息,并将其保存
     *
     * @param throwable
     */
    @AfterThrowing(value = "webLogPointcut()", throwing = "throwable")
    public void doAfterThrowing(Throwable throwable) {
        // 保存异常日志记录
        log.error("发生异常时间:{}" + LocalDateTime.now());
        log.error("抛出异常:{}" + throwable.getMessage());
    }

}

四、我们编写接口测试

image.png

当然 ,我们也可以把日志记录到数据库,可以进一步分析。