AOP日志

AOP日志

​ 通过AOP来实现日志框架。

AOP

​ AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。

​ 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

​ 简单回顾一下AOP编程中的相关概念:

  • 横切关注点 是指一个能够解决分散在不同模块中同一类问题的非核心业务(即日志、安全校验、事务处理等不涉及主体业务逻辑)。

  • 通知 是对方法的增强部分,每个横切关注点需要做的事情都需要写一个方法来实现,这个方法可以理解为对原方法的增强部分。当然,根据不同的业务需要,通知的位置也就有所不同,例如,安全校验,需要在业务方法执行前进行校验,对应的就是前置通知。类似的,日志在业务方法执行完后,根据执行结果来存储日志,对应的可能就是后置通知。

    根据位置的不同就有如下五种通知:前置通知,返回通知,异常通知,后置通知,还有一种是以上四种方法的综合,环绕通知

    需要注意上述通知之间的区别,返回通知,对应的是方法正常执行后执行,异常通知,是方法异常结束后执行,后置通知则是会在这两个方法结束后执行(类似于finally)。

  • 切面 就是封装通知的类,一个关注点可以对应一个切面。

  • 目标:被代理的目标对象。

  • 代理:向目标对象应用通知之后创建的代理对象,一般为动态代理。动态代理又可以分为两种:JDK动态代理(兄弟,通过实现和目标一样的接口来进行代理),和Cglib动态代理(没有借口,做不成兄弟,就只能拜为义父,通过继承来进行代理)

    常用的两种动态代理方式

    JDK动态代理,Proxy.newInstance(类加载器,接口,调用处理器),类加载器在代理模式中应用,使用和目标类相同的类加载器,生成实现相同接口的代理类。然后使用相同的类加载器,反射生成代理类的实例,新实例化的实例对象持有InvocationHandler

    CGLib 动态代理,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。

  • 接入点,每个方法中的各个关注点,即通知的各种位置对应不同的接入点。

  • 切入点,定义连接的方式,Spring的AOP技术通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut接口进行描述,使用类和方法作为连接点的查询条件。

代码部分

声明一个注解,将有注解标注的位置作为切入点。

注解-切入点

定义一个注解接口,包含一些操作相关信息,以及控制请求

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    public String title(); //模块名称
    public OperatorType operatorType() default OperatorType.MANAGE; //操作人员类别
    public int businessType(); //业务类型(0 其他 1新增 2修改 3删除)
    public boolean isSaveRequestData() default true; //是否保存请求的参数
    public boolean isSaveResponseData() default true; //是否保留响应的参数
}

Target 表示注解可以声明在哪些目标元素之前。

ElementType可以声明的位置
TYPE类(Class)、接口(interface)、注解(annotaiton interface)、枚举变量(enum)、记录声明(record declaration)
FIELD字段声明,包括枚举实例
METHOD方法
PARAMETER参数声明,可以用于方法的参数上
CONSTRUCTOR构造器
LOCAL_VARIABLELocal variable declaration
ANNOTATION_TYPEAnnotation interface declaration (Formerly known as an annotation type.)
PACKAGEPackage declaration
TYPE_PARAMETERType parameter declaration @since 1.8
TYPE_USEUse of a typeince 1.8
MODULEModule declaration. @since 9
RECORD_COMPONENTRecord component @jls 8.10.3 Record Members * @jls 9.7.4 Where Annotations May Appear @since 16

Retention 中文翻译为保留,可以理解为在何时保留。

​ SOURCE 在编译时忽略,

​ CLASS:会被编译器记录进字节码文件中,但是不会在虚拟机运行时保留。默认值

​ RUNTIME:被编译器记录进字节码文件中,并且在虚拟机运行时保留。一般需要通过注解来实现相应的功能都得选择这个。

定义操作人员类别枚举类型。

public enum OperatorType {
    OTHER,
    MANAGE,
    MOBILE
}

切面

声明一个切面,通过环绕通知来完成日志部分。

@Aspect //声明切面
@Component //保证这个切面类能放入IOC容器
@Slf4j
public class LogAspect {
    @Resource
    private AsyncOperLogService asyncOperLogService;
    
    @Around(value = "@annotation(sysLog)")
    public Object doAroundAdvice(ProceedingJoinPoint joinPoint, Log sysLog){
        // 构建前置参数
        SysOperLog sysOperLog = new SysOperLog();
        LogUtil.beforeHandleLog(sysLog, joinPoint, sysOperLog);
        Object proceed = null;
        try {
            proceed = joinPoint.proceed();
            // 执行业务方法
            LogUtil.afterHandleLog(sysLog, proceed, sysOperLog, 0, "") ;
            // 构建响应结果参数
        } catch (SpzxException e) {
            // 处理 SpzxException 异常
            LogUtil.afterHandleLog(sysLog, proceed, sysOperLog, 1, e.getMessage());
            System.out.println(e.getClass());
            throw e; // 继续抛出 SpzxException 异常
        } catch (Throwable e) {
            // 处理其他异常
            e.printStackTrace(); // 打印异常信息
            LogUtil.afterHandleLog(sysLog, proceed, sysOperLog, 1, e.getMessage());
            System.out.println(e.getClass());
            throw new RuntimeException(); // 抛出新的异常
        } finally {
            // 保存日志数据
            asyncOperLogService.saveSysOperLog(sysOperLog);
        }
        // 返回执行结果
        return proceed ;
    }
}
切入点表达式

value 属性用于指定切入点表达式(Pointcut Expression),它决定了哪些方法会被拦截。

value 属性中可以包含以下几种写法:

  1. 方法签名:例如 execution(* com.example.service.*.*(..)),表示拦截 com.example.service 包下所有类的所有方法。
  2. 类名:例如 within(com.example.service.*),表示拦截 com.example.service 包下的所有类的所有方法。
  3. 参数类型:例如 args(java.lang.String),表示拦截参数类型为 String 的方法。
  4. 返回类型:例如 returning(java.lang.String),表示拦截返回类型为 String 的方法。
  5. 异常类型:例如 throwing(java.lang.Exception),表示拦截抛出 Exception 类型异常的方法。
  6. 注解:例如 @annotation(org.springframework.web.bind.annotation.RequestMapping),表示拦截带有 @RequestMapping 注解的方法。
  7. 组合以上写法:例如 execution(* com.example.service.*.*(..)) && args(java.lang.String),表示拦截 com.example.service 包下所有类的所有方法,且参数类型为 String 的方法。
ProceedingJoinPoint

切入点,ProceedingJoinPoint继承JoinPoint,主要特点为:通过proceed()方法来控制何时继续执行被代理的方法。除此之外,还有继承自JoinPoint的getArgs(),允许访问连接点的方法参数,或者getSignature()方法获取连接点的方法签名对象,可以通过这些了解被调用方法的详细信息。

LogUtil

自定义一个Log的工具类,提供被代理方法被调用前以及被调用后的方法。

  • beforeHandLog,负责在被代理方法调用前,通过Log注解中的信息获取操作模块名称和定义的操作人员类型,然后通过JoinPoint切入点的getSignature获取方法签名对象,获取其方法对应的全类名,之后通过HttpServletRequest获取HTTP请求头的一些信息,例如方法类型、URI与调用接口的IP地址,最终根据Log注解中定义的参数确定是否加入请求参数。

  • afterHandleLog,负责在被代理方法调用后,设定该次请求的状态(正常,异常)以及对应的错误消息,并且根据Log注解中定义的参数确定是否加入响应参数。

public class LogUtil {
    //status 0表示正常,1表示异常
    public static void afterHandleLog(Log sysLog, Object proceed, SysOperLog sysOperLog, int status, String errorMsg) {
        if(sysLog.isSaveResponseData()) {
            sysOperLog.setJsonResult(JSON.toJSONString(proceed));//加载方法返回参数
        }
        sysOperLog.setStatus(status);
        sysOperLog.setErrorMsg(errorMsg);
    }

    //操作执行之前调用
    public static void beforeHandleLog(Log sysLog, ProceedingJoinPoint joinPoint, SysOperLog sysOperLog) {
        //设置操作模块名称
        sysOperLog.setTitle(sysLog.title());
        sysOperLog.setOperatorType(sysLog.operatorType().name());
        //获取目标方法信息 -> 获取声明方法的全类名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        sysOperLog.setMethod(method.getDeclaringClass().getName() + "." + method.getName());
        //获取请求相关参数
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        sysOperLog.setRequestMethod(request.getMethod());
        sysOperLog.setOperUrl(request.getRequestURI());
        sysOperLog.setOperIp(request.getRemoteAddr());
        // 设置请求参数
        if(sysLog.isSaveRequestData()) {
            String requestMethod = sysOperLog.getRequestMethod();
            if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) 			{
                String params = Arrays.toString(joinPoint.getArgs());
                sysOperLog.setOperParam(params);
            }
        }
        sysOperLog.setOperName(AuthContextUtil.get().getUserName());
     }
}

自动装配

因为我们将日志模块作为公共服务抽离出来,放在与其他Spring项目分离的模块中,如果需要在其他的业务服务中使用LogAspect这个切面类,需要将该切面扫描进Spring容器。Spring Boot默认会扫描和启动类所在包相同包中的bean以及子包中的bean。而LogAspect作为一个单独存在公共服务,显然无法直接在业务服务中使用。可以通过自定义直接来实现。

@Target({ElementType.TYPE}) //该注解只能声明在一个类前
@Retention(RetentionPolicy.RUNTIME)
@Import(value = LogAspect.class)            // 通过Import注解导入日志切面类到Spring容器中
public @interface EnableLogAspect {
    
}

异步存储日志

在环绕通知finally语句块中最终调用LogService来存储日志内容,通过@Async实现异步。

注意:

  1. 使用@Async需要在Application中声明@EnableAsync

  2. Spring容器启动初始化bean时,根据类中是否使用了@Async注解,创建切入点和切入点处理器,根据切入点创建代理,在调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池,实现异步执行。

    所以,需要注意的一个错误用法是,如果A类的a方法(没有标注@Async)调用它自己的b方法(标注@Async)是不会异步执行的,因为从a方法进入调用的都是它本身,不会进入代理。

  3. 当使用@Async注解但没有通过value指定线程池时,Spring将自动创建一个默认的线程池来执行异步任务。这个默认线程池是一个SimpleAsyncTaskExecutor,它不使用线程池来执行任务,而是在每次需要执行异步任务时创建一个新的线程。

    SimpleAsyncTaskExecutor是一个基本的实现,适用于执行少量且简单的异步任务。然而,对于大规模或高并发的应用场景,推荐使用更复杂的线程池配置,如ThreadPoolTaskExecutor,以提供更好的性能和资源管理。

@Service
public class AsyncOperLogServiceImpl implements AsyncOperLogService {

    @Resource
    private SysOperLogMapper sysOperLogMapper;

    @Async      //异步执行保存日志操作
    @Override
    public void saveSysOperLog(SysOperLog sysOperLog) {
        sysOperLogMapper.insert(sysOperLog);
    }
}

使用

在目标方法上标注注解即可启动日志。

    @Log(title = "角色添加",businessType = 1)
    @PostMapping("/saveSysUser")
    public Result saveSysUser(@RequestBody SysUser sysUser) {
        sysUserService.saveSysUser(sysUser);
        return Result.build(null, ResultCodeEnum.SUCCESS);
    }

其他

Record

record声明是一种简化数据类的声明方式,自Java 14开始,Record类主要用于存储不可变的数据,可以自动生成getter方法,还有传承自Object的九大方法。

record Person(int name, int age) {}

public static void main(String[] args) {
    Person person = new Person(1, 2);
    System.out.println(person);
    System.out.println(person.age());
    System.out.println(person.name());
}

通知方法

@Before 前置通知

@AfterReturning 后置通知

@AfterThrowing 异常通知

@After 最终通知

@Around 环绕通知

RequestContextHolder

RequestContextHolder是Spring框架中的一个类,它用于存储当前请求的上下文信息。在Spring MVC中,当一个请求到达时,Spring会创建一个RequestContextHolder对象来保存请求相关的数据,如请求参数、请求头等。这样,在处理请求的过程中,可以通过RequestContextHolder获取到这些信息。

例如,要获取当前请求的HttpServletRequest对象,可以使用以下代码:

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
...
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();

HttpServletRequest

当客户端通过HTTP协议访问服务器时,服务器会为每个HTTP请求创建一个HttpServletRequest对象,该对象包含了HTTP请求头中的所有必要信息。具体来说,HttpServletRequest提供了一系列的方法和属性,使得开发者能够获取客户端请求的各种数据,例如:

  • 获取完整的请求URL,可以使用getRequestURL()方法。
  • 获取请求行中的资源名部分,可以使用getRequestURI()方法。URI相比于URL,仅包括资源路径名,不包括协议、端口号等信息。
  • 获取请求行中的参数部分,可以使用getQueryString()方法。
  • 获取请求URL中的额外路径信息,可以使用getPathInfo()方法。
  • 获取发出请求的客户机的IP地址,可以使用getRemoteAddr()方法。
  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值