每一年都奔走在自己热爱里

没有人是一座孤岛,总有谁爱着你

springBoot整合spring-aop拦截日志

1 创建springboot项目

(ps:本文不做详细介绍,可以阅读另一篇博客:https://www.cnblogs.com/liyhbk/p/13572989.html

1.1 添加pom依赖

<dependencies>
        <!--Spring Boot Web 基础环境-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Spring Boot 测试环境-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.xnx3.util/xnx3-util -->
        <dependency>
            <groupId>com.xnx3.util</groupId>
            <artifactId>xnx3-util</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <!--aop-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.1.7.RELEASE</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.5</version>
        </dependency>
        <!--接口文档-->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.21</version>
        </dependency>
    </dependencies>

1.2 配置application.yml文件

# 配置端口
server:
  port: 8084

spring:
  # 配置数据源
  datasource:
    url: jdbc:mysql://localhost:3306/db1?useSSL=false&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  # 配置mapper.xml 文件所在的路径
  mapper-locations: classpath:mapper/*.xml
  # 配置映射类所在的路径
  type-aliases-package: com.liyh.entity
  # 开启驼峰映射
  configuration:
    map-underscore-to-camel-case: true
#打印sql
logging:
  level:
    com.liyh.mapper: debug

1.3 创建logback.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">

    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <!-- Console 输出设置 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

2 配置AOP拦截器

2.1 spring-aop注解拦截顺序

2.1.1 正常运行:

 

2.1.2 程序报错:

2.2 创建控制器切面类 LogAspectj 

package com.liyh.log;

import com.liyh.entity.ExceptionLog;
import com.liyh.entity.SqlLog;
import com.liyh.entity.UserLog;
import com.liyh.service.LogService;
import com.liyh.utils.SqlUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * 控制器切面
 * @Author: liyh
 * @Date: 2020/9/17 14:08
 */

@Aspect
@Component
public class LogAspectj {
    @Autowired
    private LogService logService;

    @Autowired
    SqlSessionFactory sqlSessionFactory;

    private static Logger logger = LoggerFactory.getLogger(LogAspectj.class);

    /**
     * @Pointcut : 创建一个切点,方便同一方法的复用。
     * value属性就是AspectJ表达式,
     */
    @Pointcut("execution(* com.liyh.controller.*.*(..))")
    //@Pointcut("@annotation(com.liyh.log.LogAnno)")
    public void userLog() {
    }

    @Pointcut("execution(* com.liyh.mapper.*.*(..))")
    public void sqlLog() {
    }

    @Pointcut("execution(* com.liyh.controller.*.*(..))")
    public void exceptionLog() {
    }

    //前置通知
    //指定该方法是前置通知,并指定切入点
    @Before("userLog()")
    public void userLog(JoinPoint pj) {
        try {
            UserLog userLog = new UserLog();
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String method = request.getMethod();
            Signature signature = pj.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method targetMethod = methodSignature.getMethod();
            if ("POST".equals(method) || "GET".equals(method)) {
                String ipAddress = getIpAddress(request);
                String requestId = (String) request.getAttribute("requestId");
                // 根据请求参数或请求头判断是否有“requestId”,有则使用,无则创建
                if (StringUtils.isEmpty(requestId)) {
                    requestId = "req_" +  System.currentTimeMillis();
                    request.setAttribute("requestId", requestId);
                }
                userLog.setRequestId(requestId);    //请求id
                userLog.setMethodName(targetMethod.getName());        //方法名
                userLog.setMethodClass(signature.getDeclaringTypeName()); //方法所在的类名
                userLog.setRequestUrl(request.getRequestURL().toString());//请求URI
                userLog.setRemoteIp(ipAddress); //操作IP地址
                System.out.println("userLog = " + userLog);
//                logService.saveUserLog(userLog);
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    //环绕通知
    @Around("sqlLog()")
    public Object sqlLog(ProceedingJoinPoint pj) throws Throwable {
        // 发送异步日志事件
        long start = System.currentTimeMillis();
        SqlLog sqlLog = new SqlLog();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Signature signature = pj.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        String ipAddress = getIpAddress(request);
        String requestId = (String) request.getAttribute("requestId");
        // 根据请求参数或请求头判断是否有“requestId”,有则使用,无则创建
        if (StringUtils.isEmpty(requestId)) {
            requestId = "req_" + System.currentTimeMillis();
            request.setAttribute("requestId", requestId);
        }
        //执行方法
        Object object = pj.proceed();
        //获取sql
        String sql = SqlUtils.getMybatisSql(pj, sqlSessionFactory);
        //执行时长(毫秒)
        long loadTime = System.currentTimeMillis() - start;
        sqlLog.setRequestId(requestId);    //请求id
        sqlLog.setMethodName(targetMethod.getName());        //方法名
        sqlLog.setMethodClass(signature.getDeclaringTypeName()); //方法所在的类名
        sqlLog.setRequestUrl(request.getRequestURL().toString());//请求URI
        sqlLog.setRemoteIp(ipAddress); //操作IP地址
        sqlLog.setSql(sql);//sql
        sqlLog.setLoadTime(loadTime);//执行时间
        System.out.println("sqlLog = " + sqlLog);
//        logService.saveSqlLog(sqlLog);
        return object;
    }

    //异常通知 用于拦截异常日志
    @AfterThrowing(pointcut = "exceptionLog()", throwing = "e")
    public void exceptionLog(JoinPoint pj, Throwable e) {
        try {
            ExceptionLog exceptionLog = new ExceptionLog();
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            Signature signature = pj.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method targetMethod = methodSignature.getMethod();
            String ipAddress = getIpAddress(request);
            String requestId = (String) request.getAttribute("requestId");
            // 根据请求参数或请求头判断是否有“requestId”,有则使用,无则创建
            if (StringUtils.isEmpty(requestId)) {
                requestId ="req_" + System.currentTimeMillis();
                request.setAttribute("requestId", requestId);
            }
            exceptionLog.setRequestId(requestId);    //请求id
            exceptionLog.setMethodName(targetMethod.getName());        //方法名
            exceptionLog.setMethodClass(signature.getDeclaringTypeName()); //方法所在的类名
            exceptionLog.setRequestUrl(request.getRequestURL().toString());//请求URI
            exceptionLog.setMessage(e.getMessage()); //异常信息
            exceptionLog.setRemoteIp(ipAddress); //操作IP地址
            System.out.println("exceptionLog = " + exceptionLog);
//            logService.saveExceptionLog(exceptionLog);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }


    /**
     * 获取IP地址的方法
     * @param request 传一个request对象下来
     * @return
     */
    public String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

2.3 定义注解类 LogAnno 

package com.liyh.log;

import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: liyh
 * @Date: 2020/9/17 17:12
 */
@Target({ElementType.METHOD,ElementType.TYPE})  //作用于方法 使用在类,接口
@Retention(RetentionPolicy.RUNTIME)     //运行时有效
public @interface LogAnno {
    @AliasFor("value")
    String[] operating() default {};
    @AliasFor("operating")
    String[] value() default {};
}

2.4 添加json工具类和获取sql工具类

package com.liyh.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import java.io.IOException;

/**
 * jsonUtil工具类
 * @Author: liyh
 * @Date: 2020/9/17 17:12
 */
public class JsonUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);

    private static ObjectMapper mapper = new ObjectMapper();


    /**
     * 对象转Json格式字符串
     * @param obj 对象
     * @return Json格式字符串
     */
    public static <T> String obj2String(T obj) {
        if (obj == null) {
            return null;
        }
        try {
            return obj instanceof String ? (String) obj : mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            LOGGER.warn("Parse Object to String error : {}", e.getMessage());
            return null;
        }
    }

    /**
     * 对象转Json格式字符串(格式化的Json字符串)
     * @param obj 对象
     * @return 美化的Json格式字符串
     */
    public static <T> String obj2StringPretty(T obj) {
        if (obj == null) {
            return null;
        }
        try {
            return obj instanceof String ? (String) obj : mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            LOGGER.warn("Parse Object to String error : {}", e.getMessage());
            return null;
        }
    }

    /**
     * 字符串转换为自定义对象
     * @param str 要转换的字符串
     * @param clazz 自定义对象的class对象
     * @return 自定义对象
     */
    public static <T> T jsonToObj(String str, Class<T> clazz){
        if(StringUtils.isEmpty(str) || clazz == null){
            return null;
        }
        try {
            return clazz.equals(String.class) ? (T) str : mapper.readValue(str, clazz);
        } catch (Exception e) {
            LOGGER.warn("Parse String to Object error : {}", e.getMessage());
            return null;
        }
    }


    /**
     * 集合对象与Json字符串之间的转换
     * @param str 要转换的字符串
     * @param typeReference 集合类型如List<Object>
     * @param <T> 
     * @return
     */
    public static <T> T jsonToObj(String str, TypeReference<T> typeReference) {
        if (StringUtils.isEmpty(str) || typeReference == null) {
            return null;
        }
        try {
            return (T) (typeReference.getType().equals(String.class) ? str : mapper.readValue(str, typeReference));
        } catch (IOException e) {
            LOGGER.warn("Parse String to Object error", e);
            return null;
        }
    }

    /**
     * 集合对象与Json字符串之间的转换
     * @param str 要转换的字符串
     * @param collectionClazz 集合类型
     * @param elementClazzes 自定义对象的class对象
     * @param <T>
     * @return
     */
    public static <T> T string2Obj(String str, Class<?> collectionClazz, Class<?>... elementClazzes) {
        JavaType javaType = mapper.getTypeFactory().constructParametricType(collectionClazz, elementClazzes);
        try {
            return mapper.readValue(str, javaType);
        } catch (IOException e) {
            LOGGER.warn("Parse String to Object error : {}" + e.getMessage());
            return null;
        }
    }
}
jsonUtil工具类
package com.liyh.utils;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.*;

/**
 * @Author: liyh
 * @Date: 2020/8/27 17:12
 */
public class SqlUtils {

    /**
     * 获取aop中的SQL语句
     * @param pjp
     * @param sqlSessionFactory
     * @return
     * @throws IllegalAccessException
     */
    public static String getMybatisSql(ProceedingJoinPoint pjp, SqlSessionFactory sqlSessionFactory) throws IllegalAccessException {
        Map<String,Object> map = new HashMap<>();
        //1.获取namespace+methodName
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        String namespace = method.getDeclaringClass().getName();
        String methodName = method.getName();
        //2.根据namespace+methodName获取相对应的MappedStatement
        Configuration configuration = sqlSessionFactory.getConfiguration();
        MappedStatement mappedStatement = configuration.getMappedStatement(namespace+"."+methodName,true);
//        //3.获取方法参数列表名
//        Parameter[] parameters = method.getParameters();
        //4.形参和实参的映射
        Object[] objects = pjp.getArgs(); //获取实参
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0;i<parameterAnnotations.length;i++){
            Object object = objects[i];
            if (parameterAnnotations[i].length == 0){ //说明该参数没有注解,此时该参数可能是实体类,也可能是Map,也可能只是单参数
                if (object.getClass().getClassLoader() == null && object instanceof Map){
                    map.putAll((Map<? extends String, ?>) object);
                    //System.out.println("该对象为Map");
                }else{//形参为自定义实体类
                    map.putAll(objectToMap(object));
                    //System.out.println("该对象为用户自定义的对象");
                }
            }else{//说明该参数有注解,且必须为@Param
                for (Annotation annotation : parameterAnnotations[i]){
                    if (annotation instanceof Param){
                        map.put(((Param) annotation).value(),object);
                    }
                }
            }
        }
        //5.获取boundSql
        BoundSql boundSql = mappedStatement.getBoundSql(map);
        return showSql(configuration,boundSql);
    }

    /**
     * 解析BoundSql,生成不含占位符的SQL语句
     * @param configuration
     * @param boundSql
     * @return
     */
    private  static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    String[] s =  metaObject.getObjectWrapper().getGetterNames();
                    s.toString();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 若为字符串或者日期类型,则在参数两边添加''
     * @param obj
     * @return
     */
    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }

    /**
     * 获取利用反射获取类里面的值和名称
     *
     * @param obj
     * @return
     * @throws IllegalAccessException
     */
    private static Map<String, Object> objectToMap(Object obj) throws IllegalAccessException {
        Map<String, Object> map = new HashMap<>();
        Class<?> clazz = obj.getClass();
        //System.out.println(clazz);
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            String fieldName = field.getName();
            Object value = field.get(obj);
            map.put(fieldName, value);
        }
        return map;
    }
}
SqlUtils 工具类

2.5 创建logController

package com.liyh.controller;

import com.liyh.entity.User;
import com.liyh.service.LogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: liyh
 * @Date: 2020/9/12 14:12
 */

@RestController
@RequestMapping("/log")
public class LogController {

    @Autowired
    private LogService logService;

    Logger logger = LoggerFactory.getLogger(LogController.class);


    @RequestMapping("/query/{id}")
    public void query(@PathVariable("id") int id) {
        User user = logService.query(id);
        logger.debug("这个是debug测试来的数据");
        logger.info("这个是info测试来的数据");
        logger.warn("这个是warn测试来的数据");
        logger.error("这个是error测试来的数据");
        System.out.println(user.getName());
    }

    @RequestMapping("/test")
    public void test() {
        int a = 2;
        int b= 0;
        logger.debug("这个是debug测试来的数据");
        logger.info("这个是info测试来的数据");
        logger.warn("这个是warn测试来的数据");
        logger.error("这个是error测试来的数据");
        System.out.println(a/b);
    }

}

3 访问接口,查看控制台打印日志

3.1 正常情况:

测试地址:http://127.0.0.1:8084/log/query/1

控制台:

userLog = UserLog(id=null, requestId=req_1600680898027, methodName=query, methodClass=com.liyh.controller.LogController, requestUrl=http://127.0.0.1:8084/log/query/1, remoteIp=127.0.0.1)
sqlLog = SqlLog(id=null, requestId=req_1600680898027, sql=select * from t_user where id = ?, methodName=query, methodClass=com.liyh.mapper.LogMapper, requestUrl=http://127.0.0.1:8084/log/query/1, remoteIp=127.0.0.1, loadTime=8)

3.2 异常情况:

测试地址:http://127.0.0.1:8084/log/test

控制台:

userLog = UserLog(id=null, requestId=req_1600681075412, methodName=test, methodClass=com.liyh.controller.LogController, requestUrl=http://127.0.0.1:8084/log/test, remoteIp=127.0.0.1)
exceptionLog = ExceptionLog(id=null, requestId=req_1600681075412, methodName=test, methodClass=com.liyh.controller.LogController, requestUrl=http://127.0.0.1:8084/log/test, message=/ by zero, remoteIp=127.0.0.1)

3.3 测试结果:

通过测试拦截,获取到用户操作日志,sql语句,异常日志成功在控制台打印日志信息,我在拦截得配置是拦截得某一个包,也可以通过切点拦截某一个方法。但是通过aop拦截日志这种方法效率比较慢,注意使用场景!!!

另外,获取得日志信息,可以创建数据库,从而把日志保存到数据库。

请关注我的后续博客,实现把拦截得日志上传到阿里云日志服务!!!(阿里日志是收费的)

3.4 项目地址:

 https://gitee.com/liyhGitee/springboot/tree/master/springboot_aop

posted @ 2020-09-27 16:07  helloliyh  阅读(1795)  评论(0编辑  收藏  举报