Spring AOP(面向切面编程)

目录

一、AOP面向切面编程

二、Spring-AOP的简单实现

1、定义被代理类的接口和实现类

2、定义切面:@Aspect+@Component

三、使用AOP环绕通知和注解实现权限验证

1、自定义需要被解析的注解

2、申明接口和接口的实现类

3、定义切面,通过环绕通知增强方法-ProceedingJoinPoint

四、execution()语法定义


一、AOP面向切面编程

AOP 的全称是“Aspect Oriented Programming”,即面向切面编程,和 OOP(面向对象编程)类似,也是一种编程思想。

AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。

简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。AOP 就是代理模式的典型应用。

目前最流行的 AOP 框架有两个,分别为 Spring AOP AspectJ

Spring AOP 是基于 AOP 编程模式的一个框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。有两种实现方式:基于接口的JDK动态代理和基于继承的CGLIB动态代理。

AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。

图解AOP思想,底层使用动态代理来实现原有代码的增强。

如果想了解动态代理,可以阅读我的这篇文章

静态代理、动态代理和GCLib代理

二、Spring-AOP的简单实现

本文使用Springboot(偷懒,不想引太多jar包,哈哈),首先Springboot实现切面编程,需要引入相关依赖

<!--切面AOP-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

1、定义被代理类的接口和实现类

如果被代理类有接口,AOP底层默认使用JDK动代理,否则使用Cglib动态代理。

 声明接口和接口实现类

// 声明接口
public interface AopService {
    void queryById(String userId);
    void deleteById(String userId);
}

// 接口实现类
@Service
public class AopServiceImpl implements AopService {
    @Override
    public void queryById(String id) {
        System.out.println("[AOP测试]:queryById-查询执行,参数:" + id);
    }
    @Override
    public void deleteById(String id) {
        System.out.println("[AOP测试]:deleteById-删除执行,参数:" + id);
    }
}

2、定义切面:@Aspect+@Component

定义切面,非常简单,主要是使用AspectJ相关注解,然后把定义的切面注入到IOC容器中。

@Component
@Aspect
public class Logging { // 切面类
    /**
     * 前置通知
     * execution表达式,指定被代理的目标对象。
     */
    @Before("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))") //通知 + 目标对象
    public void beforeAdvice(){ // 连接点(方法)->模拟日志输出
        System.out.println("[logging]:日志前置通知.");
    }
    /**
     * 后置通知
     */
    @After("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))")
    public void afterAdvice(){ // 连接点(方法)->模拟日志输出
        System.out.println("[logging]:日志后置通知.");
    }
}

注:execution()支持匹配多个,如 : "execution(* com.ws..*.*(*)) || execution(* com.db..*.*(*))";

测试切面

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class UserServiceImplTest {
    @Resource
    private AopService aopService;

    @Test
    public void findUserById() {
        aopService.queryById("888");
    }
    @Test
    public void deleteById() {
        aopService.deleteById("999");
    }
}

// 测试结果
[logging]:日志前置通知.
[AOP测试]:queryById-查询执行,参数:888
[logging]:日志后置通知.
------------------------------------
[logging]:日志前置通知.
[AOP测试]:deleteById-删除执行,参数:999
[logging]:日志后置通知.

如果不会写单元测试,请阅读我的这篇文章

编写简单的SpringBoot单元测试类

引入切点

上边定义的切面中,对相同的execution表达式重复写了多次,为了避免这种重复代码,可以为多个连接点定义一个统一的切点,减少重复代码。改造后的代码如下

@Component
@Aspect
public class Logging {
    /**
     * 申明一个切点
     */
    @Pointcut("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))")
    private void aopPointTest(){}
    /**
     * 前置通知
     */
    @Before("aopPointTest()")
    public void beforeAdvice(){
        System.out.println("[logging]:日志前置通知.");
    }
    /**
     * 后置通知
     */
    @After("aopPointTest()")
    public void afterAdvice(){
        System.out.println("[logging]:日志后置通知.");
    }
}

三、使用AOP环绕通知和注解实现权限验证

1、自定义需要被解析的注解

AOP和注解一起使用,通常可以非常好的简化我们的代码。比如在日志记录或者权限验证等场景中就可以使用到

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginAnnotation { //自定义注解
    // 登录名
    String loginName() default "游客";
    // 角色
    String role() default "";
}

2、申明接口和接口的实现类

// 接口(非必须) ——> 使用接口,底层会进行JDK动态代理
public interface LoginService {
    public void login();
}

// 实现类
@Service
public class LoginServiceImpl implements LoginService {
    @LoginAnnotation(loginName = "sam", role = "MANAGE")
    public void login(){ // 需要进行权限验证的方法
        System.out.println("用户认证后,方法执行...");
    }
}

spring Aop 底层用了动态代理还是 cglib?

  • 如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
  • 如果要被代理的对象不是个实现类,那么,Spring会强制使用CGLib来实现动态代理。

3、定义切面,通过环绕通知增强方法-ProceedingJoinPoint

AOP环绕通知与前置、后置通知的区别在于,环绕通知需要手动调用目标方法,需要指定在什么情况下或者在什么地方调用。

import com.swadian.spring.aop.aopservice.impl.LoginAnnotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
@Aspect
public class LoginAspect {
    /**
     * 申明一个切点
     */
    @Pointcut("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))")
    private void aopPointTest() {
    }

    /**
     * 环绕通知
     */
    @Around("aopPointTest()")
    public void recordLoginInfo(ProceedingJoinPoint pjp) throws Throwable {
        // 首先获取ProceedingJoinPoint 签名  (方法有签名 方法名称 返回值类型 参数类型 及 参数个数)
        Signature signature = pjp.getSignature();
        if (signature instanceof MethodSignature) {
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            Method method = methodSignature.getMethod();
            // 判断方法上的注解是不是权限验证注解
            if (method.isAnnotationPresent(LoginAnnotation.class)) {
                LoginAnnotation annotation = method.getAnnotation(LoginAnnotation.class);
                if (annotation.role().equals("MANAGE")) {
                    System.out.println("[AOP权限验证]:权限验证通过");
                    pjp.proceed(); // 调用目标方法
                    System.out.println("[AOP权限验证]:被代理方法执行结束");
                } else {
                    System.out.println("[AOP权限验证]:该用户没有访问权限!");
                }
            }
        }
    }
}

可以看到在上面的代码中,定义通知的时候在通知方法中添加了入参:ProceedingJoinPoint。在创建环绕通知的时候,这个参数是必须写的。因为在需要在通知中使用ProceedingJoinPoint.proceed()方法调用被通知的方法。

另外,如果忘记调用proceed()方法,那么被代理方法将不会执行

获取当前执行方法所属的包名、类名:

Object target = pjp.getTarget();
String className = target.getClass().getName();  //当前执行方法所属的类、包

测试:

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class LoginServiceImplTest {
    @Autowired
    LoginService loginService;
    @Test
    public void login() {
        loginService.login();
    }
}

// 测试结果
[AOP权限验证]:权限验证通过
用户认证后,方法执行...
[AOP权限验证]:被代理方法执行结束

四、execution()语法定义

例:定义切入点表达式 execution(* com.sample.service.impl..*.*(..))

execution()是最常用的切点函数,其语法如下所示:

整个表达式可以分为五个部分:

  1. execution(): 表达式主体。
  2. 第一个*号:表示返回类型,*号表示所有的类型。
  3. 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
  4. 第二个*号:表示类名,*号表示所有的类。
  5. *(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

swadian2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值