【Spring02】Aop面向切面编程入门详解:切入点表达式、注解

Aop面向切面编程

概述

底层是采用动态代理模式实现的。采用了两种代理:JDK的动态代理,CGLIB的动态代理。

AOP其实就是动态代理的一种规范化实现。

Aspect:切面:给目标类增加的方法,一般称为切面。一般都是非业务方法,可以独立使用。常见的有:日志,事务,统计信息,参数检查,权限验证。

JoinPoint:连接点,连接业务和切面的位置

Pointcut:切入点,指多个连接点的集合

advice:通知:表示切面功能的执行时间。

切面的三个关键要素

1、切面的功能。

2、切面的执行位置,使用PointCut表示。

3、切面的执行时间,使用advice表示,在目标方法之前还是之后。

aspectJ框架

一个开源的aop框架。spring集成了。使用方式有两种:

1、xml配置文件:配置全局事务。

2、注解;注解有五个

aspectJ

切面的执行时间

在规范中叫做Advance(通知;增强)

在aspectJ框架中使用注解表示,也可以使用xml配置:

@Before

@AfterReturning

@Around

@AfterThrowing

@After

切面的位置(切入点表达式

aspectJ框架定义了专门的表达式,用于指定切入点:

execution(<修饰符> <返回类型> <包.类.方法(参数)> <异常>) 
    
修饰符和异常可以省略。
    
* : 任意个字符
.. : 用在方法参数中,表示任意个参数。用在包名处,表示包、子孙包下的所有类。
+ : 用在类名后,表示当前类及其子类。用在接口后,表示当前接口及其实现类

使用例子

  • execution(public * *(..)) :匹配目标类的所有public方法,第一个*代表返回类型,第二个*代表方法名,..代表方法的参数。
  • execution(**User(..)) :匹配目标类所有以User为后缀的方法。第一个*代表返回类型,*User代表以User为后缀的方法
  • execution(* com.cad.demo.User.*(..)) :匹配 User 类里的所有方法
  • execution(* com.cad.demo.User+.*(..)) :匹配该类的子类包括该类的所有方法
  • execution(* com.cad.*.*(..)) :匹配 com.cad 包下的所有类的所有方法
  • execution(* com.cad..*.*(..)) :匹配 com.cad 包下、子孙包下所有类的所有方法
  • execution(* addUser(Spring, int)) :匹配 addUser 方法,且第一个参数类型是 String,第二个参数类型是 int

使用

  1. 加入依赖

  2. 创建目标类,给此类增加功能

  3. 创建切面类

    1. 在类上加入注解@Aspect
    2. 在切面类中创建方法
    3. 在方法上加入Advance注解:如@Before
    4. 还需要指定切入点表达式
  4. 创建spring配置文件,声明对象,将对象交由容器管理

    1. 声明目标对象
    2. 声明切面类对象
    3. 声明aspectJ中的自动代理生成器标签
      • 自动代理生成器:用来完成代理对象的自动创建功能

加入依赖

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
</dependencies>

创建目标类,给此类增加功能

@Component
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {
        System.out.println("=====目标类doSome=====");
    }
}

创建切面类

/*
* 表示当前类是切面类
* */
@Component("myAspect")
@Aspect
public class MyAspect {
    /*
    * 定义方法,实现切面功能
    * 要求:
    * 1、公共方法public
    * 2、没有返回值void
    * 3、如果有参数,则只能为JoinPoint(下面会说)
    * 前置:@Before:
    * 属性:value:切入点表达式
    * 特点:
    * 在目标方法前执行
    * 不会改变、影响目标方法的执行和结果
    * */
    @Before(value = "execution(public void com.wm.ba01.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        System.out.println("前置通知:方法执行时间"+ new Date());
    }

}

创建spring配置文件,声明对象,将对象交由容器管理

<!-- 扫描注解,创建对象 -->
<context:component-scan base-package="com.wm.ba01"/>
<!--
        声明自动代理生成器:使用aspectJ的内部功能,创建目标对象的代理对象
        创建代理对象是在内存中实现的,修改目标目标对象在内存中的结构。创建为代理对象
        所以目标对象就是被修改后的代理对象。
        aspectj-autoproxy会将spring容器中的所有对象都生成代理对象
-->
<aop:aspectj-autoproxy/>

测试使用

@Test
public void test01(){
    String config = "applicationContext.xml";
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
    //从容器中获取目标对象
    SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
    someServiceImpl.doSome("a", 1);
}

执行结果

前置通知:方法执行时间Tue Aug 04 23:44:05 CST 2020
=====目标类doSome=====

JoinPoint:连接点参数

连接点就表示被代理的方法。

相当于jdk自带代理的Method参数。

表示业务方法:如果需要获取被代理方法的名称、实参等信息时,可以加入JoinPoint参数。

必须是第一个位置的参数

@Before(value = "execution(public void com.wm.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore(JoinPoint joinPoint){

    System.out.println("获取方法的签名(定义):"+ joinPoint.getSignature());
    System.out.println("获取方法的名称:"+ joinPoint.getSignature().getName());
    Object[] args = joinPoint.getArgs();
    for (Object arg : args) {
        System.out.println("args:"+ arg);
    }
    System.out.println("前置通知:方法执行时间"+ new Date());
}

执行结果为:

获取方法的签名(定义):void com.wm.ba01.SomeService.doSome(String,Integer)
获取方法的名称:doSome
args:a
args:1
前置通知:方法执行时间Wed Aug 05 09:47:51 CST 2020
=====目标类doSome=====
name:a, age:1

注解

@Before

在被代理方法之前执行,如上“使用”环节的演示。

@AfterReturning

目标方法:

@Override
public String doOther(String name, Integer age) {
    System.out.println("=====doOther=====");
    System.out.println("name:"+name+", age:"+age);
    return "abc";
}

通知方法:

   /*
    * 后置通知方法的定义:
    * 要求:
    * 1、公共方法public
    * 2、没有返回值void
    * 3、方法有参数,推荐是Object类型的,参数名自定义
    * @AfterReturning:
    * 属性:
    * value:切入点表达式
    * returning:自定义的变量,表示目标方法的返回值。
    *            自定义变量名必须和通知方法的形参名一样。
    *			即returning = "res",和 void myAfterReturning(Object res),res要一样
    * 特点:
    * 在目标方法后执行
    * 能够获取目标方法的返回值,可以根据这个值做不同的处理
    * Object res = doOther();
    * 可以修改这个返回值
    * */
    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
            returning = "res")
    public void myAfterReturning(Object res){
        //Object res:是目标方法的返回值,可以根据这个返回值做不同的功能处理
        System.out.println("后置通知:获取的返回值:"+ res);
        //在此处可以对res进行修改等操作,但是因为String是不可修改的,这里返回值是String就不做演示,下方会展示
    }

修改返回值:

实体类

@Component
public class Student {
    String name;
    int age;
}

目标方法

@Override
public Student doOther2(String name, Integer age) {
    Student student = new Student();
    student.setAge(age);
    student.setName(name);
    return student;
}

通知方法:

@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther2(..))",
                returning = "res")
public void myAfterReturning2(Object res){
    
    System.out.println("后置通知:获取的返回值:"+ res);
    Student student = (Student)res;
    //这里将student对象的name属性修改。
    student.setName("修改了");
}

测试

@Test
public void test03(){
    String config = "applicationContext.xml";
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
    //从容器中获取目标对象
    SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
    Student s = someServiceImpl.doOther2("b", 2);
    System.out.println(s);
}

结果为:

后置通知:获取的返回值:Student{name='b', age=2}
Student{name='修改了', age=2}
//这里可以发现,确实修改了目标方法的返回值

另外

@AfterReturning的方法也可以用JoinPoint,但要放在参数的第一个位置。

@Around

基本使用:

目标方法:

@Override
public String doFirst(String name, Integer age) {
    System.out.println("===doFirst===");
    return "doFirst";
}

增强方法:

/*
    * 环绕通知方法定义:
    * 1、public
    * 2、必须有返回值,推荐Object
    * 3、必须有参数:ProceedingJoinPoint类型的
    *
    * @Around(value = ""):环绕通知
    * 属性:value:切入点表达式
    * 特点:
    * 功能最强的通知
    * 在目标方法前后都能增强目标方法功能
    * 控制目标方法是否被调用执行
    * 修改目标方法的执行结果,影响最后的调用结果
    * 等同于jdk动态代理的InvocationHandler接口
    *
    * 参数:ProceedingJoinPoint就等同于Method
    * 返回值:就是目标方法的执行结果,可以被修改
    * */
@Around(value = "execution(* com.wm.ba02.SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
    //实现环绕通知
    //0、在目标方法前执行
    System.out.println("目标方法前执行"+ new Date());
    //1、目标方法调用
    Object o = pjp.proceed();
    //2、目标方法后执行
    System.out.println("目标方法后执行");
    return o;
}

测试和执行结果

@Test
public void test01(){
    String config = "applicationContext.xml";
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
    //从容器中获取目标对象
    SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
    someServiceImpl.doFirst("doFirst", 2);
}

目标方法前执行Wed Aug 05 12:15:52 CST 2020
===doFirst===
目标方法后执行

另外,ProceedingJoinPoint继承自JoinPoint,因此JoinPoint的方法也都可以用。

环绕通知用的最多,经常被使用在事务方面,方法执行前开启事务,方法执行后提交事务。

@AfterThrowing

在目标方法抛出异常时执行

增强方法:

/*
    * 异常通知方法定义:
    * 1、public
    * 2、没有返回值
    * 3、必须有参数:Exception类型,可选:JoinPoint
    *
    * @AfterThrowing:异常通知
    * 属性:value:切入点表达式
    *       throwing:自定义的,表示目标方法抛出的异常。
    *       变量名必须和通知方法的Exception参数的名字一样
    * 特点:
    * 在目标方法抛出异常时执行
    * 可以做异常的监控程序,监控目标方法执行时有无异常
    * 如果有异常可以发邮件、短信通知
    * */
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",throwing = "e")
public void myAfterThrowing(Exception e){
    System.out.println("异常通知:"+e.getMessage());
}

让后我们让目标方法抛出异常:

@Override
public void doSecond() {
    System.out.println("====doSecond===="+ 10/0);//这里让除数为0
}

测试并查看执行结果:

@Test
public void test01(){
    String config = "applicationContext.xml";
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
    //从容器中获取目标对象
    SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
    someServiceImpl.doSecond();
}
//结果
异常通知:/ by zero

java.lang.ArithmeticException: / by zero

@After

最终通知:总是会执行(就算中途有异常,也会执行)

增强方法:

   /*
   *最终通知方法定义规则:
   * public void
   * 参数可以没有,如果有,只能是JoinPoint
   *
   * @After(value = ""):value:切入点表达式
   * 特点:
   * 总是会执行(就算中途有异常,也会执行)
   * 类似 finally 代码块
   * 在目标方法之后执行
   * 一般用来做资源清除
   * */
   @After(value = "")
   public void myAfter(){
       System.out.println("====myAfter====");
   }

@Pointcut

用来定义、管理切入点表达式的。当有多个重复的切入点表达式需要复用时,则可以通过@Pointcut标签来进行复用。

使用:

  • 定义在一个方法之上(void,无需代码)
  • @Pointcut(value = "")在value中定义要复用的表达式
  • 在要使用@Pointcut表达式的方法上,用@Pointcut之下的方法名表示

例子如下:

@After(value = "Pointcut()")
public void myAfter(){
    System.out.println("====myAfter====");
}
@Before(value = "Pointcut()")
public void myBefore(){
    System.out.println("====myBefore====");
}
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void Pointcut(){

}

以上动态代理全部是基于jdk的动态代理。

而若是不是继承接口的普通类,想要实现动态代理的方法,也是一样的,不过默认就是不是jdk的方式,而是spring提供的CGLIB

如果继承了接口,又想使用CGLIB动态代理的话,则可以在配置文件中加入:

<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 即,将proxy-target-class设置为true -->
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值