SpringBoot开发技巧之使用AOP记录日志示例解析

目录
  • 为什么要用AOP?
  • 常用的工作场景
  • 必须知道的概念
    • AOP 的相关术语
    • Spring 中使用注解创建切面
  • 实战应用-利用AOP记录日志
    • 定义日志信息封装
    • 统一日志处理切面

为什么要用AOP?

答案是解耦!

Aspect Oriented Programming 面向切面编程。解耦是程序员编码开发过程中一直追求的。AOP也是为了解耦所诞生。

具体思想是:定义一个切面,在切面的纵向定义处理方法,处理完成之后,回到横向业务流。

AOP 主要是利用代理模式的技术来实现的。具体的代理实现可以参考这篇文章,讲解的非常详细。https://www.cnblogs.com/yanbincn/archive/2012/06/01/2530377.html

通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

常用的工作场景

事务控制

日志记录

本文没有过度深度学习原理,因为是菜鸟一个,先学会怎么不加班。

必须知道的概念

AOP 的相关术语

通知(Advice)

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

  • 前置通知(Before):在目标方法调用前调用通知功能;
  • 后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;
  • 返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;
  • 异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;
  • 环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。

连接点(JoinPoint)

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

切点(Pointcut)

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

切面(Aspect)

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

引入(Introduction)

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

织入(Weaving)

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

Spring 中使用注解创建切面

相关注解

@Aspect:用于定义切面

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

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

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

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

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

@Pointcut:定义切点表达式

切点表达式

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

execution
(方法修饰符

返回类型

方法所属的包.类名.方法名称(方法参数)
//com.ninesky.study.tiny.controller包中所有类的public方法都应用切面里的通知
execution(public * com.ninesky.study.tiny.controller.*.*(..))
//com.ninesky.study.tiny.service包及其子包下所有类中的所有方法都应用切面里的通知
execution(* com.ninesky.study.tiny.service..*.*(..))
//com.ninesky.study.tiny.service.PmsBrandService类中的所有方法都应用切面里的通知
execution(* com.macro.ninesky.study.service.PmsBrandService.*(..))

实战应用-利用AOP记录日志

从传统行业转行,以前都没想过打日志埋点,第一份工作,真的应该选择一个好的平台比较重要。

定义日志信息封装

用于封装需要记录的日志信息,包括操作的描述、时间、消耗时间、url、请求参数和返回结果等信息

public class WebLog {
    /**
     * 操作描述
     */
    private String description;
    /**
     * 操作用户
     */
    private String username;
    /**
     * 操作时间
     */
    private Long startTime;
    /**
     * 消耗时间
     */
    private Integer spendTime;
    /**
     * 根路径
     */
    private String basePath;
    /**
     * URI
     */
    private String uri;
    /**
     * URL
     */
    private String url;
    /**
     * 请求类型
     */
    private String method;
    /**
     * IP地址
     */
    private String ip;
    /**
     * 请求参数
     */
    private Object parameter;
    /**
     * 请求返回的结果
     */
    private Object result;
    //省略了getter,setter方法
}

定义注解,通过注解减少代码量

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OperationLog {
    String name();//调用接口的名称

    boolean intoDb() default false;//该条操作日志是否需要持久化存储
}
 

统一日志处理切面

@Aspect
@Component
@Order(1)
@Slf4j
public class WebLogAspect {
    private static final Logger controlLog = LoggerFactory.getLogger("tmall_control");
    @Pointcut("execution(public * com.yee.walnut.*.*.*(..))")
    public void webLog() {
    }

    @Before(value = "webLog()&& @annotation(OperationLog)")
    public void doBefore(ControllerWebLog controllerWebLog) throws Throwable {
    }

    @AfterReturning(value = "webLog()&& @annotation(OperationLog)", returning = "ret")
    public void doAfterReturning(Object ret, ControllerWebLog controllerWebLog) throws Throwable {
    }

    @Around(value = "webLog()&& @annotation(OperationLog)")
    public Object doAround(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
        long startTime = System.currentTimeMillis();
        //获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //记录请求信息
        Object[] objs = joinPoint.getArgs();
        WebLog webLog = new WebLog();
        Object result = joinPoint.proceed();//返回的结果,这是一个进入方法和退出方法的一个分界
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        long endTime = System.currentTimeMillis();
        String urlStr = request.getRequestURL().toString();
        webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
        webLog.setIp(request.getRemoteUser());
        webLog.setMethod(request.getMethod());
        webLog.setParameter(getParameter(method, joinPoint.getArgs()));
        webLog.setResult(JSONUtil.parse(result));
        webLog.setSpendTime((int) (endTime - startTime));
        webLog.setStartTime(startTime);
        webLog.setUri(request.getRequestURI());
        webLog.setUrl(request.getRequestURL().toString());
        controlLog.info("RequestAndResponse {}", JSONObject.toJSONString(webLog));
        //必须有这个返回值。可以这样理解,Around方法之后,不再是被织入的函数返回值,而是Around函数返回值
        return result;
    }
    /**
     * 根据方法和传入的参数获取请求参数
     */
    private Object getParameter(Method method, Object[] args) {
        List<Object> argList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argList.add(args[i]);
            }
            //将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (!StringUtils.isEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                map.put(key, args[i]);
                argList.add(map);
            } else {
                argList.add(args[i]);
            }
        }
        if (argList.size() == 0) {
            return null;
        } else if (argList.size() == 1) {
            return argList.get(0);
        } else {
            return argList;
        }
    }
}
 

在方法上加上自定义注解即可

@OperationLog(name = "TurnOnOffStrategy")
public String doOperation(GlobalDto globalDto, DeviceOperator deviceOperator) {
}

以上就是SpringBoot开发技巧之利用AOP记录日志示例解析的详细内容,更多关于SpringBoot开发AOP记录日志的资料请关注我们其它相关文章!

(0)

相关推荐

  • SpringBoot开发教程之AOP日志处理

    目录 日志处理: 需求分析 实现过程: 实验效果: 参考文献: 总结 日志处理: 需求分析 日志处理需要记录的是: 请求的URL 访问者IP 调用的方法 传入的参数 返回的内容 上面的内容要求在控制台和日志中输出. 在学习这部分知识的时候,真的感觉收获很多,在之前Spring学习的aop只是初步了解,现在有了一些深入的理解.好记性不如烂笔头! 在日志处理这部分主要是aop的使用,通过切面的方式来整合到项目了,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率. 人话

  • Springboot接口项目如何使用AOP记录日志

    一. 背景 一直想给项目构建一个统一的日志收集系统,先迈出第一步,构建一个日志收集类,用AOP实现无侵入日志收集 二. 环境 1.此随笔内容基于spring boot项目 2.数据库为mysql 5.7.9版本 3.jdk 版本为1.8 三. 说明 此版采用数据库存储,之后考虑使用elasticsearch等工具存储 四. 内容 1.构建日志采集实体类:BaseLogMessage public class BaseLogMessage { private String serverIP; pr

  • 详解基于SpringBoot使用AOP技术实现操作日志管理

    操作日志对于程序员或管理员而言,可以快速定位到系统中相关的操作,而对于操作日志的管理的实现不能对正常业务实现进行影响,否则即不满足单一原则,也会导致后续代码维护困难,因此我们考虑使用AOP切面技术来实现对日志管理的实现. 文章大致内容: 1.基本概念 2.基本应用 3.日志管理实战 对这几部分理解了,会对AOP的应用应该很轻松. 一.基本概念 项目 描述 Aspect(切面) 跨越多个类的关注点的模块化,切面是通知和切点的结合.通知和切点共同定义了切面的全部内容--它是什么,在何时和何处完成其功

  • 利用spring AOP记录用户操作日志的方法示例

    前言 最近项目已经开发完成,但发现需要加用户操作日志,如果返回去加也不太现实,所以使用springAOP来完成比较合适.下面来一起看看详细的介绍: 注解工具类: @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogAnnotation { String operateModelNm() default ""; String operateFuncNm() default

  • spring-boot使用AOP统一处理日志

    AOP我想大家都很清楚,有时候我们需要处理一些请求日志,或者对某些方法进行一些监控,如果出现例外情况应该进行怎么样的处理,现在,我们从spring-boot中引入AOP. [开发环境:jdk版本号为1.8,spring boot的版本号为1.4.1]{style="background-color:#FF0000"} 首先,我们先引入jar包, POM文件添加如下内容: <!--引用AOP--> <dependency> <groupId>org.s

  • SpringBoot开发技巧之使用AOP记录日志示例解析

    目录 为什么要用AOP? 常用的工作场景 必须知道的概念 AOP 的相关术语 Spring 中使用注解创建切面 实战应用-利用AOP记录日志 定义日志信息封装 统一日志处理切面 为什么要用AOP? 答案是解耦! Aspect Oriented Programming 面向切面编程.解耦是程序员编码开发过程中一直追求的.AOP也是为了解耦所诞生. 具体思想是:定义一个切面,在切面的纵向定义处理方法,处理完成之后,回到横向业务流. AOP 主要是利用代理模式的技术来实现的.具体的代理实现可以参考这篇

  • SpringBoot开发技巧之如何处理跨域请求CORS

    目录 SpringBoot处理跨域 特殊情况 解决方法 自定义Cors过滤器 通过配置类指定过滤器的优先级 在前后分离的架构下,我们经常会遇到跨域CORS问题,在浏览器上的表现就是出现如下一段错误提示: No 'Access-Control-Allow-Origin' header is present on the requested resource. 下面看一下如何让你的SpringBoot项目支持CORS跨域. SpringBoot处理跨域 在SpringBoot后端处理跨域比较简单,只

  • java开发WMS仓库商品预警需求示例解析

    目录 1.预警需求 1. 预警需求分析 2.后端代码实现: 1. 定时器任务 2.优化加入队列 3.注意:关于Mq 4.测试是否成功 3.前端提一嘴 前端展示: 1.预警需求 为了更好的管理商品日期,需要对仓库的商品进行预警管理,对商品的保质期控制在一个范围内提示出来,也可以通过该功能间接的展示出一个商品的的销量程度和对下次进货做个考量! 1. 预警需求分析 前端界面需要设置商品的预警天数 后端保存预警天数 数据库有字段存放商品需要预警的天数 通过定时器运行指点方法算出对应那些商品的批次存低于设

  • SpringBoot开发技巧启动时配置校验实现示例

    目录 概述 增加启动校验 校验类型 定制校验逻辑 测试验证结果 小结 概述 在项目开发过程中,某个功能需要依赖在配置文件中配置的参数.这时候就可能出现下面这种现象问题: 有时候经常出现项目启动了,等到使用某个功能组件的时候出现异常,提示参数未配置或者bean注入失败. 有没有一种方法在项目启动时就对参数进行校验而不是在实际使用的时候再抛出提示呢? 答案就是使用Spring提供的Java Validation功能,简单实用. 增加启动校验 只需要在我们创建的配置Properties类增加Valid

  • Java SpringBoot开发小技巧详解

    目录 一.SpringBoot开发小技巧 1.1 Lombok 1.2 dev-tools 1.3 Spring Initializr 总结 一.SpringBoot开发小技巧 1.1 Lombok 作用:在程序编译的时候,自动帮我们生成setter和getter方法以及我们的toString方法和我们的全参和无参构造器等等. 那么,怎么用呢?很简单,用下边这四个注解就行了: 1.@Data:自动生成setter和getter方法. 2.@ToString:自动生成toString方法. 3.@

  • Flutter开发技巧ListView去除水波纹方法示例

    正文 ScrollConfiguration( behavior: NoScrollBehaviorWidget(), child: ListView( ...... ...... ), ), 调用ScrollConfiguration官方类,实现behavior NoScrollBehaviorWidget用于去除水波纹的自定义Widget import 'package:flutter/material.dart'; /// 去除listview水印 /// ScrollConfigurat

  • springboot整合@Retryable实现重试功能的示例代码

    目录 前言 @Retryable 简介 使用步骤 1.引入依赖 2.启用@Retryable 3.添加@Retryable注解 4.测试 注意事项 最后 结语 前言 在实际工作中,重试机制是一个很常见的场景,比如:发送消息失败,下载网络文件失败等…,因为这些错误可能是网络波动造成的,等待一些延迟就能成功处理.我们通常会使用try/catch.while循环等进行相关处理,但是这样看起来比较臃肿复杂,且不好看.于是就有了spring提供的重试模块—— @Retryable @Retryable 简

  • SpringBoot集成本地缓存性能之王Caffeine示例详解

    目录 引言 Spring Cache 是什么 集成 Caffeine 核心原理 引言 使用缓存的目的就是提高性能,今天码哥带大家实践运用 spring-boot-starter-cache 抽象的缓存组件去集成本地缓存性能之王 Caffeine. 大家需要注意的是:in-memeory 缓存只适合在单体应用,不适合与分布式环境. 分布式环境的情况下需要将缓存修改同步到每个节点,需要一个同步机制保证每个节点缓存数据最终一致. Spring Cache 是什么 不使用 Spring Cache 抽象

随机推荐