• SpringBoot+AOP+自定义注解,优雅实现日志记录


    前言

    首先我们看下传统记录日志的方式是什么样的:

    @DeleteMapping("/deleteUserById/{userId}")
    public JSONResult deleteUserById(@PathVariable("userId") Long userId){
        //调用Service实现类方法做删除操作
        userService.deleteUserById(userId);
        //记录操作日志
        LogUtils.addLog("用户模块", "删除用户操作", "12");
        return JSONResult.success();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 日志记录代码与业务代码强耦合,万一哪天需要多记录一个字段到数据库的话,所有调用的地方都需要修改
    2. 许多参数需要花费很大代价才能记录到数据库,比如:请求方法全路径、请求方式(get还是post等)、方法执行耗时、入参、出参、方法执行状态等
    3. 非常不优雅,难维护

    接下来给大家分享一种非常优雅的方式记录日志,就是采用自定义注解+AOP切面编程技术,实现日志记录,现在记录日志的方式就是这样了:

    @PostMapping("/save")
    @MyLog(title = "用户模块", content = "新增用户信息")
    public JSONResult save(@RequestBody UserDto dto){
        //业务逻辑代码这里,省略
        return JSONResult.success(dto);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到,直接使用自定义注解@MyLog完成日志记录即可,与业务代码没有任何耦合,是不是看着非常优雅呢?

    好了,废话不多说,接下来跟着下面的步骤,将这个功能集成到你的项目中的

    准备阶段

    1、数据库日志表

    我们数据库先准备一张记录日志信息的表,建表语句如下:

    CREATE TABLE `sys_oper_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
      `title` varchar(50) DEFAULT '' COMMENT '模块标题',
      `content` varchar(100) DEFAULT NULL COMMENT '日志内容',
      `method` varchar(100) DEFAULT '' COMMENT '方法名称',
      `request_method` varchar(10) DEFAULT '' COMMENT '请求方式',
      `oper_name` varchar(50) DEFAULT '' COMMENT '操作人员',
      `request_url` varchar(255) DEFAULT '' COMMENT '请求URL',
      `ip` varchar(128) DEFAULT '' COMMENT '请求IP地址',
      `ip_location` varchar(255) DEFAULT '' COMMENT 'IP归属地',
      `request_param` varchar(2000) DEFAULT '' COMMENT '请求参数',
      `response_result` varchar(2000) DEFAULT '' COMMENT '方法响应参数',
      `status` int(1) DEFAULT NULL COMMENT '操作状态(0正常 1异常)',
      `error_msg` varchar(2000) DEFAULT NULL COMMENT '错误消息',
      `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
      `take_time` bigint(20) DEFAULT NULL COMMENT '方法执行耗时(单位:毫秒)',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='操作日志记录';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2、自定义注解编写

    好,表已经准备好了,下面是下载到本地到项目:

    在这里插入图片描述

    这里对项目结构就不多做介绍了,在此基础上,我们新建一个包,用来写自定义注解,代码如下:

    package org.js.annotation;import java.lang.annotation.*;/**
     * 自定义注解记录系统操作日志
     */
    //Target注解决定 MyLog 注解可以加在哪些成分上,如加在类身上,或者属性身上,或者方法身上等成分
    @Target({ ElementType.PARAMETER, ElementType.METHOD })
    //Retention注解括号中的"RetentionPolicy.RUNTIME"意思是让 MyLog 这个注解的生命周期一直程序运行时都存在
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyLog
    {
        /**
         * 模块标题
         */
        String title() default "";
        /**
         * 日志内容
         */
        String content() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    OK,到目前为止,我们就新增了一个自定义注解类,现在项目结构变成这样了:

    在这里插入图片描述

    3、AOP切面类编写

    好,自定义注解写好后,我们开始写AOP切面类,需要先导入AOP相关依赖jar包,所以需要在pom.xml中加入下面依赖

    
    
        org.springframework.boot
        spring-boot-starter-aop
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后切面类代码如下:

    package org.js.aop;import com.alibaba.fastjson.JSON;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.*;
    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 org.js.annotation.MyLog;
    import org.js.domain.OperLog;
    import org.js.service.IOperLogService;
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;/**
     * 切面处理类,记录操作日志到数据库
     */
    @Aspect
    @Component
    public class OperLogAspect {@Autowired
        private IOperLogService operLogService;//为了记录方法的执行时间
        ThreadLocal<Long> startTime = new ThreadLocal<>();/**
         * 设置操作日志切入点,这里介绍两种方式:
         * 1、基于注解切入(也就是打了自定义注解的方法才会切入)
         *    @Pointcut("@annotation(org.js.annotation.MyLog)")
         * 2、基于包扫描切入
         *    @Pointcut("execution(public * org.js.controller..*.*(..))")
         */
        @Pointcut("@annotation(org.js.annotation.MyLog)")//在注解的位置切入代码
        //@Pointcut("execution(public * org.js.controller..*.*(..))")//从controller切入
        public void operLogPoinCut() {
        }@Before("operLogPoinCut()")
        public void beforMethod(JoinPoint point){
            startTime.set(System.currentTimeMillis());
        }/**
         * 设置操作异常切入点记录异常日志 扫描所有controller包下操作
         */
        @Pointcut("execution(* org.js.controller..*.*(..))")
        public void operExceptionLogPoinCut() {
        }
    ​
    ​
        /**
         * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
         *
         * @param joinPoint 切入点
         * @param result      返回结果
         */
        @AfterReturning(value = "operLogPoinCut()", returning = "result")
        public void saveOperLog(JoinPoint joinPoint, Object result) {
            // 获取RequestAttributes
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            // 从获取RequestAttributes中获取HttpServletRequest的信息
            HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
            try {
                // 从切面织入点处通过反射机制获取织入点处的方法
                MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                // 获取切入点所在的方法
                Method method = signature.getMethod();
                // 获取操作
                MyLog myLog = method.getAnnotation(MyLog.class);OperLog operlog = new OperLog();
                if (myLog != null) {
                    operlog.setTitle(myLog.title());//设置模块名称
                    operlog.setContent(myLog.content());//设置日志内容
                }
                // 将入参转换成json
                String params = argsArrayToString(joinPoint.getArgs());
                // 获取请求的类名
                String className = joinPoint.getTarget().getClass().getName();
                // 获取请求的方法名
                String methodName = method.getName();
                methodName = className + "." + methodName + "()";
                operlog.setMethod(methodName); //设置请求方法
                operlog.setRequestMethod(request.getMethod());//设置请求方式
                operlog.setRequestParam(params); // 请求参数
                operlog.setResponseResult(JSON.toJSONString(result)); // 返回结果
                operlog.setOperName("张三"); // 获取用户名(真实环境中,肯定有工具类获取当前登录者的账号或ID的,或者从token中解析而来)
                operlog.setIp(getIp(request)); // IP地址
                operlog.setIpLocation("湖北武汉"); // IP归属地(真是环境中可以调用第三方API根据IP地址,查询归属地)
                operlog.setRequestUrl(request.getRequestURI()); // 请求URI
                operlog.setOperTime(new Date()); // 时间
                operlog.setStatus(0);//操作状态(0正常 1异常)
                Long takeTime = System.currentTimeMillis() - startTime.get();//记录方法执行耗时时间(单位:毫秒)
                operlog.setTakeTime(takeTime);
                //插入数据库
                operLogService.insert(operlog);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }/**
         * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
         */
        @AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")
        public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
            // 获取RequestAttributes
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            // 从获取RequestAttributes中获取HttpServletRequest的信息
            HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);OperLog operlog = new OperLog();
            try {
                // 从切面织入点处通过反射机制获取织入点处的方法
                MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                // 获取切入点所在的方法
                Method method = signature.getMethod();
                // 获取请求的类名
                String className = joinPoint.getTarget().getClass().getName();
                // 获取请求的方法名
                String methodName = method.getName();
                methodName = className + "." + methodName + "()";
                // 获取操作
                MyLog myLog = method.getAnnotation(MyLog.class);
                if (myLog != null) {
                    operlog.setTitle(myLog.title());//设置模块名称
                    operlog.setContent(myLog.content());//设置日志内容
                }
                // 将入参转换成json
                String params = argsArrayToString(joinPoint.getArgs());
                operlog.setMethod(methodName); //设置请求方法
                operlog.setRequestMethod(request.getMethod());//设置请求方式
                operlog.setRequestParam(params); // 请求参数
                operlog.setOperName("张三"); // 获取用户名(真实环境中,肯定有工具类获取当前登录者的账号或ID的,或者从token中解析而来)
                operlog.setIp(getIp(request)); // IP地址
                operlog.setIpLocation("湖北武汉"); // IP归属地(真是环境中可以调用第三方API根据IP地址,查询归属地)
                operlog.setRequestUrl(request.getRequestURI()); // 请求URI
                operlog.setOperTime(new Date()); // 时间
                operlog.setStatus(1);//操作状态(0正常 1异常)
                operlog.setErrorMsg(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));//记录异常信息
                //插入数据库
                operLogService.insert(operlog);
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }/**
         * 转换异常信息为字符串
         */
        public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
            StringBuffer strbuff = new StringBuffer();
            for (StackTraceElement stet : elements) {
                strbuff.append(stet + "\n");
            }
            String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
            message = substring(message,0 ,2000);
            return message;
        }/**
         * 参数拼装
         */
        private String argsArrayToString(Object[] paramsArray)
        {
            String params = "";
            if (paramsArray != null && paramsArray.length > 0)
            {
                for (Object o : paramsArray)
                {
                    if (o != null)
                    {
                        try
                        {
                            Object jsonObj = JSON.toJSON(o);
                            params += jsonObj.toString() + " ";
                        }
                        catch (Exception e)
                        {
                            e.printStackTrace();
                        }
                    }
                }
            }
            return params.trim();
        }//字符串截取
        public static String substring(String str, int start, int end) {
            if (str == null) {
                return null;
            } else {
                if (end < 0) {
                    end += str.length();
                }if (start < 0) {
                    start += str.length();
                }if (end > str.length()) {
                    end = str.length();
                }if (start > end) {
                    return "";
                } else {
                    if (start < 0) {
                        start = 0;
                    }if (end < 0) {
                        end = 0;
                    }
                    return str.substring(start, end);
                }
            }
        }/**
         * 转换request 请求参数
         * @param paramMap request获取的参数数组
         */
        public Map<String, String> converMap(Map<String, String[]> paramMap) {
            Map<String, String> returnMap = new HashMap<>();
            for (String key : paramMap.keySet()) {
                returnMap.put(key, paramMap.get(key)[0]);
            }
            return returnMap;
        }//根据HttpServletRequest获取访问者的IP地址
        public static String getIp(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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259

    代码里面的逻辑我就不赘述了,里面的注释写的非常全,大家应该看得懂,不懂的评论区留言即可

    现在项目结构如下:

    在这里插入图片描述

    4、业务层

    4.1、Service 层:

    package cn.js.service;
    
    import cn.js.domain.SysOperLog;
    import cn.js.query.SysOperLogQuery;
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.baomidou.mybatisplus.core.metadata.IPage;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    
    
    /**
     * 

    * 操作日志记录 服务类 *

    * * @author js * @date 2023-11-02 */
    public interface SysOperLogService extends IService<SysOperLog> { IPage<SysOperLog> selectMyPage(SysOperLogQuery query); Page<SysOperLog> selectMySqlPage(SysOperLogQuery query); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    4.2 Service 实现层:

    package cn.js.service.impl;
    
    import cn.hutool.core.util.StrUtil;
    import cn.js.Mapper.SysOperLogMapper;
    import cn.js.domain.SysOperLog;
    import cn.js.query.SysOperLogQuery;
    import cn.js.service.SysOperLogService;
    import com.baomidou.mybatisplus.core.metadata.IPage;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.stereotype.Service;
    import lombok.extern.slf4j.Slf4j;
    import java.util.List;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.beans.factory.annotation.Autowired;
    
    /**
     * 

    * 操作日志记录 服务实现类 *

    * * @author js * @date 2023-11-02 */
    @Transactional @Service @Slf4j public class SysOperLogServiceImpl extends ServiceImpl<SysOperLogMapper, SysOperLog> implements SysOperLogService { @Autowired private SysOperLogMapper sysOperLogMapper; //查询分页列表数据 public IPage<SysOperLog> selectMyPage(SysOperLogQuery query) { QueryWrapper<SysOperLog> wrapper = new QueryWrapper<>(); if (StrUtil.isNotEmpty(query.getKeyword())) { //下面条件根据实际情况修改 wrapper.and( i -> i.like("user_name", query.getKeyword()) .or().like("login_name", query.getKeyword()) ); } //排序(默认根据主键ID降序排序,根据实际情况修改) wrapper.orderByDesc("id"); Page<SysOperLog> page = new Page<>(query.getCurrent(), query.getSize()); return super.page(page, wrapper); } //查询分页列表数据(自己写SQL) public Page<SysOperLog> selectMySqlPage(SysOperLogQuery query) { Page<SysOperLog> page = new Page<>(query.getCurrent(), query.getSize()); List<SysOperLog> list = sysOperLogMapper.selectMySqlPage(page, query); return page.setRecords(list); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    5、测试

    接下来我们就可以测试了,在Controller接口中直接用自定义注解开始记录日志,如下方法使用:

    @GetMapping("/deleteUserById/{userId}")
    @MyLog(title = "用户模块", content = "删除用户操作")
    public JSONResult deleteUserById(@PathVariable("userId") Long userId){
        //这里具体删除用户代码 省略.....
        return JSONResult.success();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后启动项目,浏览器输入地址:http://localhost:8001/deleteUserById/123

    显示结果如下:

    在这里插入图片描述

    说明接口调用成功,看下数据库是否记录了日志:

    在这里插入图片描述

    数据库已经新增了一条日志记录,而且里面记录到信息非常全

    OK,至此,我们以后项目中再记录日志就非常方便了,只需要在方法上面打一个注解就可以了,在AOP里负责往数据库写,方便日后维护

    完整代码

  • 相关阅读:
    《卡拉马佐夫兄弟》人物表
    JavaScript面试常见问题(三)
    一文教你在IDEA中使用Git
    前后台分离开发 YAPI平台 前端工程化之Vue-cli
    【Unity3D】相机跟随
    Nginx学习(在 Docker 中使用 Nginx)
    RabbitMQ的幂等性、优先级队列和惰性队列
    unittest 统计测试执行case总数,成功数量,失败数量,输出至文件,生成一个简易的html报告带饼图
    sparksql broadcast join opt
    【JavaSE】多线程篇(四)线程的同步机制、互斥锁、线程死锁与释放锁
  • 原文地址:https://blog.csdn.net/qq_45525848/article/details/134224450