Spring 学习3--AOP(面向切面编程)

Spring 学习3

AOP(面向切面编程)

利用 AOP 可以对业务逻辑的各部分进行隔离,使业务逻辑的各部分耦合度降低,提高程序可重用性。

简单理解就是把我们程序重复的代码抽取出来,在需要执行的时候,用动态代理的技术,在不改源码的基础上,对我们的已有方法进行增强。

名词解释

Joinpoint(连接点):指被拦截的点,在 Spring 中这些点指方法, Spring 只支持方法类型的连接点。业务方法和增强代码通过连接点联系在一起。

Pointcut(切入点):指我们要对哪些 Joinpoint 进行拦截的定义。 连接点被增强了,它就变成切入点

提示:所有切入点都是连接点,但是,所有连接点不都是切入点。

Advice(翻译–建议,理解为增强可能好一点):指拦截到 Joinpoint 之后所要做的事情就是建议。 这里建议分为:前置建议、后置建议、异常建议、最终建议、环绕建议。

Introduction(引进): 引进是一种特殊的建议在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field

Target(目标): 代理的目标对象。

Weaving(织入): 是指把建议应用到目标对象来创建新的代理对象的过程。 Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入

Proxy(代理): 一个类被 AOP 织入建议后,就产生一个结果代理类。

Aspect(切面): 是切入点和建议(引进)的结合。

开发写代码时,可以先完成核心业务代码,再把公共代码抽取出来制作成建议,在配置文件中声明切入点和建议的联系。Spring 会监控切入点方法的执行,一旦监控到切入点方法要执行,就用代理机制,动态创建目标对象的代理对象,根据建议的类型会在代理对象相应的位置将建议的功能织入,组成完整代码并运行。

在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理(实现接口或继承子类)的方式。

创建演示项目(Maven 方式创建)

pom.xml 文件里添加2个依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rgb3</groupId>
    <artifactId>noteProject</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
    </dependencies>

</project>

aspectjweaver 是因为用到切入点表达式。

AOP 配置介绍(基于 XML )

<aop:config>:开始 AOP 配置。
<aop:aspect>:配置切面。

属性 id:给切面一个唯一标识。 ref:指定建议(增强)类 bean 的 id。

<aop:before>:表示前置建议。

属性 method:指定建议(增强)类中的哪个方法作为前置建议。

pointcut:指定切入点表达式,表达式的含义是指定对哪一个类的哪一个方法增强。

切入点表达式写法:

​ execution(访问修饰符 返回值 包名.包名…类名.方法名(参数))

​ 如:execution(public void com.rgb3.service.impl.AccountServiceImpl.saveAccount())

​ 提示:

​ 访问修饰符可以省略

​ execution(void com.rgb3.service.impl.AccountServiceImpl.saveAccount())

​ 返回值可以用通配符*,表示任意返回值

​ execution(* com.rgb3.service.impl.AccountServiceImpl.saveAccount())

​ 包名可以用通配符*表示任意包,每一级包用1个*。也可以用…表示任意包及其子包

​ execution(* *.*.*.impl.AccountServiceImpl.saveAccount())

​ execution(* *…impl.AccountServiceImpl.saveAccount())

​ 类名和方法名都可以用通配符* 来表示

​ execution(* *…*.*())

​ 方法参数

​ 基本类型可以直接写,如 int

​ 引用类型写包名.类名,如 java.lang.String

​ 可以直接用…表示有无参数都行,即使有参数也可以是任意类型

​ 可以用通配符*表示任意类型,但必须有参数

​ 全通配写法(一般不用):execution(* *…*.*(…))

​ 实际中常用写法

​ execution(* com.rgb3.service.impl.*.*(…))

<aop:after-returning> 后置建议,属性同上
<aop:after-throwing> 异常建议,属性同上
<aop:after> 最终建议,属性同上

演示例子:

IAccountService 接口

public interface IAccountService {
    void saveAccount();//模拟保存账户
}

AccountServiceImpl 类,实现 IAccountService 接口

public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("保存用户");
        //int i=1/0;
    }
}

工具类 AppLogger ,通过打印语句演示作用

//工具类
public class AppLogger {

    //模拟前置打印日志
    public void beforePrintLog(){
        System.out.println("AppLogger工具类的beforePrintLog执行----前置");
    }
    //模拟后置打印日志
    public void afterPrintLog(){
        System.out.println("AppLogger工具类的afterPrintLog执行---后置");
    }
    //模拟出现异常打印日志
    public void throwingPrintLog(){
        System.out.println("AppLogger工具类的throwingPrintLog执行---异常");
    }
    //模拟最终打印日志
    public void finalPrintLog(){
        System.out.println("AppLogger工具类的finalPrintLog执行---最终");
    }
}

bean.xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
             http://www.springframework.org/schema/aop
             http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="accountService" class="com.rgb3.service.impl.AccountServiceImpl"></bean>

    <bean id="appLogger" class="com.rgb3.util.AppLogger"></bean>

    <!--配置AOP-->
    <aop:config>
        <aop:aspect id="logAdvice" ref="appLogger">
            <aop:before method="beforePrintLog" 
                        pointcut="execution(* com.rgb3.service.impl.*.*(..))"></aop:before>

            <aop:after-returning method="afterPrintLog" 
                                 pointcut="execution(* com.rgb3.service.impl.*.*(..))">	
            </aop:after-returning>

            <aop:after-throwing method="throwingPrintLog" 
                                pointcut="execution(* com.rgb3.service.impl.*.*(..))">
            </aop:after-throwing>

            <aop:after method="finalPrintLog" 
                       pointcut="execution(* com.rgb3.service.impl.*.*(..))"></aop:after>
    	</aop:aspect>
    </aop:config>

</beans>

启动类 AppNoteDemo

public class AppNoteDemo {

    public static void main(String[] args) {
        ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
        IAccountService accountService=(IAccountService)ac.getBean("accountService");

        //打印保存用户语句
        accountService.saveAccount();

    }
}

在这里插入图片描述

在这里插入图片描述

在 AccountServiceImpl 类里的 saveAccount()方法里添加一个错误让他出现异常

public void saveAccount() {
        System.out.println("保存用户");
        int i=1/0;
    }

在这里插入图片描述

配置 AOP 语句优化

切入点表达式可以单独写出来,用<aop:pointcut>

<aop:pointcut> 标签可以写在<aop:aspect>标签,表示这个表达式只能在当前的切面里使用。

写在<aop:aspect>标签,表示所有切面都可以用这个表达式,注意,此时表达式要写在切面的前面

属性:

​ id:指定表达式的唯一标识

​ expression:表达式内容

写在<aop:aspect>标签

<aop:config>
        <aop:aspect id="logAdvice" ref="appLogger">

            <aop:before method="beforePrintLog" pointcut-ref="pointCut1"></aop:before>

            <aop:after-returning method="afterPrintLog" pointcut-ref="pointCut1">
            </aop:after-returning>

            <aop:after-throwing method="throwingPrintLog" pointcut-ref="pointCut1">
            </aop:after-throwing>

            <aop:after method="finalPrintLog" pointcut-ref="pointCut1"></aop:after>
            <!--切入点表达式单独写出来-->
            <aop:pointcut id="pointCut1" 
                          expression="execution(* com.rgb3.service.impl.*.*(..))">
            </aop:pointcut>
        </aop:aspect>
    </aop:config>

写在<aop:aspect>标签,此时表达式要写在切面的前面

<aop:config>
        <!--切入点表达式单独写在前面-->
        <aop:pointcut id="pointCut1" 
                      expression="execution(* com.rgb3.service.impl.*.*(..))">
    	</aop:pointcut>
        <aop:aspect id="logAdvice" ref="appLogger">

            <aop:before method="beforePrintLog" pointcut-ref="pointCut1"></aop:before>

            <aop:after-returning method="afterPrintLog" pointcut-ref="pointCut1">
            </aop:after-returning>

            <aop:after-throwing method="throwingPrintLog" pointcut-ref="pointCut1">
            </aop:after-throwing>

            <aop:after method="finalPrintLog" pointcut-ref="pointCut1"></aop:after>

        </aop:aspect>
    </aop:config>
<aop:around> 环绕通知

属性与 before 、after 他们一样

Spring 提供了一个接口 ProceedingJoinPoint ,这个接口有一个方法 proceed() ,这个方法用于调用切入点方法。这个接口可以作为环绕通知的方法参数,在程序执行时, Spring 会提供这个接口的实现类给我们用。

AOP 配置改为:

<!--配置AOP-->
    <aop:config>
        <!--切入点表达式单独写在前面-->
        <aop:pointcut id="pointCut1" expression="execution(* com.rgb3.service.impl.*.*(..))"></aop:pointcut>
        <aop:aspect id="logAdvice" ref="appLogger">

            <aop:around method="aroundPrintLog" pointcut-ref="pointCut1"></aop:around>
        </aop:aspect>
    </aop:config>

AppLogger 类中增加一个方法:

//模拟打印环绕日志
    public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){

        Object returnValue=null;
        Object[] args=proceedingJoinPoint.getArgs();    //得到切入点方法执行所需要的参数
        try {
                System.out.println("AppLogger工具类的aroundPrintLog执行---前置");//前置建议

                returnValue=proceedingJoinPoint.proceed(args);  //明确调用切入点方法

                System.out.println("AppLogger工具类的aroundPrintLog执行---后置");//后置建议
                return returnValue;
        }catch (Throwable t){
            System.out.println("AppLogger工具类的aroundPrintLog执行---异常");//异常建议
            throw new RuntimeException(t);
        }finally {
            System.out.println("AppLogger工具类的aroundPrintLog执行---最终");//最终建议
        }
    }

在这里插入图片描述

returnValue=proceedingJoinPoint.proceed(args) 明确调用切入点方法

  • 在这句之的代码,就是前置建议,像 XML 配置里的<aop:before> 标签
  • 在这句之的代码,就是后置建议,像 XML 配置里的<aop:after-returning> 标签
  • 在 catch 块里的,就是异常建议,像 XML 配置里的<aop:after-throwing> 标签
  • 在 finally 块里的,就是最终建议,像 XML 配置里的<aop:after> 标签

观察一下代码,感觉像是把 XML 配置里的代码,转移到 java 代码里,让我们在代码中手动控制建议方法什么时候执行。

AOP 配置(基于注解)

bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
             http://www.springframework.org/schema/aop
             http://www.springframework.org/schema/aop/spring-aop.xsd
             http://www.springframework.org/schema/context
             http://www.springframework.org/schema/context/spring-context.xsd">

    <!--Spring 创建容器时要扫描的包-->
    <context:component-scan base-package="com.rgb3"></context:component-scan>

    <!--Spring 开启注解 AOP 支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

AccountServiceImpl 类上添加 @Service(“accountService”) 注解,里面的代码与上面的例子一样

  • @Aspect: 写在类上,表示这个类是切面类

  • @Pointcut:参数写切入点表达式。这个注解写在方法上,方法名可作为引用这个切入点表达式的名字。

    //切入点表达式
        @Pointcut("execution(* com.rgb3.service.impl.*.*(..))")
        private void pointCut1(){}
    
  • @Before:前置建议,使用:

    @Before("pointCut1()")
        public void beforePrintLog(){
            System.out.println("AppLogger工具类的beforePrintLog执行----前置");
        }
    
  • @AfterReturning:后置建议,使用同上

  • @AfterThrowing:异常建议,使用同上

  • @After:最终建议,使用同上

  • @Around:环绕建议,使用同上

AppLogger 工具类

@Component("appLogger")
@Aspect //表示这是个切面类
public class AppLogger {

    //切入点表达式
    @Pointcut("execution(* com.rgb3.service.impl.*.*(..))")
    private void pointCut1(){}

    //模拟前置打印日志
    @Before("pointCut1()")
    public void beforePrintLog(){
        System.out.println("AppLogger工具类的beforePrintLog执行----前置");
    }

    //模拟后置打印日志
    @AfterReturning("pointCut1()")
    public void afterPrintLog(){
        System.out.println("AppLogger工具类的afterPrintLog执行---后置");
    }

    //模拟出现异常打印日志
    @AfterThrowing("pointCut1()")
    public void throwingPrintLog(){
        System.out.println("AppLogger工具类的throwingPrintLog执行---异常");
    }

    //模拟最终打印日志
    @After("pointCut1()")
    public void finalPrintLog(){
        System.out.println("AppLogger工具类的finalPrintLog执行---最终");
    }

    //模拟打印环绕日志
    //@Around("pointCut1()")
    public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){

        Object returnValue=null;
        Object[] args=proceedingJoinPoint.getArgs();    //得到切入点方法执行所需要的参数
        try {
                System.out.println("AppLogger工具类的aroundPrintLog执行---前置");//前置建议

                returnValue=proceedingJoinPoint.proceed(args);  //明确调用切入点方法

                System.out.println("AppLogger工具类的aroundPrintLog执行---后置");//后置建议
                return returnValue;
        }catch (Throwable t){
            System.out.println("AppLogger工具类的aroundPrintLog执行---异常");//异常建议
            throw new RuntimeException(t);
        }finally {
            System.out.println("AppLogger工具类的aroundPrintLog执行---最终");//最终建议
        }
    }
}

这里先注释了@Around 环绕建议,演示其他注解的效果
在这里插入图片描述

这里发现最终建议后置建议的顺序不对,百度了下这是 Spring自身的原因,所以开发时注意是否要用注解方式。

如果把 @Before、@AfterReturning、@AfterThrowing、@After 都注释了,使用@Around 注解,顺序就是对的
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值