AOP(面向切面编程)

2023-12-27 15:03:07


前言

提示:这里可以添加本文要记录的大概内容:
此篇文章从为什么使用AOP、AOP的基本概念、AOP在项目中的运用进行讲解,其中还嵌套了一小部分的自定义注解的讲解。若是你是零基础的选手,直接跳过“ 一、AOP有啥用?”,从AOP的基本概念开始阅读即可。


提示:以下是本篇文章正文内容,下面案例可供参考

一、AOP有啥用?

老样子,秉持着知其然且知其所以然的初心,先看看AOP到底能给我带来什么好处?请看下面的小🌰

没有使用 AOP 的情况:

假设有一个服务类 MyService,我们希望在每个方法执行前后记录日志。在没有使用 AOP 的情况下,可能会在每个方法中都加入日志记录的代码,导致代码冗余,难以维护。

public class MyService {

    public void doSomething() {
        // 业务逻辑
        System.out.println("Doing something...");
        // 日志记录
        System.out.println("Log: Method 'doSomething' executed.");
    }

    public void doAnotherThing() {
        // 业务逻辑
        System.out.println("Doing another thing...");
        // 日志记录
        System.out.println("Log: Method 'doAnotherThing' executed.");
    }

    // 其他方法...
}

使用 AOP 的情况:

通过使用 AOP,我们可以将日志记录逻辑独立出来,定义一个切面类,以便在每个方法执行前后执行相同的日志记录逻辑。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.MyService.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before executing: " + joinPoint.getSignature().getName());
    }

    @After("execution(* com.example.MyService.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("After executing: " + joinPoint.getSignature().getName());
    }
}

在这个例子中,LoggingAspect 类是一个切面,它定义了 logBeforelogAfter 两个通知方法,用于在目标方法执行前后执行相应的日志记录逻辑。

对比两种情况:

  • 没有使用 AOP:
    • 代码冗余:在每个方法中都有相同的日志记录代码,增加了代码量。
    • 难以维护:如果需要修改日志记录逻辑,需要逐个修改每个方法。
  • 使用 AOP:
    • 模块化:日志记录逻辑被独立成一个切面类,使代码更具模块化。
    • 重用性:相同的日志记录逻辑可以在多个方法中重用,减少了代码的重复性。
    • 易维护性:如果需要修改日志记录逻辑,只需修改切面类,而不需要修改每个方法。

通过使用 AOP,我们能够更清晰地关注业务逻辑,将横切关注点从业务逻辑中分离出来,提高了代码的可维护性和可读性。

二、AOP的基本概念

首先,要明确一点。AOP是一种编程思想,模块化的编程思想。不搞虚的,直接举个🌰。
比如:我们在进行项目开发时,有好几个方法要用到日志记录,那我们就需要在很多个地方log.info(),这样不好的地方就是不标准化,当需要改动的时候需要到处找代码改动。采用AOP的话就可以把日志记录这个功能集中,也就是把它写成一个类里的方法(也就是所谓的模块化)。然后哪个方法需要用,我就在该方法处调用就行。这里的类就是切面类,需要调用日志记录的方法就是连接点。
下面的这些概念以及注解都是AOP(Aspect-Oriented Programming)编程思想的代码实现。

Aop的连接点、切入点、切面、通知的概念

  1. 连接点(Join Point): 连接点是具体执行的方法。
  2. 切入点(Pointcut): 切入点定义了一组连接点的集合,决定在哪里应用切面逻辑。
  3. 通知(Advice): 在拦截到连接点(一般指业务方法)后进行的事儿就是通知。它定义了在连接点(例如方法执行前、执行后、发生异常时)执行的代码。常见的通知类型包括前置通知(Before)、后置通知(After)、返回通知(AfterReturning)和异常通知(AfterThrowing)。
  4. 切面(Aspect): 通知和切入点的结合,也就是切面类。也就是我们的模块化单元,也就是那个类。它包含了一组通知和切点,通知定义了在何时、何地执行横切逻辑,而切点定义了在何处执行。

对于切入点和连接点没有搞太明白的兄弟不要着急,继续往下看!!
对应代码如下:

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.MyService.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before executing: " + joinPoint.getSignature().getName());
    }

    @After("execution(* com.example.MyService.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("After executing: " + joinPoint.getSignature().getName());
    }
}

在上述代码中:

@Aspect 注解标记类为切面。

@Before 注解表示在连接点之前执行的通知。

@After 注解表示在连接点之后执行的通知。

切入点表达式 "execution(* com.example.MyService.*(..))" 匹配了 MyService 类的所有方法。
所以logBefore会在 MyService 类的所有方法执行前通过
System.out.println("Before executing: " + joinPoint.getSignature().getName());进行日志的打印。

??注意:joinPoint这个就是连接点,也就是具体执行的方法,会通过joinPoint.getSignature().getName()拿到该方法对应的具体方法名。
所以在这里切入点匹配的是 MyService 类的所有方法,而每个具体的方法就是joinPoint,也就是连接点。

知识点补充:切入点表达的两种方式
1、第一种:采用@Pointcut注解

@Aspect
public class MyAspect {
	
    // 使用 @Pointcut 注解定义切入点表达式
    @Pointcut("execution(* com.example.MyService.*(..))")
    public void myServiceMethods() {
        // 该方法体通常为空,因为注解值已经定义了切入点表达式
    }

    // 使用 @Before、@Around、@After 等注解引用切入点表达式
    @Before("myServiceMethods()")
    public void beforeMyServiceMethods() {
        // 在切入点方法执行前执行的逻辑
    }

    // 其他通知和切入点方法的定义类似
}

2、第二种:采用execution表达式

@Aspect
public class MyAspect {
	
    // 使用 @Before、@Around、@After 等注解引用切入点表达式
    @Before("execution(* com.example.MyService.*(..))")
    public void beforeMyServiceMethods() {
        // 在切入点方法执行前执行的逻辑
    }

    // 其他通知和切入点方法的定义类似
}

三、AOP在项目中的实际运用

自定义注解 @Auth:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
    String functionCode();
}
  1. @interface Auth
    • @interface 是用于定义注解的关键字。
    • Auth 是注解的名称。
  2. @Target({ElementType.METHOD})
    • @Target 注解指定了注解可以应用的元素类型,这里指定了该注解只能用于方法上。
    • 在这个例子中,@Auth 注解只能应用在方法上,用于标识某个方法需要进行权限检查。
  3. @Retention(RetentionPolicy.RUNTIME)
    • @Retention 注解指定了注解的保留策略,这里设置为 RetentionPolicy.RUNTIME 表示注解在运行时可通过反射获取。
    • 这是因为在运行时我们可能需要通过反射来检查方法上是否存在这个注解以执行相应的逻辑。
  4. String functionCode();
    • 这是注解的一个元素,注解元素定义了注解可以接受的值。
    • 在这个例子中,functionCode 是一个方法,该方法返回一个 String 类型的值。
    • 在使用 @Auth 注解时,你需要提供 functionCode 的值,例如:@Auth(functionCode = "READ_DATA")

AOP 切面 AuthAspect:

@Component
@Aspect
public class AuthAspect {

    @Resource
    private CockpitService cockpitService;
    
	//这里定义了一个切入点
    @Pointcut("@annotation(com.longfor.idata.server.annotation.Auth)")
    public void checkAuth() {

    }
	
	//定义了一个环绕通知,且环绕通知是在checkAuth()这个切入点的方法中的前后执行的
    @Around(value = "checkAuth()")
    public BaseResponse<Object> doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    	//**这一堆是方法执行前的**

    	//获取该连接点的方法签名(方法名、返回类型、参数列表、修饰符)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取方法中的@Auth注解
        Auth authAnno = signature.getMethod().getAnnotation(Auth.class);
        
        String code = authAnno.functionCode();
        
        // 封装了一个从session获取登录名的方法
        String username = getUserName();
        if (StringUtils.isEmpty(username)) {
            throw new ForbiddenException("登录认证信息失效,请重新登录!");
        }
        boolean hasAuth = checkAuthorization(username,code);
        
		//**这是方法执行中**
        //如果有权限
        if (hasAuth) {
        	// 获取方法调用时的参数
            Object[] args = joinPoint.getArgs();
            //继续执行该连接点的方法
            return (BaseResponse) joinPoint.proceed(args);
        }
		
		//**这一堆是方法执行后执行的**
        throw new ForbiddenException("您没有权限访问该功能!");
    }
}

使用注解和AOP的Controller方法:

	 /**
     * 获取需求列表
     *
     * @return
     */
    @PostMapping("/list/all")
	@Auth(functionCode = "READ_DATA")
    public BaseResponse<Object> getAllRequirementList(@RequestBody RequirementSearchDTO searchDTO) {
        return new BaseResponse(requirementService.getRequirementList(searchDTO));
    }

@Auth(functionCode = "READ_DATA") 注解标记在需要进行权限检查的方法上,表示该方法需要权限码为 “READ_DATA” 的权限。当访问该controller方法时, AOP 切面会在这些被标记的方法执行前后进行拦截和处理。若是通过functionCode和userName获取到权限则可以执行这个controller,否则抛出异常“没有权限访问该功能!”

总结

总体来说,AOP是一个强大的编程范式,它使得我们能够更加灵活地管理和组织代码。欢迎个人官爷评论区交流,若有收获请不吝点赞👍哟,🌹🌹🌹

文章来源:https://blog.csdn.net/weixin_54574094/article/details/135243899
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。