springboot注解式AOP(@Aspect)统一日志管理

简介

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。在日常开发当中经常用来记录日志,方法跟踪、事务,权限等

切面方法说明:

  • @Aspect – 作用是把当前类标识为一个切面供容器读取
  • @Pointcut – (切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
  • @Before – 标识一个前置增强方法,相当于BeforeAdvice的功能
  • @AfterReturning – 后置增强,相当于AfterReturningAdvice,方法退出时执行
  • @AfterThrowing – 异常抛出增强,相当于ThrowsAdvice
  • @After – final增强,不管是抛出异常或者正常退出都会执行
  • @Around – 环绕增强,相当于MethodInterceptor

准备工作

  • 1、我们使用springboot,引入一个aop相关的包
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 2、创建一张日志表,内容如下:
CREATE TABLE `interface_log` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `url` text COMMENT '请求url',
  `request_type` varchar(50) DEFAULT NULL COMMENT '请求类型',
  `request_body` varchar(1000) COMMENT '请求报文',
  `request_date` datetime DEFAULT NULL COMMENT '请求时间',
  `request_method` varchar(50) DEFAULT NULL COMMENT '请求方式',
  `response_body` varchar(1000) COMMENT '响应报文',
  `response_status` varchar(10) DEFAULT NULL COMMENT '响应状态值',
  `response_message` varchar(255) DEFAULT NULL COMMENT '响应消息',
  `response_date` datetime DEFAULT NULL COMMENT '响应时间',
  `remote_addr` varchar(50) DEFAULT NULL COMMENT '操作者IP地址',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `creator_id` bigint(11) DEFAULT NULL COMMENT '创建人id',
  `creator` varchar(64) DEFAULT NULL COMMENT '创建人名称',
  `create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `valid` tinyint(1) DEFAULT '1' COMMENT '是否有效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COMMENT='接口日志';

编写Demo

  • 1、创建一个类 InterfaceLogAspect
package com.honghh.bootfirst.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.honghh.bootfirst.entity.InterfaceLog;
import com.honghh.bootfirst.service.InterfaceLogService;
import com.honghh.bootfirst.utils.IPUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * ClassName: InterfaceLogAspect
 * Description:
 *
 * @author honghh
 * @date 2019/03/11 14:54
 */
@Slf4j
@Aspect
@Component
public class InterfaceLogAspect {
    @Resource
    private InterfaceLogService interfaceLogService;

    private static final String REQUEST_TYPE = "log";

    private static final String SUCCESS = "200";

    /**
     * 建立日志对象线程变量
     **/
    private ThreadLocal<InterfaceLog> threadLocal = new ThreadLocal<>();


    @Pointcut("execution(public * com.honghh.bootfirst.controller.*.*(..))")
    public void interfaceLog() {
    }

    @Before(value = "interfaceLog()")
    public void before(JoinPoint point) {
        InterfaceLog interfaceLogDO = new InterfaceLog();
        try {
            //  获取请求对象
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();

            interfaceLogDO.setUrl(request.getRequestURI());
            interfaceLogDO.setRequestType(REQUEST_TYPE);
            // 请求报文
            if (point.getArgs() != null && point.getArgs().length > 0) {
                Object parameterObject = point.getArgs()[0];
                if (parameterObject instanceof String) {
                    interfaceLogDO.setRequestBody((String) parameterObject);
                } else {
                    interfaceLogDO.setRequestBody(JSON.toJSONString(parameterObject));
                }
            }
            interfaceLogDO.setRequestMethod(request.getMethod());
            interfaceLogDO.setRequestDate(new Date());
            interfaceLogDO.setRemoteAddr(IPUtils.getIpAddr(request));
        } catch (Exception e) {
            log.error("Before 日志记录报错!message:{}", e.getMessage());
            e.printStackTrace();
        }
        threadLocal.set(interfaceLogDO);
    }

    @AfterReturning(value = "interfaceLog()", returning = "ret")
    public void after(Object ret) {
        InterfaceLog interfaceLogDO = threadLocal.get();
        try {
            interfaceLogDO.setResponseDate(new Date());
            interfaceLogDO.setResponseBody(JSONObject.toJSONString(ret));
            interfaceLogDO.setResponseStatus(SUCCESS);
            interfaceLogDO.setResponseMessage("成功");
        } catch (Exception e) {
            log.error("AfterReturning 日志记录报错!message:{}", e.getMessage());
            e.printStackTrace();
        }
        interfaceLogService.saveLog(interfaceLogDO);
        threadLocal.remove();
    }

    @AfterThrowing(value = "interfaceLog()", throwing = "throwing")
    public void error(Throwable throwing) {
        InterfaceLog interfaceLogDO = threadLocal.get();
        try {
            //将报错信息写入error字段
            interfaceLogDO.setResponseDate(new Date());
            interfaceLogDO.setResponseMessage(throwing.getMessage());
            interfaceLogDO.setRemark(throwing.getStackTrace().length > 0 ? throwing.getStackTrace()[0].toString() : null);
//            记录自定义的错误状态码
//            if (throwing instanceof ApiException) {
//                interfaceLogDO.setResponseStatus(String.valueOf(((ApiException) throwing).getErrorCode()));
//            }
        } catch (Exception e) {
            log.error("AfterThrowing 日志记录报错!message:{}", e.getMessage());
            e.printStackTrace();
        }
        interfaceLogService.saveLog(interfaceLogDO);
        threadLocal.remove();
    }
}

我们介绍一下 @Pointcut(“execution(public * com.honghh.bootfirst.controller..(…))”) 的使用:

1. 任意公共方法的执行:
execution(public * *(..))
##public可以省略, 第一个* 代表方法的任意返回值 第二个参数代表任意包+类+方法 (..)任意参数

2.. 任何一个以“set”开始的方法的执行:
execution(* set*(..))

3.定义在com.honghh.bootfirst.controller包里的任意方法的执行:
execution(* com.honghh.bootfirst.controller.*.*(..))
# 第一个 .* 代表任意类, 第二个 .* 代表任意方法

4.定义在controller包和所有子包里的任意类的任意方法的执行:
execution(* com.honghh.bootfirst.controller..*.*(..))
#  ..* 代表任意包或者子包
  • 2、上面使用了一个IP获取的工具类 IPUtils,代码如下:
package com.honghh.bootfirst.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * ClassName: IPUtils
 * Description:
 *
 * @author honghh
 * @date 2019/03/11 14:38
 */
public class IPUtils {
    private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
    /**
     * Description : 获取本地IP地址
     * Group :
     *
     * @author honghh
     * @date  2019/3/11 0011 14:38
     * @return
     */
    public static String getLocalHost() {
        InetAddress addr = null;
        try {
            addr = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return addr.getHostAddress();
    }

    /**
     * Description : 获取IP地址
     * Group :
     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
     *
     * @param request
     * @return java.lang.String
     * @author honghh
     * @date 2019/3/11 0011 14:38
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = null;
        try {
            ip = request.getHeader("x-forwarded-for");
            if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } catch (Exception e) {
            logger.error("IPUtils ERROR ", e);
        }

//        //使用代理,则获取第一个IP地址
//        if(StringUtils.isEmpty(ip) && ip.length() > 15) {
//			if(ip.indexOf(",") > 0) {
//				ip = ip.substring(0, ip.indexOf(","));
//			}
//		}

        return ip;
    }

    public static boolean isEmpty(CharSequence value) {
        return value == null || value.length() == 0;
    }
}

  • 3、我们使用postman请求看一下结果(这里定义的myinfo接口是使用之前的项目)springboot整合swagger里面的接口,这里就不细说,有兴趣可以看一下之前的文章。
    image
    查看数据库:
    image
    从查询结果中可以看到,前两次请求Ip地址有点问题,这个是因为我用localhost:8080请求导致。如果是本地调用的话,建议还是使用127.0.0.1
  • 4、有关interfaceLog 相关的代码我这就不在一一罗列了,基本的增删改查相信大家都会。如果不知道的话,可以去获取源码。

代码获取

https://gitee.com/honghh/boot-demo.git [boot-aspect]

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐