Spring Aop

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

术语

  • Aspect: 切面 跨多个类的关注点的模块化。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。在 Spring AOP 中,方面是通过使用常规类(基于模式的方法)或使用 @Aspect 注释的常规类(@AspectJ 样式)来实现的。

  • Join Point: 连接点 程序执行过程中的点,例如方法的执行或异常的处理。在 Spring AOP 中,连接点始终代表方法执行。

  • Advice:通知:某个方面在特定连接点采取的操作。不同类型的建议包括“周围”、“之前”和“之后”通知。 (通知类型稍后讨论。)许多 AOP 框架(包括 Spring)将建议建模为拦截器,并在连接点周围维护一系列拦截器。

  • Pointcut: 切入点:匹配连接点的谓词。建议与切入点表达式关联,并在与切入点匹配的任何连接点运行(例如,执行具有特定名称的方法)。与切入点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。

  • Target object: 目标对象:由一个或多个方面建议的对象。也称为“建议对象”。由于 Spring AOP 是使用运行时代理实现的,因此该对象始终是代理对象。

  • Aop Proxy: AOP 代理:由 AOP 框架创建的对象,用于实现方面契约(建议方法执行等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。

  • Weaving: 编织:将方面与其他应用程序类型或对象链接起来以创建建议的对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。 Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。

通知

  • Before Advice
    • 之前通知:在连接点之前运行的通知
  • After returning Advice
    • 返回通知:在连接点正常完成后运行的通知(例如,如果方法返回而不引发异常)。
  • After throwing Advice
    • 抛出异常后通知:如果方法因抛出异常而退出,则运行通知。
  • After Advice
    • 之后通知:无论连接点以何种方式退出(正常或异常返回),都要运行的之后。
  • Around Advice(最常用)
    • 环绕通知:环绕连接点(例如方法调用)的通知。这是最有力的通知。环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点还是通过返回自己的返回值或抛出异常来缩短建议的方法执行。

环绕通知是最常用的通知,但是我们还是建议实现所需功能的最弱的通知类。例如,只想输出参数列表,那么最好使用before去处理,当然环绕通知也可以实现。以最优解完成所需功能,出错的可能性也会相对更小。例:不需要使用环绕通知的 JoinPoint 上调用proceed() 方法。

Aop代理

SpringAop 默认使用标准的JDK动态代理作为Aop代理。这使用任何接口都可以被代理。

因为JDK动态代理只支持接口,所以关于类的代理,SpringAop交给了CGLIB

官方说明:如果要代理的目标对象至少实现一个接口,则使用JDK动态代理。目标类型实现的所有接口都被代理。如果目标对象未实现任何接口,则会创建 CGLIB 代理。

如果需要指定CGLIB来进行代理的话

注解方式

@EnableAspectJAutoProxy(proxyTargetClass = true)

xml方式

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

IOC

SpringAop的支持是在IOC之上的,也就是说 SpringAop是依赖IOC的,只有当我们的对象是Bean才能支持Aop的功能

使用

开启Aop

注解方式:@EnableAspectJAutoProxy**标注在Bean对象上

xml形式:<aop:aspectj-autoproxy/>

定义切面

在Bean对象上标注@Aspect使得成为切面。

@Aspect
@Component
public class LogPointcut {}

定义切点

在切面类中定义一个void方法,并使用@Pointcut注解来指示切入点表达式

  • execution(常用):用于匹配方法执行连接点。这是使用 Spring AOP 时使用的主要切入点指示符。
    • 例:* com.mfyuan.aopdemo.model.ExecutionPoint.*()
    • 第一部分*代表修饰符。
    • 第二部分为包+类+方法名 这三个都可以为*,也可以为 User*这样通配的形式
    • 第三部分为 参数列表,
      • ()表示没有参数
      • (..)表示可以有任意参数(零个或多个)
      • (*)只能有一个参数
      • (+)表示至少有一个参数
      • (String,..) 表示第一个必须是String,并且允许有其他任何参数
      • (..,int,..) 表示参数列表中必须拥有一个int参数,并且允许有其他任何参数
  • within:用于匹配指定包或类中的所有方法
    • within(com.mfyuan.aopdemo.model.*) 表示com.mfyuan.aopdemo.model包下的所有类的方法都被定义为切点。
  • args:匹配参数满足指定类型的方法 与execution的第三部分类似,但这个只限定参数。
    • args(String,int) 表示匹配方法中第一个是String,第二个是int的方法
  • target:是一个用于匹配目标对象类型的切点表达式。它通常用于匹配某个接口或类的所有实现类的方法。
    • target(com.mfyuan.aopdemo.service.UserService)表示UserService的实现类或子类的所有方法都是接入点。
  • @annotation(常用): 表示带有匹配带有特定注解的**方法**
    • @annotation(com.mfyuan.aopdemo.annotation.SwitchDataSource) 只有标注了SwitchDataSource这个注解的方法才会定义为切入点。
  • bean: 表示指定某个name的bean为切入点
    • 例:bean(userDestroyServiceImpl) beanname 为userDestroyServiceImpl为切入点
    • 也可以使用 *Service来处理

官网:由于 Spring AOP 框架基于代理的性质,根据定义,目标对象内的调用不会被拦截。对于JDK代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,代理上的公共和受保护方法调用将被拦截(如果需要,甚至包可见的方法)。但是,通过代理进行的常见交互应始终通过公共签名来设计。

简单说明:内部调用的方式,即使是切入点也不会触发通知。

多个切点可以使用 || && ! 来进行组合

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

使用通知

@Before 前置通知

在目前方法前执行

ExecutionPoint类中包含int参数的方法调用 beforeLog方法

@Pointcut("execution(* com.mfyuan.aopdemo.model.ExecutionPoint.*(..,int,..))")
private void executionPoint(){}

@Before(value = "executionPoint()")
public void beforeLog(JoinPoint joinPoint){
    log.info("{} before",joinPoint.getSignature().getName());
}
@AfterReturn 返回通知

当匹配的方法执行正常返回时运行(执行异常时不能执行)

所有方法中带有(String,int)两个参数的方法返回成功后调用returnLog

@Pointcut("args(String,int)")
private void argPoint(){}

@AfterReturning(value = "argPoint()")
public void returnLog(JoinPoint joinPoint){
    log.info("{} return",joinPoint.getSignature().getName());
}

如果需要获取到原方法执行的返回值,可以通过returning属性来帮忙绑定一个代理方法的参数,需名称对应

 @AfterReturning(value = "argPoint()",returning = "returning")
public void returnLog(JoinPoint joinPoint,Object returning){
    log.info("{} return val {}\t return",joinPoint.getSignature().getName(),returning);
}
@AfterThrowing 异常通知

当匹配的方法执行通过引发异常退出时运行

model包下的所有类都如果抛出异常则执行throwingLog

@Pointcut("within(com.mfyuan.aopdemo.model.*)")
private void withinPoint(){}

@AfterThrowing(value = "withinPoint()")
public void throwingLog(JoinPoint joinPoint){
    log.info("{} throwing ",joinPoint.getSignature().getName());
}

如果你希望只有抛出指定类型的异常时才执行的话,可以通过throwing来绑定方法的参数类型,默认是Throwalbe。抛出属性中使用的名称必须与通知方法中的参数名称相对应

只有当匹配的方法出现算术异常时才执行。

@AfterThrowing(value = "withinPoint()",throwing = "ex")
public void throwingLog(JoinPoint joinPoint,ArithmeticException ex){
    log.info("{} throwing ",joinPoint.getSignature().getName());
}
@After 后置通知

在匹配的方法执行退出时允许。无论他是否抛出异常都将执行,类型try{}catch{}finally{}中的finally通常用于释放资源。

model包下的中所有的方法退出后,无论他是否产生异常。都执行afterLog方法

@After(value = "withinPoint()")
public void afterLog(JoinPoint joinPoint){
    log.info("{} after",joinPoint.getSignature().getName());
}

与@AfterRuturn不同的是@AfterRuturn只在正常退出的方法之后调用,而@After则是所有

@Around 环绕通知 (最常用,功能最强大)

他可以在方法之前,之后,返回后,或者抛出异常时,执行。这里的之前,之后等,都是指调用point.proceed()时。

环绕通知的一个参数必须是ProceedingJoinPoint 类型的

@Around(value = "executionPoint()")
public Object around(ProceedingJoinPoint joinPoint){
    log.info("{} around before ",joinPoint.getSignature().getName());
    Object proceed = null;
    try {
        // 不传参数则是默认传入的参数,也可以根据默认参数进行修改后再传入
        proceed = joinPoint.proceed();
        log.info("{} around returning ",joinPoint.getSignature().getName());
    } catch (Throwable e) {
        log.info("{} around throwing ",joinPoint.getSignature().getName());
        throw new RuntimeException(e);
    }finally {
        log.info("{} around finally ",joinPoint.getSignature().getName());
    }
    return proceed;
}
JoinPoint的其他方法
  • getArgs(): 返回方法参数。
  • getThis(): 返回代理对象。也就是通过jdk动态代理。或者cglib代理出来的对象。
  • getTarget(): 返回目标对象。
  • getSignature():返回建议的方法的说明。
  • toString():打印建议的方法的有用说明
参数传递

可以通过args来绑定参数。

匹配切点的第一个参数与通知方法的参数a进行绑定execution是绑定类型,而args可以绑定参数名称

@Pointcut(value = "execution(* com.mfyuan.aopdemo.model.ExecutionPoint.*(..,int,..)) && args(a,..)")
private void executionPoint(String a){}

官方样例,支持泛型,但不支持泛型集合。

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}
注解传递

@annotation 与注解绑定,获取注解的值,

// 切点
@Pointcut("@annotation(com.mfyuan.aopdemo.annotation.SwitchDataSource) && @annotation(name)")
    private void annotationPoint(SwitchDataSource name){
}

// 通知
@Before(value = "annotationPoint(name)")
public void switchDateLog(JoinPoint joinPoint,SwitchDataSource name){
    log.info("{} switchDateSource before",joinPoint.getSignature().getName());
    log.info("SwitchDataSource name {}",name.name());

}
通知顺序

定义了多个同样的通知他的顺序是无序的,按照Ordered.getOrder()越小的越先执行为标准。如果需要让某一个通知比较先执行,可以通过标注@Order注解来实现该需求

自调用
public static void main(String[] args) {
    // 代理了Pojo这个对象
    ProxyFactory factory = new ProxyFactory(new SimplePojo());
    factory.addInterface(Pojo.class);
    factory.addAdvice(new RetryAdvice());
	
    // 获取这个代理对象
    Pojo pojo = (Pojo) factory.getProxy();
    // 这里是代理对象调用的.foo()
    pojo.foo();
}

public class SimplePojo implements Pojo {

    public void foo() {
        // 一旦进入 对象本来,则是对象本来进行调用,而不是代理对象来进行调用。
        this.bar();
    }

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

通常我们使用代理对象去调用方式的时候,我们是对代理对象进行调用,但是一旦到达目标对象本身,也就是自己调用自己的内部方法时,则是通过this对象来进行调用的。

这很关键,这会导致我们一些需要的代理在内部方法调用中失效。

官方给了解决方案,但是不推荐

public class SimplePojo implements Pojo {

    public void foo() {
        // 通过在代理对象内部进行获取当前的代理对象,再通过代理对象去调用内部的其他方法
        ((Pojo) AopContext.currentProxy()).bar();
    }

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

简单示例

记录接口耗时
class MyHttpClient{
    String doGet(String url);
    
    String doPost(String url);
    
    String doPut(String url);
    
    String doDelete(String url);
}
切面+通知
@Aspect
@Commpent
class PointcutAdrive{
    @Pointcut(value = "execution(* *.MyHttpClient.do*(String,..)) && args(url)")
    private void executeTimePoint(String url);
    
    @Around(value = "executeTimePoint(url)"")
    public Object executeTimeAround(ProceedingJoinPoint joinPoint,String url){
        log.info("{} around before ",joinPoint.getSignature().getName());
        Object proceed = null;
        try {
            log.info("{} execute url {}  ",joinPoint.getSignature().getName(),url);
            // 不传参数则是默认传入的参数,也可以根据默认参数进行修改后再传入
            proceed = joinPoint.proceed();
            log.info("{} around returning ",joinPoint.getSignature().getName());
        } catch (Throwable e) {
            log.info("{} around throwing ",joinPoint.getSignature().getName());
            throw new RuntimeException(e);
        }finally {
            log.info("{} around finally ",joinPoint.getSignature().getName());
        }
        long end = System.currentTimeMillis();
        log.info("execute time {}ms",end - start);
        return proceed;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

假女吖☌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值