6.spring-aop

原文地址:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop

一、基本概念

1、AOP

面向切面编程:Aspect Oriented Programming,对于项目业务所关心的处理逻辑之外的、覆盖多个模块的、相似的功能,我们可以将其收集在一个横切面中来解决这类混杂、分散的功能,这将会使我们更好的专注于业务逻辑。例如:事务管理、日志记录、参数校验、服务调用信息打印、锁重试等等。

2、相关术语

①切面(aspect):模块化的横切关注点组成的对象

②横切关注点(crossingcut concern point):与多个类相关的功能点

③通知(advice):用来完成切面对目标对象要做的任务

④连接点(joinPoint):方法执行的位置,也就是通知要作用的位置

⑤切点(pointCut):匹配和查找需要通知的连接点

⑥目标(targetObject):需要切面执行通知的目标对象

⑦代理对象(proxyObject):为目标对象完成切面功能而创建的代理对象

3、五种通知类型

①前置通知(BeforeAdvice):方法执行前执行的通知

②返回通知(AfterReturningAdvice):方法正常成功返回执行的通知,可以返回结果值

③异常通知(AfterThrowingAdvice):方法发生异常执行的通知,可以返回异常信息

④后置通知(或最终通知)(AfterAdvice):方法最终退出执行的通知,不管正常返回还是发生异常都会执行,类似try-catch中的finally

⑤环绕通知(AroundAdvice):方法执行的周围都会执行的通知,能够完成①②③④合在一起的通知任务

二、以@AspectJ注解形式使用AOP

1、Java配置启用切面自动代理配置

package com.csdn.spring.aop;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}

2、编写切面并由IOC容器管理

package com.csdn.spring.aop;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class aopAspect {
}

3、利用切点表达式设置切点

package com.csdn.spring.aop;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AopAspect {
    /**
     * 共通切点
     * 切点表达式中的execution方法:使用通配符 * 匹配 所有包中修饰符是 
     * public、任意返回类型、任意参数的方法作为切点
     */
    @PointCut("execution(public * *.(..))")
    public void commonPonitCut() {}

    /**
     * 特殊切点
     * 限制切点必须是名称为transfer()的方法及重载的方法
     */
    @PointCut("within(* transfer(..))")
    public void specialPonitCut() {}

    /**
     * 结合共通切点和特殊切点, 可以使用 && || !
     */
    @PointCut("commonPonitCut() && specialPonitCut()")
    public void combinePonitCut() {}
}

4、设置通知及切点

package com.csdn.spring.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AopAspect {
    /**
     * 共通切点
     * 切点表达式中的execution方法:使用通配符 * 匹配 所有包中修饰符是 
     * public、任意返回类型、任意参数的方法作为切点
     */
    @PointCut("execution(public * *(..))")
    public void commonPointCut() {}

    /**
     * 特殊切点
     * 限制切点必须是名称为包com.csdn.spring.aop下的方法
     */
    @PointCut("within(com.csdn.spring.aop..*)")
    public void specialPointCut() {}

    /**
     * 特殊切点
     * 限制切点必须是名称为divide的方法
     */
    @PointCut("execution(* divide(..))")
    public void dividePointCut() {}

    /**
     * 结合共通切点和特殊切点
     * 注意:由于测试需要,所以不匹配divide方法
     * 可以使用 && || !
     */
    @PointCut("commonPointCut() && specialPointCut() && (!dividePointCut())")
    public void combinePointCut() {}

    /**
     * 前置通知
     * JoinPoint:可以获取到连接点处的切面信息,签名(方法声明)、目标对象、代理对象、方法入参、方法描述
     */
    @Before("combinePointCut()")
    public void beforeAdvice(JoinPoint jp) {
        // 签名,即方法声明
        Signature signature = jp.getSignature();
        // 方法名
        String methodName = signature.getName();
        // 入参数组
        Object[] args = jp.getArgs();
        // 拼接入参为字符串用于输出
        StringBuffer argStr = new StringBuffer("");
        if (args != null) {
            for (Object arg : args) {
                argStr.append(arg);
                argStr.append(" ");
            }
        }
        // 输出信息
        System.out.println("beforeAdvice...前置通知执行:" + methodName + ", 入参:" + argStr.toString());
    }

    /**
     * 返回通知
     * returning属性指定返回变量名称要与通知声明的参数名称一致
     * result:通知声明的返回值类型可以任意
     */
    @AfterReturning(pointcut = "combinePointCut()", returning = "result")
    public void afterReturningAdvice(JoinPoint jp, Object result) {
        // 签名,即方法声明
        Signature signature = jp.getSignature();
        // 方法名
        String methodName = signature.getName();
        // 输出信息
        System.out.println("afterReturningAdvice...返回通知执行:" + methodName + ", 结果:" + result);
    }

    /**
     * 异常通知
     * 注意:由于测试需要,只匹配divide方法
     * throwing属性指定异常变量名称要与通知声明的参数名称一致
     * e:通知声明的异常类型可以任意
     */
    @AfterThrowing(pointcut = "dividePointCut()", throwing= "e")
    public void afterThrowingAdvice(JoinPoint jp, Throwable e) {
        // 签名,即方法声明
        Signature signature = jp.getSignature();
        // 方法名
        String methodName = signature.getName();
        // 输出信息
        System.out.println("afterThrowingAdvice...异常通知执行:" + methodName + ", 异常信息:" + e);
    }
    
    /**
     * 后置通知(或最终通知)
     */
    @After("combinePointCut()")
    public void afterAdvice(JoinPoint jp) {
        // 签名,即方法声明
        Signature signature = jp.getSignature();
        // 方法名
        String methodName = signature.getName();
        // 输出信息
        System.out.println("afterAdvice...后置通知执行:" + methodName);
    }

    /**
     * 环绕通知
     * 注意:由于测试需要,只匹配divide方法
     * ProceedingJoinPoint是JoinPoint子类,必须声明此参数 
     */
    @Around("dividePointCut()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) {
        // 签名,即方法声明
        Signature signature = pjp.getSignature();
        // 方法名
        String methodName = signature.getName();
        // 入参数组
        Object[] args = pjp.getArgs();
        // 拼接入参为字符串用于输出
        StringBuffer argStr = new StringBuffer("");
        if (args != null) {
            for (Object arg : args) {
                argStr.append(arg);
                argStr.append(" ");
            }
        }
        // 类似前置通知
        System.out.println("aroundAdvice...环绕通知执行:" + methodName + ", 入参:" + argStr.toString());
        Object retValue = null;
        try {
            retValue = pjp.proceed();
            // 类似返回通知
            System.out.println("aroundAdvice...环绕通知返回:" + methodName + ", 结果:" + retValue);
        } catch (Throwable e) {
            // 类似异常通知
            System.out.println("aroundAdvice...环绕通知异常:" + methodName + ", 异常信息:" + e);
        }
        // 类似后置通知
        System.out.println("aroundAdvice...环绕通知完成:" + methodName);
        
        return retValue;
    }
}

5、创建计算类进行测试

①创建spring的applicationContext.xml配置文件,开启组件扫描

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.csdn.spring"/>
</beans>

②在包com.csdn.spring或其子包下创建计算类并添加方法

package com.csdn.spring.aop;

import org.springframework.stereotype.Component;

/**
 * 计算类需要被IOC容器管理,因此要加注解@Component使其可以被扫描到
 */
@Component
public class Calculator {
    /**
     * 两数相加
     * 返回包装类型是因为在环绕通知返回的类型为引用类型,使用基本类型会出现拆箱失败错误
     */
    public Integer add(int i, int j) {
        return i + j;
    }

    /**
     * 两数相除
     * 返回类型说明:参考add()方法
     */
    public Double divide(int i, int j) {
        return Double.valueOf(i/j);
    }
}

③编写main方法执行计算验证AOP

import com.csdn.spring.aop.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringApplication {
    public static void main(String[] args) {
        // 读取类路径下的spring配置文件创建IOC容器
        ApplicationContext cxt = new 
                                 ClassPathXmlApplicationContext("applicationContext.xml");
        // 从IOC容器中根据Beanm名称获取bean实例
        Calculator calculator = (Calculator) cxt.getBean("calculator");
        // 使用bean实例,调用加法
        Integer addResult = calculator.add(1, 1);
        // 输出计算结果
        System.out.println("calculator.add's result: " + addResult);
        // 调用除法
        Double divideResult = calculator.divide(4, 2);
        // 输出计算结果
        System.out.println("calculator.divide's result: " + divideResult);
    }
}

控制台输出:根据切点设置,加法没有执行环绕通知,除法只执行了环绕通知

beforeAdvice...前置通知执行:add, 入参:1 1 
afterReturningAdvice...返回通知执行:add, 结果:2
afterAdvice...后置通知执行:add
calculator.add's result: 2
aroundAdvice...环绕通知执行:divide, 入参:
aroundAdvice...环绕通知返回:divide, 结果:2.0
aroundAdvice...环绕通知完成:divide
calculator.divide's result: 2.0

前面的测试并没有执行异常通知,下面测试

import com.csdn.spring.aop.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringApplication {
    public static void main(String[] args) {
        // 读取类路径下的spring配置文件创建IOC容器
        ApplicationContext cxt = new 
                                 ClassPathXmlApplicationContext("applicationContext.xml");
        // 从IOC容器中根据Beanm名称获取bean实例
        Calculator calculator = (Calculator) cxt.getBean("calculator");
        // 调用除法,除数是0发生异常
        Double divideResult = calculator.divide(4, 0);
        // 输出计算结果
        System.out.println("calculator.divide's result: " + divideResult);
    }
}

控制台输出:根据切点设置,环绕通知和异常通知都匹配divide方法的切点,所以发生异常时都执行了并打印异常。注意,返回通知没有执行是因为切点设置时将divide方法排除匹配范围了,否则是会执行的。

aroundAdvice...环绕通知执行:divide, 入参:4 0 
afterThrowingAdvice...异常通知执行:divide, 异常信息:java.lang.ArithmeticException: / by zero
aroundAdvice...环绕通知异常:divide, 异常信息:java.lang.ArithmeticException: / by zero
aroundAdvice...环绕通知完成:divide
calculator.divide's result: null

三、使用注解@Pointcut的指示符定义切点表达式

注:连接点是指使用AOP时方法的执行

①execution: 匹配方法执行连接点,AOP中主要使用的指示符。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

modifiers-pattern:修饰符模式,可选

ret-type-pattern:返回类型模式,必填,返回类型模式确定方法的返回类型必须是什么,才能匹配连接点,* 可以匹配任意返回类型。

declaring-type-pattern:声明类型模式,可选

name-pattern:名称模式,必填,匹配给定名称的方法,可以使用通配符  *  作为名称模式的部分或全部,如果指定声明类型模式,请包含尾部  .  将其连接到名称模式组件

(param-pattern):(参数模式),必填,() 匹配无参、(..) 匹配任意个参数、(*) 匹配任意类型的一个参数、(*,  String) 匹配两个参数,第一个任意类型,第二个String类型。

throws-pattern:异常模式,可选

举例:

// 匹配任意public方法
execution(public * *(..))
// 匹配任意以set开头的方法
execution(* set*(..))
// 匹配AccountService接口中定义的任意方法
execution(* com.xyz.service.AccountService.*(..))
// 匹配service包定义的任意方法
execution(* com.xyz.service.*.*(..))
// 匹配service或其子包中定义的任意方法
execution(* com.xyz.service..*.*(..))

②within: 限制匹配确定类型的连接点 (方法执行的声明要在匹配类型当中)。

// 匹配service包中的任意连接点
within(com.xyz.service.*)
// 匹配service及其子包中的任意连接点
within(com.xyz.service..*)

③this: 限制匹配bean引用(AOP代理)是给定类型实例的连接点,常用于绑定形式。

// 匹配代理实现AccountService接口的任意连接点
this(com.xyz.service.AccountService)

④target: 限制匹配目标对象 (被代理的应用对象) 是给定类型实例的连接点,常用于绑定形式。

// 匹配目标对象实现AccountService接口的连接点
target(com.xyz.service.AccountService)

⑤args: 限制匹配参数是给定类型实例的连接点,常用于绑定形式。

// 匹配有一个运行时传递参数是Serializable的连接点
args(java.io.Serializable)

⑥@target: 限制匹配执行对象的类具有给定类型注解的连接点。

// 匹配目标对象的声明类型中具有@Transactional注解的连接点
@target(org.springframework.transaction.annotation.Transactional)

⑦@args:  限制匹配传递的实参的运行时类型具有给定类型注解的连接点。

// 匹配一个传递参数的运行时类型具有@Classified注解的连接点
@args(com.xyz.security.Classified)

⑧@within: 限制匹配给定注解中的类型的连接点。

// 匹配目标对象的声明类型中具有@Transactional注解的连接点
@within(org.springframework.transaction.annotation.Transactional)

⑨@annotation: 限制匹配具有给定注解的连接点。

// 匹配具有@Transactional注解的正在执行的方法的连接点
@annotation(org.springframework.transaction.annotation.Transactional)

四、代理机制

1、AOP中使用的创建代理方式:

①JDK动态代理

②CGLIB(通用开源类定义库,在包spring-core中)

2、代理创建依据:

①如果代理对象至少实现了一个接口,使用JDK动态代理,并且目标类型实现的全部接口都将会被代理

②如果代理对象未实现任何接口,使用CGLIB创建代理

3、如果硬要使用CGLIB的话,需要考虑两个问题:

①使用CGLIB,由于final方法在运行时的子类中不能被重写,所以不能被通知

②从Spring4.0起,代理对象的构造方法不能重复调用,因为CGLIB代理实例是通过Objenesis类库创建的

五、AOP代理

①SpringAOP是基于代理的,掌握这个对于使用AOP切面是非常重要的

如下是一个普通的对象引用:

public class SimplePojo implements Pojo {
    public void foo() {
        // 直接调用this引用的bar()方法
        this.bar();
    }
    public void bar() {
        // some logic...
    }
}

如果调用一个对象引用的方法,这个方法会在对象引用上被直接调用,如图所示:

aop proxy plain pojo call

public class Main {
    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // 在pojo引用上直接调用方法
        pojo.foo();
    }
}

接下来,将客户端代码改为具有代理引用,如图所示:

aop proxy call

public class Main {
    public static void main(String[] args) {
        // 为类创建代理工厂
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        // 被代理的接口
        factory.addInterface(Pojo.class);
        // 添加通知
        factory.addAdvice(new RetryAdvice());
        // 为类创建对象并给对象创建代理对象
        Pojo pojo = (Pojo) factory.getProxy();
        // 在代理对象上调用方法
        pojo.foo();
    }
}

    这里要明白一个关键是客户端代码包含在具有代理引用的Main类的main(..)方法中,这意味着对该对象引用的方法调用是对代理的调用。代理可以委托给与特定方法调用相关的所有拦截器(通知)。

    但是,一旦调用最终的目标对象,对象会调用它的任何方法,而没有通过代理调用,这具有重要意义,这意味着自调用会导致与方法调用关联的通知没有机会运行。例如:foo()方法中会被this引用调用bar()方法,但是没有代理调用,因此bar()方法不会被通知。

    如何处理上述这种情况呢?

    ①重构代码使其不发生自调用,这是最好的、零侵入的

    ②将类中的逻辑绑定到AOP中,这显然增加了耦合度,如下所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // 就像这样,将代码在AOP上下文中使用。。。糟糕透了!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

在创建代理时还需要增加一点配置

public class Main {
    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        // 启用暴露代理
        factory.setExposeProxy(true);
        Pojo pojo = (Pojo) factory.getProxy();
        pojo.foo();
    }
}

最后,必须注意的是,AspectJ没有这种自调用问题,因为它不是基于代理的AOP框架。

②通过@AspectJ自动创建代理

org.springframework.aop.aspectj.annotation.aspectjproxy工厂类为一个或多个@AspectJ切面通知的目标对象创建代理,基本使用如下:

// 创建一个为给定目标对象生成代理的工厂
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// 添加具有@AspectJ注解的切面类,这个方法可以根据多个不同切面进行多次调用
factory.addAspect(SecurityManager.class);
// 添加一个存在的切面实例, 但是所提供的对象的类必须是具有@AspectJ注解的切面
factory.addAspect(usageTracker);
// 获取代理对象
MyInterfaceType proxy = factory.getProxy();

六、在Spring应用中使用AspectJ

在这之前我们讨论的都是使用SpringAOP使用切面,它要求目标对象都必须是被SpringIoC容器所管理的,对于域对象并不起作用,因此,接下来我们使用AspectJ来完成SpringAOP不具备的功能。

使用AspectJ向Spring依赖注入域对象:spring-aspects.jar包中提供了注解驱动切面,可以使SpringIoC容器管理之外的任何对象依赖注入,即Spring应用上下文定义的bean之外的也可以接受切面通知,例:域对象,它们通常是通过new操作符创建,或者由ORM工具作为数据库查询的结果而创建的

如下所示:@Configurable注解标记了一个符合Spring-driven配置的类

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

    当用这种方式标记一个接口时,Spring通过使用与完全限定类型名同名的bean定义(通常是prototype作用域)来配置带注解类型(在本例中是Account)的新实例(com.xyz.myapp.domain.Account),如果要显式指定要使用的原型bean定义的名称,可以直接在注释中指定,Spring会找到一个名为account的bean定义,并使用该定义来配置新的account实例。

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

    可以使用@Configurable的autowire属性来避免指定专用bean定义,像这样@Configurable(autowire=Autowire.BY_TYPE) or @Configurable(autowire=Autowire.BY_NAME)通过类型或者名称自动装配,作为替代方案,最好通过@Autowired或@Inject在字段或方法级别为@Configurable bean指定显式的、注解驱动的依赖项注入。可以使用属性 dependencyCheck 像这样@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) 对新创建和配置的对象引用开启Spring的依赖检查,开启依赖检查后,Spring在配置之后验证所有属性(不是基础类型或集合)是否都已设置。

    单独使用@Configurable注解是不起作用的,还需要spring-aspects.jar中 AnnotationBeanConfigurerAspect对于存在@Configurable注解起作用。本质上,切面所讲述的,“在初始化一个用@Configurable注解的类型的新对象之后,根据注解的属性使用Spring配置新创建的对象”。“这其中,初始化”指的是新实例化的对象(例如,使用new操作符实例化的对象)以及正在进行反序列化的可序列化对象,隐含的表名新对象的依赖关系是在构造方法执行后被注入的,如果需要在构造方法中使用之前注入的依赖项,可以像这样@Configurable(preConstruction = true) 使用@Configurable注解的preConstruction属性。对于AnnotationBeanConfigurerAspect使用的问题,必须使用aspectjweaver织入带注解的类型,如果使用基于Java的配置,则可以将@EnableSpringConfigured添加到任何具有@configuration注解的类中,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {
}

七、其他AspectJ的Spring切面

@Transactional注解是事务处理切面,在使用时必须要将注解添加到类或者方法上,不能是接口,因为接口上的注解不能被继承,并且类上添加这注解后只对公共方法执行默认语义的事务,方法上添加的注解会覆盖类上的注解的默认事务语义,任何可见方法都可以使用此注解,包括私有方法,这是非公共方法执行事务划分的唯一方式。

八、注意事项

    推荐SpringAOP使用@AspectJ的注解形式,而不是XML配置文件

    原因1:因为XML配置文件形式弊端是会使得声明(切面配置)和实现(切面Bean定义)分离(不符合DRY原则),但是注解方式可以将切面封装在一个位置;

    原因2:注解方式可以将多个切点根据要求任意组合,这是XML做不到的。

    注:DRY原则是指系统中的任何知识都应该有一个单一的、明确的、权威的表示。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值