Spring 复习 aop部分(2)

本文详细介绍了Spring中的AOP概念和使用方法,包括切面(Aspect)、连接点(JoinPoint)、切入点(Pointcut)和通知(Advice)。通过@Aspect注解创建切面类,使用@Before、@AfterReturning等注解定义切面执行时机。讲解了切入点表达式的编写,以及JoinPoint在传递目标方法参数中的应用。同时,文章涵盖了后置通知@AfterReturning、环绕通知@Around、异常通知@AfterThrowing和最终通知@After。此外,还探讨了@Pointcut作为辅助注解的作用,以及Spring的动态代理(JDK和CGLIB)。最后,介绍了Spring事务管理的注解配置和AspectJ框架配置方式。
摘要由CSDN通过智能技术生成



前言

该笔记是我在b站看动力节点的视频时对老师所讲的知识点的一个归纳总结

一、AOP概念(Aspect Orient Programming)

AOP(Aspect Orient Programming)面向切面编程,切面指的是:给你的目标类增加的功能,是业务方法以外的功能,叫切面(如事务、日志、统计信息、参数检查、权限验证等等)
AOP需要掌握:

  1. 找出切面:需要在分析项目功能时,找出切面。
  2. 安排切面的执行时间:分析切面是在目标方法之前还是在目标方法之后。
  3. 安排切面执行的位置:在什么类、什么方法上要使用切面

如下概念需要掌握

  1. Aspect(切面)
  2. JoinPoint(连接点)
  3. Pointct(切入点)
  4. 目标对象:需要执行切面的某一个类的函数,该类就是目标对象。
  5. Advice:通知,通知表示切面功能执行的时间。

一个切面有三个关键的要素:

  1. 切面的功能代码,切面干什么
  2. 切面的执行位置,使用Pointcut表示切面执行的位置
  3. 切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。

二、如何使用

我们首先需要知道在什么类上加切面功能,然后开始步骤:

使用流程

先创建切面的包(可以不要这一步,大项目需要)
首先在项目中要创建出放切面类的包(以便于管理)
在这里插入图片描述

然后创建切面类,在类上加@Aspect注解

在这里插入图片描述

在类中创建切面方法,在方法上加入注解(@Before,@AfterReturning这些),这里以@Before为例:

@Before 是 前置通知,在目标方法执行之前先执行。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class MyAspect {
    @Before(value = "execution(public void com.webApp.settings.service.Impl.ExampleServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        System.out.println("Hello,I'm myBefore");
    }
}

当然,该注解写法是最全的写法,它代表:
先找到com.webApp.settings.service.Impl包下的ExampleServiceImpl类中的doSome方法,在这个方法执行之前(Before)先执行切面方法,我们有很多方法简写它,之后再总结。

在spring的xml配置文件中做如下配置

  1. 声明bean标签,管理我们创建的切面类
  2. 创建自动代理生成器

具体如下:

	<!--声明切面类对象-->
    <bean id="myAspectj" class="com.webApp.aspect.MyAspect" />
	
	<!--声明自动代理生成器-->
	<aop:aspectj-autoproxy />

@Before & 切入点表达式使用

在@Before注解中,我们需要写切入点表达式来告诉spring我们需要在哪些方法上执行切面功能,如上示例子中
@Before(value = “execution(public void com.webApp.settings.service.Impl.ExampleServiceImpl.doSome(String, Integer))”)
的value值就告诉了我们需要执行切面的具体方法在什么位置(可以是多个)
其中execution中写的就是切入点表达式,其结构如下:

execution(方法访问限 方法返回类型 方法声明(参数…) 异常类型)

  1. 方法访问限:如 public 、 private 这些 (注:可以省略不写)
  2. 方法返回类型:如int类型,void类型之类
  3. 方法声明:完整的写法是写出在项目中的完整结构(带包名、类名、方法名)
  4. 异常类型:该方法会抛出什么异常 (注:可以省略不写)

注意:以上的语法都可以用通配符来进行大规模的匹配,给很多个方法匹配切面功能。

如上述例子就还可以有如下写法:
例1:

@Aspect
public class MyAspect {
    @Before(value = "execution(public void *..ExampleServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        System.out.println("Hello,I'm myBefore");
    }
}

如上写法带有 “ *.. ” ,表示任意包下的 ExampleServiceImpl 类的带参数String、Integer的、返回值为空(void)的dosome方法

例2:

@Aspect
public class MyAspect {
    @Before(value = "execution(public * *..ExampleServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        System.out.println("Hello,I'm myBefore");
    }
}

这个写法表示除了是任意包下的 ExampleServiceImpl 类的带参数String、Integer的dosome方法,还表示返回类型可以是任意的
例3:

@Aspect
public class MyAspect {
    @Before(value = "execution(public void *..ExampleServiceImpl.doSome(..))")
    public void myBefore(){
        System.out.println("Hello,I'm myBefore");
    }
}

如上写法在方法参数(括号)中带有 “ .. ” ,表示可以匹配任意个参数。

符号意义
*0至任意多个字符
..用在方法参数中,表示任意多个参数,用在包名后,表示当前包以及子包路径
+用在类名后,表示当前类以及子类,用在接口后,表示当前接口以及实现类

再来几个例子:

表达式意义
execution(public * *(..))任意公共方法
execution(* set*(..))任何以set开始的方法

这样我们的功能就加上去了,之后我们再来看更多的功能实现。

JoinPoint 类型(传递目标方法的参数)

经过前面的例子,我们发现,@Before是在该方法之前执行的,我们如果想在切面方法中获取到目标方法的参数,我们该怎么办呢?
答案是使用JoinPoint。

@Aspect
public class MyAspect {

    @Before(value = "execution(public void com.webApp.vo.Test001.doSome())")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("Hello,I'm myBefore");
    }
}

JoinPoint是一个类,放在切面方法的参数的第一个位置必须放在第一个位置,要么不放
它可以获取到目标方法的一系列信息,使用方法如下:

@Aspect
public class MyAspect {

    @Before(value = "execution(public void com.webApp.vo.Test001.doSome(..))")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("Hello,I'm myBefore");
        Object[] args = joinPoint.getArgs();//获取目标方法的参数列表
        Signature signature = joinPoint.getSignature();//获取参数的完整定义
        for (Object i : args)
            System.out.println(i);
    }
}

目标方法如下:

public void doSome(String name,int age) {
        System.out.println("Hello ExampleServiceImpl.doSome!");
    }

执行测试方法:

public class test {
    @Test
    public void test001(){
        String config = "conf/applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
        Test001 test001 = (Test001) applicationContext.getBean("test");
        test001.doSome("小猪猪", 19);
    }
}

得到的结果是:
在这里插入图片描述

@AfterReturning(后置通知)

例子:
现在目标方法为:

public String doSome01() {
        String a = "hello world";
        System.out.println("========执行了doSome01=========\n原本的返回值为:" + a);
        return a;
    }

切面方法为:

@AfterReturning(value = "execution(public void com.webApp.vo.Test001.doSome01(..))", returning = "o")
    public void myAfterReturning(Object o) {

    }

注:1、使用位置也是在方法定义之上 。2、使用JoinPoint要放在参数的首位置

属性:

  1. value :和前置通知@Before一样,用于写切入点表达式的,表明该切面功能执行的位置
  2. returning:自定义的变量,表示目标方法的返回值,不过,自定义变量名必须和通知方法的形参名一样(如下图,名字都必须是“o”)
    在这里插入图片描述

特点:

  1. 在目标方法之后执行
  2. 能够获取目标方法的返回值,可以根据这个返回值做不同的处理功能
  3. 可以修改这个返回值

注意!!!如果值是简单类型,在此方法中直接修改res的值的话,原本的res值是不会发生改变的;但是如果传的是一个对象之类的引用类型,那么res的值就会发生改变!
如下是两个后置通知,第一个res是简单类型(String)第二个是引用类型(Student):

	@AfterReturning(value = "execution(public * com.webApp.vo.Test001.doSome01(..))", returning = "res")
    public void myAfterReturning(Object res) {
        //Object res是目标方法的返回值,我们可以根据这个返回值做一些操作
        System.out.println("后置通知方法执行,获取到的目标方法返回值对象是:" + res);
        res = "I love U";
        System.out.println("现在将后置通知里的res值改成:" + res);
        //注意,如果值是简单类型,在此方法中直接修改res的值的话,原本的res值是不会发生改变的。
        //但是如果传的是一个对象之类的引用类型,那么res的值就会发生改变!
    }


    @AfterReturning(value = "execution(public * com.webApp.vo.Test001.doSome02(..))", returning = "res")
    public void myAfterReturning02(Object res) {
        //Object res是目标方法的返回值,我们可以根据这个返回值做一些操作
        System.out.println("后置通知方法执行,获取到的目标方法返回值对象是:" + res);
        //注意,如果值是简单类型,在此方法中直接修改res的值的话,原本的res值是不会发生改变的。
        //但是如果传的是一个对象之类的引用类型,那么res的值就会发生改变!
        Student student = (Student) res;
        student.setName("littlePig");
        System.out.println("现在将后置通知里的res值改成:" + student);
    }

如下分别是上两个后置通知的目标方法:

    public String doSome01() {
        String a = "hello world";
        System.out.println("========执行了doSome01=========\n原本的返回值为:" + a);
        return a;
    }

    public Student doSome02() {
        Student student1 = new Student(19, "hxy");
        System.out.println("========执行了doSome02=========\n原本的返回值为:" + student1);
        return student1;
    }

执行测试方法:

@Test
    public void test003() {
        String config = "conf/applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
        Test001 test001 = (Test001) applicationContext.getBean("test");
        System.out.println("执行方法:"+test001.doSome01());
        System.out.println("执行方法:"+test001.doSome02());
    }

得到的结果为:

========执行了doSome01=========
原本的返回值为:hello world
后置通知方法执行,获取到的目标方法返回值对象是:hello world
现在将后置通知里的res值改成:I love U
执行方法:hello world

========执行了doSome02=========
原本的返回值为:Student{age=19, name='hxy'}
后置通知方法执行,获取到的目标方法返回值对象是:Student{age=19, name='hxy'}
现在将后置通知里的res值改成:Student{age=19, name='littlePig'}
执行方法:Student{age=19, name='littlePig'}

在这里插入图片描述
由执行结果可知,在后置方法中改简单类型是不会对结果造成影响,而对引用类型会造成影响。

@Around (环绕通知)

环绕通知是功能最强大的,它既有前置通知部分也有后置通知部分,还可以改变目标方法的返回结果(无论简单类型还是引用类型),示范代码如下:

@Around(value = "execution(* *..Test001.doSome03(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        Object result;
        System.out.println("在目标执行方法之前有前置的切面方法");
        result = pjp.proceed();
        System.out.println("目标方法执行完毕,现在得到目标方法的结果:result:" + result);
        result = "i am result!";
        System.out.println("现在改变目标方法的返回值,result:" + result);
        return result;
    }

有几点注意:

  1. 该注解的属性:只有value一个属性:@Around(value = “…”)
  2. 方法要声明为Object,返回的是改变后的目标方法的返回值。
  3. 该方法必须要有一个ProceedingJoinPoint类型的参数(ProceedingJoinPoint pjp)用于控制目标方法什么时候执行,和获取目标方法的返回值
  4. 该切面方法需要抛出 Throwable 异常

其他通知(异常通知 & 最终通知)

其他通知注解还有:异常通知, 最终通知
异常通知(@AfterThrowing)
发生异常的时候会执行该切面方法的代码,相当于try catch结构的catch部分代码

属性

  1. value :和上述其他通知注解的value用法一样。
  2. throwing为自定义变量,表示目标方法抛出的异常对象,变量名必须和方法名的参数名称一样

特点

  1. 在目标方法抛出异常的时候才会执行。
  2. 可以做异常的监控程序,监控目标方法是不是有异常发生(发生的话可以写发送邮件之类的代码通知开发人员)
  3. 要么有一个参数(异常参数),要么是第一个为JoinPoint参数而第二个是异常的参数
    在这里插入图片描述

最终通知(@After)
一定会被执行,相当于try catch finally 中的finally部分,一般用于最后的资源清楚工作的(清除缓存之类的)
用法与其他通知注解大同小异,在此不作过多赘述
在这里插入图片描述

@Pointcut(辅助注解,复用value值)

这是一个辅助功能的注解,它有什么用呢?
如果我们写了很多的切面方法,但是他们的value值都是相同的,那么这个value就冗余了且不好大规模改动,因此出现了@Pointcut注解单独定义value值,定义方法如下:

@Pointcut(value = "execution(* *..Test001.doSome03(..))")
    public void myPointCut(){
        //此处为空就行
    }

然后我们在之前的通知注解中这样使用:

@AfterReturning(value = "myPointCut()", returning = "res")
    private void myAfterReturning02(Object res) {
        ...
    }

注意:value的引号不能丢,而且一般情况都将其定义为私有的(private)

动态代理的方式(jdk & cglib)

我们知道,aop是通过动态代理来实现的,那么我们在使用spring框架的时候用的是什么动态代理方式呢?
我们需要知道,默认情况下,如果我们的目标方法是有接口的话,那么代理方式则是jdk的动态代理方式(通过实现接口来实现aop功能),如果我们的目标方法没有接口,就是一个普通类,则使用的是Spring框架提供的cglib代理方式通过继承父类来实现aop功能

有无接口代理方式采用方式
有接口jdk动态代理实现接口
无接口cglib动态代理继承父类

如果想让项目强制都使用cglib动态代理的话,只需要在spring配置文件中将自动代理生成器添加一个属性proxy-target-class 设置其值为真(proxy-target-class=“true”),这种方式在执行的时候效率会更高一些

<aop:aspectj-autoproxy proxy-target-class="true" />

事务

事务,就是让某一个方法中执行的 很多个 sql语句要么一起执行,要么一起不执行,是绑定在一起的

我们有两种办法来实现事务功能,一是通过spring自带的注解方案添加事务功能( 机制是环绕通知(@Around)),还有一种是通过aspectj框架来配置事务,通过aspectj框架配置事务功能的方法更加适合于大项目的开发,因为方便管理。我们先来说第一种方案:通过注解配置事务功能

注解配置方式(小项目适用)

第一步:在配置文件中声明事务管理器

配置事务一定要先告诉spring我们用的数据库是什么,如何告诉呢?是通过告诉spring容器中管理mybatis容器的方法让其知道的,首先我们要连接数据库:

<!--声明数据源,连接数据库-->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close" >
        <property name="maxActive" value="${jdbc.maxActive}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>

得知数据源的id值(为myDataSource)之后我们就可以来配置事务了:

	<!--声明事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="myDataSource" />
    </bean>

    <!--开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象-->
    <tx:annotation-driven transaction-manager="transactionManager" />

然后在我们想要用事务的方法上加上注解就可以了:

@Transactional
    public String doSome03() {
        System.out.println("========执行了doSome03=========");
        return "doSome03";
    }

注意:最简单的添加方法就是加上**@Transactional**就可以了,但是我们也需要学习他的更详细的用法:

@Transactional(
            propagation = Propagation.REQUIRED,
            isolation = Isolation.DEFAULT,
            readOnly = false,
            rollbackFor = {
                    NullPointerException.class,
                    LoginException.class
            }
    )
    public String doSome03() {
        System.out.println("========执行了doSome03=========");
        return "doSome03";
    }
  1. propagation:传播行为
  2. isolation:隔离级别
  3. readOnly:是否只读
  4. rollbackFor :碰到什么异常时回滚(除了这些还包括运行时异常:RunTimeException

Aspectj框架配置方式(大项目适用)

这种方式区别于注解配置的一个特点就是:业务方法和事务配置完全分离

实现全部都是在配置文件中完成的,假设现在基本的ssm配置已经完成,现在来说配置事务的方式:

<!--声明事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="myDataSource" />
    </bean>

    <!--aspectj的事务配置方案-->
    <tx:advice id="myAdvice" transaction-manager="transactionManager" >
        <tx:attributes>
            <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                       rollback-for="java.lang.NullPointerException"  />
            <tx:method name="add*" propagation="REQUIRED" isolation="DEFAULT"
                       rollback-for="java.lang.NullPointerException, com.webApp.excep.LoginException"  />
            <tx:method name="delete*" propagation="REQUIRED" isolation="DEFAULT"
                       rollback-for="java.lang.NullPointerException"  />
            <!--指定以上方法以外的则直接用*通配其他所有方法-->
            <tx:method name="*" propagation="SUPPORTS" isolation="DEFAULT" read-only="true" />
        </tx:attributes>
    </tx:advice>
    
    <aop:config>
    	<!-- 配置切入点,用于下面的代码和advisor关联 -->
        <aop:pointcut id="servicePt" expression="execution(* com.webApp.*..*(..))"/>
        <!--
        配置增强器:关联advice和pointcut,
        advice-ref:通知,上面tx:advice处的配置
        pointcut-ref:切入点表达式的id
        -->
        <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt" />
    </aop:config>

两种事务配置方法,各有各的优点,如果是小项目,一个注解就可以完成事务的配置,不用写一大堆配置文件,如果是大项目,大项目的类特别多,一个一个加注解特别麻烦,所以采用配置文件来配置的这种分离的方式,可以很快速且方便就能完成事务的配置


总结

本文我们总结了spring框架中aop的最粗略的用法,包括:

1、流程解析:从没有aop功能到添加aop功能的流程解析(前提是基本的ssm配置已经完成)
2、各种切面的介绍:介绍了各种切面注解(前置通知、后置通知、环绕通知、最终通知、异常通知)
3、事务的两种配置方法①小项目适用的通过注解配置。②大项目使用的通过配置文件配置。

之后我会总结SpringMVC的最基本的知识点,目的用于复习知识点,所以会挑重点说,喜欢的看客们留下你的赞吧~
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值