Spring AOP的概念及使用

AOP的概念、思想

OOP语言提供了类与类之间纵向的关系(继承、接口),而AOP补充了横向的关系(比如在不改变目标类中源代码的情况下给com.john.demo.dao包下所有类中以insert和update开头的方法添加事务管理)

面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。方面实现了跨越多种类型和对象的关注点(例如事务管理)的模块化。(这些担忧在AOP文献中通常被称为“横切”问题。)

Spring的一个关键组件是AOP框架。虽然Spring IoC容器不依赖于AOP(意味着您不需要使用AOP),但AOP补充了Spring IoC以提供非常强大的中间件解决方案。

AOP在Spring Framework中用于:

  • 提供声明性企业服务。最重要的此类服务是 声明式事务管理。
  • 让用户实现自定义方面,补充他们使用AOP的OOP。
主要功能

日志记录,性能统计,安全控制,事务处理,异常处理等等。

主要意图

将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

为什么用aop

  1. 就是为了方便,看一个国外很有名的大师说,编程的人都是“懒人”,因为他把自己做的事情都让程序做了。用了aop能让你少写很多代码,这点就够充分了吧
  2. 就是为了更清晰的逻辑,可以让你的业务逻辑去关注自己本身的业务,而不去想一些其他的事情,这些其他的事情包括:安全,事物,日志等。

在这里插入图片描述

AOP术语

切面(Aspect)

我们将自己需要插入到目标业务逻辑中的代码模块化, 通过AOP使之可以横切多个类的模块,称之为切面。

在Spring AOP配置中切面通常包含三部分:

  • 切面模块本身
  • 通知
  • 切入点

示例:

<!-- 目标业务逻辑代码 -->
<bean id="calc" class="com.lanou3g.spring.simple.calc.CalcImpl"/>

<!-- 切面模块化对象(代表我们要附加到原始业务逻辑中的代码) -->
<bean id="calcAspect" class="com.lanou3g.spring.simple.calc.CalcAspect" />

<!-- 示例说明: 将切面calcAspect中的代码插入到calc原始业务代码中 -->
<aop:config>
    <!-- 定义公用的切入点表达式,如果aspect中有多个通知,都可以通过pointcut-ref复用 -->
    <aop:pointcut id="all_calc_method" expression="execution(* com.lanou3g.spring.simple.calc.CalcImpl.*(..))" />
    <aop:aspect ref="calcAspect">
        <!-- 切面包含的通知(什么时间)、切入点(什么地点) -->
        <aop:around method="computeTime" pointcut-ref="all_calc_method" />
    </aop:aspect>
</aop:config>

切入点 (Pointcut)

在 Spring AOP 中,需要使用 AspectJ 的切点表达式来定义切点。

AspectJ 指示器描述
execution ()用于匹配连接点的执行方法 最常用
args ()限制连接点的指定参数为指定类型的执行方法
@args ()限制连接点匹配参数类型由指定注解标注的执行方法
this ()限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类
target ()限制连接点匹配特定的执行对象,目标对象是指定的类型
@target ()限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型注解
within()限制连接点匹配指定类型,比如哪个包下,或哪个类里面
@within()限制连接点匹配指定注释所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里)
@annotation限制匹配带有指定注释的连接点

通知(Advice)

环绕通知(around)
  • 在目标方法执行前、后被通知, 可以获取连接点对象(ProceedingJoinPoint, 该对象可以获取被拦截方法的签名、参数、返回值、包括调用与否)
  • 该方法的返回值,即代表了真正业务逻辑代码的返回值
  • 可以选择终止或正常执行目标方法
前置通知(before)

在目标方法调用前通知切面, 什么参数也无法获取。也不能终止目标方法执行

后置(返回值)通知(after returning)

只有在目标方法 正常 执行结束后才会通知, 在通知方法中可以获取到方法的返回值

后置(最终)通知 (after)

在目标方法执行结束后通知切面, 什么参数也无法获取。无论目标方法是正常执行结束还是抛出异常终止,都会被通知

异常通知(after throwing)

只有在目标方法 出现异常 才会通知, 在通知方法中可以获取到抛出的异常信息

五种通知的示例

见下面的 XML方式示例(五种通知)注解方式示例(五种通知)

连接点(JoinPoint)

spring仅支持方法连接点

  • 因为 Spring 基于动态代理,所以 Spring 只支持方法连接点。

  • Spring 缺失对字段连接点的支持,无法让我们更加细粒度的通知,例如拦截对象字段的修改

  • Spring 缺失对构造器连接点支持,我发在 Bean 创建时候进行通知。

连接点有很多种,比如方法执行期间(开始执行、执行结束、抛出异常)、字段修饰符、字段值被更改…

在Spring AOP中只支持方法连接点(因为Spring AOP底层是通过动态代理实现的)。

连接点与切入点的关系可以简单理解为: 切入点一定是连接点, 连接点不一定是切入点。

织入(Weaver)

织入的过程其实就是Spring AOP帮我们把切面中的代码织入到目标代码中的过程。

使用步骤

添加依赖包

在mavan工程中添加下面的依赖

<dependency>           
 <groupId>org.aspectj</groupId>        
 <artifactId>aspectjweaver</artifactId>
 <version>1.9.4</version>
</dependency>

xml方式配置

AOP 配置元素描述
aop : advisor定义 AOP 通知器
aop : after定义 AOP 后置通知(不管被通知方法是否执行成功)
aop : after-returing定义 AOP after-returing 通知
aop : after-throwing定义 AOP after-throwing 通知
aop : around定义 AOP 环绕通知
aop : aspect定义切面
aop : aspectj-autoproxy启动 @AspectJ 注解驱动的切面
aop : before定义 AOP 前置通知
aop : config顶层的 AOP 配置元素,大多数 aop : * 元素必须包含在 元素内
aop : declare-parents为被通知的对象引入额外接口,并透明的实现
aop : pointcut定义切点

加入aop schema

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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">

</beans>

定义切面类,并配置到applicationContext中

<bean id="calcAspect" class="com.lanou3g.spring.simple.calc.CalcAspect" />

CalcAspect.java

public class CalcAspect {
    /**
     * 计算方法耗时
     * 环绕通知
     * @param joinPoint 代表连接点对象,该对象可以获取被代理方法的所有信息
     */
    public Object aroundM(ProceedingJoinPoint joinPoint) throws Throwable {

        // 获取连接点代表的方法的签名
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        Object[] args = joinPoint.getArgs();

        System.out.println("[aroundM] ---- 目标方法"+methodName+"("+ Arrays.toString(args)+")开始执行");
        long start = System.currentTimeMillis();
        // 调用目标方法
        Object retVal = joinPoint.proceed();

        // 插入我们自己的逻辑代码
        long timer = System.currentTimeMillis() - start;

        System.out.println("[aroundM] ---- 目标方法["+methodName+"("+ Arrays.toString(args)+")]执行结束,返回值: "+retVal+", 耗时: " + timer + "ms.");

        // 正常返回目标方法的返回值
        return retVal;
    }

    public void beforeM() {
        System.out.println("[beforeM] ---- 目标方法开始执行");
    }

    public void afterReturningM(Object retVal) {
        System.out.println("[afterReturningM] ---- 目标方法执行结束,返回值: " + retVal);
    }

    public void afterFinallyM() {
        System.out.println("[afterFinallyM] ---- 方法执行结束");
    }

    public void afterThrowing(Throwable throwable) {
        System.out.println("[afterThrowing] ---- 方法执行出错", throwable);
    }
}

定义 < aop:config >

<aop:config>
        <aop:pointcut id="all_calc_method" expression="execution(* com.lanou3g.spring.simple.calc.CalcImpl.*(..))" />
        <aop:aspect ref="calcAspect">
            <aop:around method="computeTime" pointcut-ref="all_calc_method" />
        </aop:aspect>
</aop:config>
引用切面bean
<bean id="calcAspect" class="com.lanou3g.spring.simple.CalcAspect" />
定义切入点表达式

在 Spring AOP 中,需要使用 AspectJ 的切点表达式来定义切点。

AspectJ 指示器描述
execution ()用于匹配连接点的执行方法 最常用
args ()限制连接点的指定参数为指定类型的执行方法
@args ()限制连接点匹配参数类型由指定注解标注的执行方法
this ()限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类
target ()限制连接点匹配特定的执行对象,目标对象是指定的类型
@target ()限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型注解
within()限制连接点匹配指定类型,比如哪个包下,或哪个类里面
@within()限制连接点匹配指定注释所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里)
@annotation限制匹配带有指定注释的连接点
切入点表达式语法:
语法一:单个表达式

切入点表达式语法

<aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..))" />
  • execution表达式以 * 号开头,标识了我们不关心的方法返回值的类型。
  • 后我们指定了权限定类名和方法名。(类名和方法名都可以用号通配符代替)
  • 对于方法的参数列表,使用(…)标识切点选择任意的 play( ) 方法,无论入参是什么。
语法二:多个表达式组合

切入点表达式语法

<aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..)) &amp;&amp; within(com.lanou3g.spring)" />

这里我们使用 &&(在xml中是关键字,需要转义) 将 execution( ) 和 within( ) 连接起来,形成的 and 关系。同理也可以使用 || 或关系、!非关系

定义通知
<aop:around method="computeTime" pointcut-ref="all_calc_method" />
通知类型:引入切入点表达式、织入切面bean中的方法
 <bean id="calcAspect" class="com.lanou3g.spring.simple.CalcAspect" />
    <!-- 示例说明: 将切面calcAspect中的代码插入到calc原始业务代码中 -->
    <aop:config>
        <aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..))" />
        <aop:aspect ref="calcAspect">
            <!-- 环绕通知(around) -->
            <aop:around method="aroundMethod" pointcut-ref="all_method" />
            <!-- 前置通知(before) -->
            <aop:before method="beforeMethod" pointcut-ref="all_method" />
            <!-- 后置(返回值)通知(after returning) -->
            <aop:after-returning method="afterReturningMethod" pointcut-ref="all_method" returning="retVal" />
            <!-- 后置(最终)通知 (after) -->
            <aop:after method="afterFinallyMethod" pointcut-ref="all_method" />
            <!-- 异常通知(after throwing) -->
            <aop:after-throwing method="afterThrowingMethod" pointcut-ref="all_method" throwing="throwable" />
        </aop:aspect>
    </aop:config>
XML方式示例(五种通知)
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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="calc" class="com.lanou3g.spring.simple.CalcImpl" />
    <!-- 切面模块化对象(代表我们要附加到原始业务逻辑中的代码) -->
    <bean id="calcAspect" class="com.lanou3g.spring.simple.CalcAspect" />
    <!-- 示例说明: 将切面calcAspect中的代码插入到calc原始业务代码中 -->
    <aop:config>
        <aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..))" />
        <aop:aspect ref="calcAspect">
            <!-- 环绕通知(around) -->
            <aop:around method="aroundMethod" pointcut-ref="all_method" />
            <!-- 前置通知(before) -->
            <aop:before method="beforeMethod" pointcut-ref="all_method" />
            <!-- 后置(返回值)通知(after returning) -->
            <aop:after-returning method="afterReturningMethod" pointcut-ref="all_method" returning="retVal" />
            <!-- 后置(最终)通知 (after) -->
            <aop:after method="afterFinallyMethod" pointcut-ref="all_method" />
            <!-- 异常通知(after throwing) -->
            <aop:after-throwing method="afterThrowingMethod" pointcut-ref="all_method" throwing="throwable" />
        </aop:aspect>
    </aop:config>
</beans>
切面模块化对象
public class CalcAspect {
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取连接点代表的方法的签名
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        Object[] args = joinPoint.getArgs();

        System.out.println("[aroundMethod] ---- 目标方法" + methodName + "(" + Arrays.toString(args) + ")开始执行");

        long start = System.currentTimeMillis();
        // 调用目标方法
        Object retVal = joinPoint.proceed();

        // 插入我们自己的逻辑代码
        long timer = System.currentTimeMillis() - start;

        System.out.println("[aroundMethod] ---- 目标方法[" + methodName + "(" + Arrays.toString(args) + ")]执行结束,返回值: " + retVal + ", 耗时: " + timer + "ms.");

        // 正常返回目标方法的返回值
        return retVal;
    }

    public void beforeMethod() {
        System.out.println("[beforeMethod] ---- 目标方法开始执行");
    }

    public void afterReturningMethod(Object retVal) {
        System.out.println("[afterReturningMethod] ---- 目标方法执行结束,返回值: " + retVal);
    }

    public void afterFinallyMethod() {
        System.out.println("[afterFinallyMethod] ---- 方法执行结束");
    }

    public void afterThrowingMethod(Throwable throwable) {
        System.out.println("[afterThrowingMethod] ---- 方法执行出错" + throwable);
    }
}
目标业务逻辑代码
//	逻辑业务接口
public interface Calc {
    public int sum(int num1, int num2);

    public int product(int num1, int num2);
}

//	接口实现类
public class CalcImpl implements Calc {
    @Override
    public int sum(int num1, int num2) {
        return num1 + num2;
    }

    @Override
    public int product(int num1, int num2) {
        // 模拟方法执行报错,便于看到after-throwing通知
        int ret = 9 / 0;
        return num1 * num2;
    }
}

//	程序运行入口
import com.lanou3g.spring.simple.Calc;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Calc calc = ctx.getBean(Calc.class);

        int sum = calc.sum(527, 682);
        System.out.println("控制台输出:"+ sum);

        int product = calc.product(5, 89);
        System.out.println("控制台输出:" + product);

    }
}
控制台输出结果
//程序正常运行结果
[aroundMethod] ---- 目标方法sum([527, 682])开始执行
[beforeMethod] ---- 目标方法开始执行
[aroundMethod] ---- 目标方法[sum([527, 682])]执行结束,返回值: 1209, 耗时: 1ms.
[afterReturningMethod] ---- 目标方法执行结束,返回值: 1209
[afterFinallyMethod] ---- 方法执行结束
控制台输出:1209

//模拟异常运行结果
[aroundMethod] ---- 目标方法product([5, 89])开始执行
[beforeMethod] ---- 目标方法开始执行
[afterFinallyMethod] ---- 方法执行结束
[afterThrowingMethod] ---- 方法执行出错java.lang.ArithmeticException: / by zero

注解方式

开启自动织入支持
在xml中开启
<!-- 开启aop注解支持 -->
<aop:aspectj-autoproxy />

<!-- 开启注解支持,同时强制指定代理机制为cglib -->
<aop:aspectj-autoproxy proxy-target-class="true" />
通过注解开启
@Configuration
// 开启注解支持,同时强制指定代理机制为cglib(注:一般不指定) 
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class MyAOPConf {}
定义切面类,添加@Aspect注解、@Component注解

由于@Aspect注解没有让Spring作为组件bean扫描的能力,所以我们需要额外添加@Component注解

定义切入点
/**
     * 通过@Pointcut注解定义切入点表达式
     * 此处表达式含义:拦截com.lanou3g.spring.simple.say包下所有类(包括子包中所有类)中的所有方法
     */
    @Pointcut("execution(* com.lanou3g.spring.simple.say..*.*(..))")
    public void say_all_method() {}
定义通知
@AfterReturning(pointcut = "com.lanou3g.spring.GlobalPointcut.say_all_method()", returning = "ret")
    public Object afterRM(Object ret) {
        log.debug("[afterRM] 返回值: " + ret);
        return ret;
    }
注解方式示例(五种通知)
定义系统中所有用到的切入点表达式
/**
 * 定义系统中所有用到的切入点表达式
 */
@Component
public class GlobalPointcut {

    /**
     * 通过@Pointcut注解定义切入点表达式
     * 此处表达式含义:拦截com.lanou3g.spring.annotation包下所有类(包括子包中所有类)中的所有方法
     */
    @Pointcut("execution(* com.lanou3g.spring.annotation.*.*(..))")
    public void all_method() {}

}
切面模块化对象
@Component
@Aspect
public class ClacAspectAnnotation {

    @Around("com.lanou3g.spring.annotation.GlobalPointcut.all_method()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取连接点代表的方法的签名
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("[aroundMethod] ---- 目标方法" + methodName + "(" + Arrays.toString(args) + ")开始执行");

        long start = System.currentTimeMillis();
        // 调用目标方法
        Object retVal = joinPoint.proceed();
        // 插入我们自己的逻辑代码
        long timer = System.currentTimeMillis() - start;
        System.out.println("[aroundMethod] ---- 目标方法[" + methodName + "(" + Arrays.toString(args) + ")]执行结束,返回值: " + retVal + ", 耗时: " + timer + "ms.");

        // 正常返回目标方法的返回值
        return retVal;
    }

    @Before("com.lanou3g.spring.annotation.GlobalPointcut.all_method()")
    public void beforeMethod() {
        System.out.println("[beforeMethod] ---- 目标方法开始执行");
    }

    @AfterReturning(value = "com.lanou3g.spring.annotation.GlobalPointcut.all_method()",returning = "retVal")
    public void afterReturningMethod(Object retVal) {
        System.out.println("[afterReturningMethod] ---- 目标方法执行结束,返回值: " + retVal);
    }

    @AfterThrowing(value = "com.lanou3g.spring.annotation.GlobalPointcut.all_method()",throwing = "throwable")
    public void afterThrowingMethod(Throwable throwable) {
        System.out.println("[afterThrowingMethod] ---- 方法执行出错" + throwable);
    }

    @After("com.lanou3g.spring.annotation.GlobalPointcut.all_method()")
    public void afterFinallyMethod() {
        System.out.println("[afterFinallyMethod] ---- 方法执行结束");
    }
}
目标业务逻辑代码
//	逻辑业务接口
public interface CalcAnnotation {
    int sum(int num1, int num2);
    int product(int num1, int num2);
}

//	接口实现类
@Component
public class CalcAnnotationImpl implements CalcAnnotation {

    @Override
    public int sum(int num1, int num2) {
        return num1 + num2;
    }

    @Override
    public int product(int num1, int num2) {
        // 模拟方法执行报错,便于看到after-throwing通知
        int ret = 9 / 0;
        return num1 * num2;
    }
}

//	程序运行入口
@Configuration
@ComponentScan(basePackages = "com.lanou3g.spring.annotation")
@EnableAspectJAutoProxy
public class AppAnnotation {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppAnnotation.class);
        CalcAnnotation calcAnnotation = ctx.getBean(CalcAnnotation.class);

        int sum = calcAnnotation.sum(213,3213);
        System.out.println("控制台输出:"+ sum);

        int product = calcAnnotation.product(5, 89);
        System.out.println("控制台输出:" + product);

    }
}
控制台输出结果
//程序正常运行结果
[aroundMethod] ---- 目标方法sum([213, 3213])开始执行
[beforeMethod] ---- 目标方法开始执行
[aroundMethod] ---- 目标方法[sum([213, 3213])]执行结束,返回值: 3426, 耗时: 1ms.
[afterFinallyMethod] ---- 方法执行结束
[afterReturningMethod] ---- 目标方法执行结束,返回值: 3426
控制台输出:3426

//模拟异常运行结果
[aroundMethod] ---- 目标方法product([5, 89])开始执行
[beforeMethod] ---- 目标方法开始执行
[afterFinallyMethod] ---- 方法执行结束
[afterThrowingMethod] ---- 方法执行出错java.lang.ArithmeticException: / by zero
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值