4

官方文档传送门

本文仅涉及从 11. Aspect Oriented Programming with Spring11.2.5 Introductions 之间的内容, 11.2.5 Introductions 之后的讲的是引入的内容,也与本文无关。

本文中所有被使用引用格式的文字均来自于官方文档,可以打开官方文档后在网页内搜索定位,以更好理解上下文。同时本文的内容的顺序并不完全与官方文档一致,对一些内容舍弃不纳入,对一些内容整理和提前了几节。

AOP中的术语的翻译和文档中词汇的理解

词汇理解

type : 可以认为此处使用 class 不够完备,因为还有 interface 之类的也符合当前语句的说明。所以使用 type

AOP基本概念

Advice: action taken by an aspect at a particular join point. Different types of advice include "around", "before" and "after" advice. (Advice types are discussed below.) Many AOP frameworks, including Spring, model an advice as an interceptor, maintaining a chain of interceptors around the join point.

我们约定将 Advice 翻译为增强。抛开引用内容(官方的文档的安排是有问题的。应当先有 Advice 的概念,再有其它的概念。文档中未将 Adive 置于第一个并且对它的解释中使用了其它概念术语,这是不妥当的。),增强作为动词,可以理解为指针对类的方法,非侵犯地(不改动方法本身)为其在被调用之前与被调用之后添加新的功能。作为名词,可以理解为之前说的容纳/实现新的功能的方法。

Aspect: a modularization of a concern that cuts across multiple classes. Transaction management is a good example of a crosscutting concern in enterprise Java applications. In Spring AOP, aspects are implemented using regular classes (the schema-based approach) or regular classes annotated with the @Aspectannotation (the @AspectJ style).

我们约定将 Aspect 翻译为切面。切面是对用于增强的方法按照一定理解和规则进行整理分类后的模块,其中包含了符合这一模块的概念和理解的用于增强的方法。

Join point: a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.

我们约定将 joint point 翻译为接入点。接入点是可被增强的内容,在 Spring AOP 中可以简单地理解为一个类的方法。

Pointcut: a predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.

我们约定将 Pointcut 翻译为切点。切点是一条规则,也是根据这条规则被筛选出来的接入点的集合。

Introduction: declaring additional methods or fields on behalf of a type. Spring AOP allows you to introduce new interfaces (and a corresponding implementation) to any advised object. For example, you could use an introduction to make a bean implement an IsModified interface, to simplify caching. (An introduction is known as an inter-type declaration in the AspectJ community.)

我们约定将 introduction 翻译为引入。引入是指为一个 type 在改动它的源码的基础上为之增加方法的行为。

Target object: object being advised by one or more aspects. Also referred to as the advised object. Since Spring AOP is implemented using runtime proxies, this object will always be a proxied object.

我们约定将 target object 翻译为源对象。源对象指被增强的方法所属的实例。

AOP proxy: an object created by the AOP framework in order to implement the aspect contracts (advise method executions and so on). In the Spring Framework, an AOP proxy will be a JDK dynamic proxy or a CGLIB proxy.

我们约定将 AOP proxy 翻译为代理人。代理人指经由 Spring AOP 指派,代理源对象实现对源对象方法的增强,并将所有调用源对象的被增强的方法的请求都先由自己经手的另一个对象。在Spring AOP中,代理人要么使用JDK动态代理功能创建出来,要么使用CGLIB代理功能创建。

Weaving: linking aspects with other application types or objects to create an advised object. This can be done at compile time (using the AspectJ compiler, for example), load time, or at runtime. Spring AOP, like other pure Java AOP frameworks, performs weaving at runtime.

我们约定将 Weaving 翻译为织入。前面说的代理人经由 Spring AOP 指派,这个指派就是这里的织入。

另外

我们约定,统一使用使用调用一词来描述接入点作为方法被调用/被执行。

增强的类型的术语

  • Before advice: Advice that executes before a join point, but which does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).
  • After returning advice: Advice to be executed after a join point completes normally: for example, if a method returns without throwing an exception.
  • After throwing advice: Advice to be executed if a method exits by throwing an exception.
  • After (finally) advice: Advice to be executed regardless of the means by which a join point exits (normal or exceptional return).
  • Around advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.

从上到下我们分别约定翻译为:前置增强,返回后增强,抛出异常后增强,后置增强,环绕增强。

Spring AOP 被期待实现的能力和目标

Spring AOP currently supports only method execution join points (advising the execution of methods on Spring beans). Field interception is not implemented, although support for field interception could be added without breaking the core Spring AOP APIs. If you need to advise field access and update join points, consider a language such as AspectJ.

....

Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.

Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. CGLIB is used by default if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes; business classes normally will implement one or more business interfaces. It is possible toforce the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface, or where you need to pass a proxied object to a method as a concrete type.

It is important to grasp the fact that Spring AOP is proxy-based. See Section 11.6.1, “Understanding AOP proxies” for a thorough examination of exactly what this implementation detail actually means.

Spring AOP 并不实现对成员对象的赋值,更新(也就是我们说的构造器,this.filed = something )的拦截。仅支持对注册为 Spring bean 的实体类的方法进行增强。

Spring AOP 默认使用 JDK动态代理 来为实现了接口并具有切点(即使具有的切点并不是实现的接口的方法)的类进行增强。而对不实现任何接口的类则默认使用 CGLIB代理。

Thus, for example, the Spring Framework’s AOP functionality is normally used in conjunction with the Spring IoC container. Aspects are configured using normal bean definition syntax (although this allows powerful "autoproxying" capabilities): this is a crucial difference from other AOP implementations. There are some things you cannot do easily or efficiently with Spring AOP, such as advise very fine-grained objects (such as domain objects typically): AspectJ is the best choice in such cases. However, our experience is that Spring AOP provides an excellent solution to most problems in enterprise Java applications that are amenable to AOP.

这是因为 Spring AOP 设计之初,目标就是和 Spring IoC container 一起提供常见的优秀的java商用领域的解决方案。因此对于非常细节处进行增强,不是 Spring AOP 的目标,请使用 AspectJ 代替吧。

Spring AOP 使用的必需条件

In either case you will also need to ensure that AspectJ’s aspectjweaver.jar library is on the classpath of your application (version 1.6.8 or later). This library is available in the 'lib' directory of an AspectJ distribution or via the Maven Central repository.

必需准备好aspectjweaver.jar

关于使用 maven 引入aspectjweaver.jarpom.xml的代码

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>自己写</version>
</dependency>
@AspectJ refers to a style of declaring aspects as regular Java classes annotated with annotations. The @AspectJ style was introduced by the AspectJ project as part of the AspectJ 5 release. Spring interprets the same annotations as AspectJ 5, using a library supplied by AspectJ for pointcut parsing and matching. The AOP runtime is still pure Spring AOP though, and there is no dependency on the AspectJ compiler or weaver.

这一段说明了引入aspectjwearver不代表将底层实现从 JDK动态代理或CGLIB代理 改为了ASPECTJ。

必需通知 Spring 你要使用它。

显然先要有 Spring 框架。我们的官方文档直接定位到了 Spring AOP 这块,其实回到顶部看目录,第一节的标题的就是 Getting Started with Spring ,如果 Spring 环境还没有配好,先按照文档配好。

然后在一个配置类上增加@EnableAspectJAutoProxy注解

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

XML写法文档有,此处略。

声明一个类含有切面

请注意之前我们对切面的理解:切面是对用于增强的方法按照一定理解和规则进行整理分类后的模块,其中包含了符合这一模块的概念和理解的用于增强的方法。而一个类含有切面,不代表它的所有方法均是切面,简单来说,并未用于增强的方法便不包含在该类的切面中。

形如

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

XML写法文档有,此处略。

切面所在的类也可作为源对象

Aspects (classes annotated with @Aspect) may have methods and fields just like any other class. They may also contain pointcut, advice, and introduction (inter-type) declarations.

如开头所说,类是类,切面是切面,不过是这个切面(模块)都在这个类中,不代表这个类就不正常了。

切面不能再被增强

In Spring AOP, it is not possible to have aspects themselves be the target of advice from other aspects. The @Aspect annotation on a class marks it as an aspect, and hence excludes it from auto-proxying.

切面声明不足以让切面被 Spring beans 容器发现

You may register aspect classes as regular beans in your Spring XML configuration, or autodetect them through classpath scanning - just like any other Spring-managed bean. However, note that the @Aspect annotation is not sufficient for autodetection in the classpath: For that purpose, you need to add a separate @Component annotation (or alternatively a custom stereotype annotation that qualifies, as per the rules of Spring’s component scanner).

公用切点规则表达式 @Pointcut 一次定义,处处引用。

When working with enterprise applications, you often want to refer to modules of the application and particular sets of operations from within several aspects. We recommend defining a "SystemArchitecture" aspect that captures common pointcut expressions for this purpose. A typical such aspect would look as follows:

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.someapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.someapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}

    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.someapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
     * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

对于一个被多个增强应用的切点规则,可以将之写入@Pointcut注解内,而这个注解则注解在一个方法上。之后向要应用该切点规则,只需在增强的注解的内容中写被该@Pointcut注解的方法的全限定名,而不必反复写切点规则。

例子(本例中com.xyz.myapp.SystemArchitecture.dataAccessOperation()便是上面那段代码里的(最后那个)的切点规则):

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

当然,直接写也没什么问题。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

}

切点规则表达式(value属性的值)

接下来的内容一般是写在@Pointcut这个定义切点规则和@Before`@After等定义增强时机的注解的括号中的。因为只有它(切点规则表达式),所以就被默认赋给了注解中的value属性。实际上,还有argNames属性可用。如果说这两个都使用,那就必须明确指定value = "起点规则表达式"`。

这里先谈切点规则表达式。

切点规则中辅助用的限定符

以下限定符的格式省略了它们是被包在@Pointcut()切点声明注解或者@Before@Around等标识增强时机的注解的括号之中的。

execution 限定符:接入点(方法)的全路径,返回类型和权限符限定

execution - for matching method execution join points, this is the primary pointcut designator you will use when working with Spring AOP

Spring AOP users are likely to use the execution pointcut designator the most often. The format of an execution expression is:

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

All parts except the returning type pattern (ret-type-pattern in the snippet above), name pattern, and parameters pattern are optional. The returning type pattern determines what the return type of the method must be in order for a join point to be matched. Most frequently you will use * as the returning type pattern, which matches any return type. A fully-qualified type name will match only when the method returns the given type. The name pattern matches the method name. You can use the * wildcard as all or part of a name pattern. If specifying a declaring type pattern then include a trailing . to join it to the name pattern component. The parameters pattern is slightly more complex: () matches a method that takes no parameters, whereas (..) matches any number of parameters (zero or more). The pattern (*)matches a method taking one parameter of any type, (*,String) matches a method taking two parameters, the first can be of any type, the second must be a String. Consult the Language Semantics section of the AspectJ Programming Guide for more information.

  • modifiers-pattern

    • 权限修饰符,如publicprotectprivate
    • 可以省略,代表没有限制。
  • ret-type-pattern

    • 方法(接入点/切点是一个个方法)返回值的 type
    • 不可省略 但可以用*表示所有 type 都行的意思。
    • 一旦指定必须使用全限定名,且接入点返回的值的 type 为要求 type 的子类或实现类时亦不算符合。
  • declaring-type-pattern

    • 方法所属的 type 的全限定名
    • 可以省略,代表没有限制。
    • 如果不省略,则与 name-pattern 间用一个.连接。
    • 没有*可以作为通配符使用。要么不写,要么写全
  • name-pattern

    • 方法自己的名字
    • 不可以省略。
    • 也可以用*作为通配符,比如set*表示以set开头,后接零个多个其它字符的方法名的接入点。
  • param-pattern

    • 方法的参数表
    • 不可以省略。
    • ()表示匹配不接收任何传参的接入点。
    • (..)表示零个至多个参数都可以,即完全不在这件事上有限制。
    • 可以用*代表一个参数,但不限制该参数的 type。例如(*,String) 要求切入点接受两个参数,第一个参数的 type 没有限制,第二个参数必须是 String
    • 在指定一个 type 的情况下,应当使用 type 的全限定名。匹配时只会认准这个 type ,对于该 type 的子类或实现类,不认为匹配。
  • throws-pattern

    • 可以省略,代表没有限制。
    • 方法可能抛出的异常。

一些例子

  • the execution of any public method:
execution(public * *(..))
  • the execution of any method with a name beginning with "set":
execution(* set*(..))
  • the execution of any method defined by the AccountService interface:
execution(* com.xyz.service.AccountService.*(..))
  • the execution of any method defined in the service package:
execution(* com.xyz.service.*.*(..))
  • the execution of any method defined in the service package or a sub-package:
execution(* com.xyz.service..*.*(..))

within 限定符:限定到某个 type

within - limits matching to join points within certain types (simply the execution of a method declared within a matching type when using Spring AOP)

在某一 type 内部的所有接入点。之前在词汇理解时就已经探讨过, type 用于写 class 感觉并不完善的情况下。这里就是一个例子,如果要完善的话,显然还要说接口,说包(package)才完善(当然我也不确定这样是否完善了)。

一些例子

  • any join point (method execution only in Spring AOP) within the service package:
within(com.xyz.service.*)
  • any join point (method execution only in Spring AOP) within the service package or a sub-package:
within(com.xyz.service..*)

this限定符和target限定符:限定调用接入点的实例是指定 type 的实例

this - limits matching to join points (the execution of methods when using Spring AOP) where the bean reference (Spring AOP proxy) is an instance of the given type

target - limits matching to join points (the execution of methods when using Spring AOP) where the target object (application object being proxied) is an instance of the given type

这两个限定符我网上也找了不少资料,但是都没有举具体的实例,让人十分困惑。所以对于两者的区别作为疑问留存,无法给出。两者的共性效果就是限制调用接入点的实例是被指定的 type (类或者接口)的实例

举例说明;

在一个基础的引入了AOP的 Spring Boot 应用中

@SpringBootApplication
@EnableAspectJAutoProxy
public class LogbackandaopApplication {

    public static void main(String[] args) {
        SpringApplication.run(LogbackandaopApplication.class, args);
        
        //其中DemoA实现了IDemo接口,而DemoB未实现任何接口
        DemoA demoA = (DemoA) ApplicationContextProvider.getBean("demoA");
        DemoB demoB = (DemoB) ApplicationContextProvider.getBean("demoB");
        demoA.IDemoTest();
        demoB.IDemoTest();
    }
}

其中:

package xyz.d613.logbackandaop.demo;

public interface IDemo {
    void IDemoTest();
}
@Component
public class DemoA implements IDemo {
    @Override
    public void IDemoTest() {
        System.out.println("A");
    }
}
/*
注意!DemoB类未实现任何接口!
*/
@Component
public class DemoB {

    public void IDemoTest() {
        System.out.println("B");
    }
}

定义切面为

@Component
@Aspect
public class MyAspect {
    @Before("execution(* *.IDemoTest(..)) && this(...logbackandaop.demo.IDemo)")
    public void advice() throws Throwable {
        System.out.println("MyAspect advice");
    }
}

运行此应用,得到控制台上的输出:

MyAspect advice
A
B

可以发现A被前置增强了,而B没有。这就是this(...logbackandaop.demo.IDemo)在起作用。尽管B拥有满足execution(* *.IDemoTest(..))的方法,但它不是IDemo接口的实例。故被排除。

args 限定符:限定接入点中的参数表(按给出顺序)的各个参数是指定 type 或它的子/实现类

args - limits matching to join points (the execution of methods when using Spring AOP) where the arguments are instances of the given types

限定接入点为接入点被调用时传入的实参(按照顺序地)都是args限定符中声明的 type 的实例。

Note that the pointcut given in this example is different to execution(* *(java.io.Serializable)): the args version matches if the argument passed at runtime is Serializable, the execution version matches if the method signature declares a single parameter of type Serializable.

假设有如下的类型

public interface A;
public class B implements A;

那么切点@Pointcut(execution(* *(A))不会对public anytype method(B);生效。但@Pointcut(args(A))则会对public anytype method(B);拦截并进行增强。这就是所谓 the argument passed at runtime

利用args限定符使增强获得切点的参数

只限定头部几个参数,后面的不关心该怎么写,跳转过去后注意看加粗部分与上下文。

@target限定符 @within限定符:限定接入点为调用者必须指定的注解的接入点

两者的区别留作疑问。

@target - limits matching to join points (the execution of methods when using Spring AOP) where the class of the executing object has an annotation of the given type

  • any join point (method execution only in Spring AOP) where the target object has an @Transactional annotation:
@target(org.springframework.transaction.annotation.Transactional)

@within - limits matching to join points within types that have the given annotation (the execution of methods declared in types with the given annotation when using Spring AOP)

  • any join point (method execution only in Spring AOP) where the declared type of the target object has an @Transactional annotation:
@within(org.springframework.transaction.annotation.Transactional)

限制接入点被调用时,调用它的实例所属的类必须带有指定的注解。在上述例子中为@Transactional

@args限定符:限定被传入的实参所属的类(按指定的顺序地)有被要求的注解

@args - limits matching to join points (the execution of methods when using Spring AOP) where the runtime type of the actual arguments passed have annotations of the given type(s)

  • any join point (method execution only in Spring AOP) which takes a single parameter, and where the runtime type of the argument passed has the @Classifiedannotation:
@args(com.xyz.security.Classified)

限定接入点参数的数量的同时,对应自己指定的顺序,被传入的实参的所属的类有被@args限定符指定的注解。

@annotation限定符:限定接入点自身有被限定符指定的注解

@annotation - limits matching to join points where the subject of the join point (method being executed in Spring AOP) has the given annotation

  • any join point (method execution only in Spring AOP) where the executing method has an @Transactional annotation:
@annotation(org.springframework.transaction.annotation.Transactional)

接入点(指方法本身而不是其所在的类)要带有@annotation限定符指定的注解。

不知为何对java.lang.Override注解无效。

bean限定符:限定接入点是被限定符指定的 bean 中的接入点

Spring AOP also supports an additional PCD named bean. This PCD allows you to limit the matching of join points to a particular named Spring bean, or to a set of named Spring beans (when using wildcards). The bean PCD has the following form:

bean(idOrNameOfBean)

The idOrNameOfBean token can be the name of any Spring bean: limited wildcard support using the * character is provided, so if you establish some naming conventions for your Spring beans you can quite easily write a bean PCD expression to pick them out. As is the case with other pointcut designators, the bean PCD can be &&'ed, ||'ed, and ! (negated) too.

可以使用通配符以一次性指定多个 bean

使用 Bean 这个术语,就意味着其必须是被 Spring IOC 所管理的类,即带有@Component@Controller之类的注解或通过配置交由Spring的Bean容器管理。

argNames属性

上一节开篇已经说过

接下来的内容一般是写在@Pointcut这个定义切点规则和@Before`@After等定义增强时机的注解的括号中的。因为只有它(切点规则表达式),所以就被默认赋给了注解中的value属性。实际上,还有argNames属性可用。如果说这两个都使用,那就必须明确指定value = "起点规则表达式"`。

这一节关于另一个属性:argNames

The parameter binding in advice invocations relies on matching names used in pointcut expressions to declared parameter names in (advice and pointcut) method signatures. Parameter names are not available through Java reflection, so Spring AOP uses the following strategies to determine parameter names:

这里用我的例子。

假设有这么一个类有这么一个方法想要被增强:

package root.demo
    
public class Demo {
    public void printf(String info){
        System.out.println(info);
    }
}

确定切点切到这个接入点身上并不麻烦,以下两种写法都行:

package root.aspects;

@Aspect
public class MyAspect {

    @Before("execution(* root.demo.Demo.printf(java.lang.String))")
    public void advice(){
    }
    
    //另一种写法
    @Before("execution(* root.demo.Demo.printf(..) && args(java.lang.String)")
    public void advice(){
    }
}

然而问题在于,我想要获得切点中的那个被传入的String类型的参数,怎么办?这样写么?

    @Before("execution(* root.demo.Demo.printf(java.lang.String))")
    public void advice(String s){
    }

这里的s是不会得到任何值的。

那么要怎么做呢,官方文档的意思,这么写:

    @Before(value = "execution(* xyz.d613.logbackandaop.demo.Demo.printf(..)) && args(s))",
    argNames = "s")
    public void advice(String s){
        System.out.println(s);
    }

请注意,此处args(s)中,虽然args仍是限定符,但其内部的值显然不可能是一个 type 了。在使用argNames的使用,原来限定符的用法发生了改变。写在value中的各个限定符(排除execution)中的值总是要和argNames属性中的值存在一一对应关系,而argNames中的值又于增强的函数签名的参数表中的参数一一对应,如这里的argNames="s"中的spublic void advice(String s)中的String s中的s对应。

这样建立起联系后,传入切点中的参数就能被传入增强中并交给指定增强的方法签名中的指定的参数。同时 Spring AOP 也会反过来(由于String s的这个String)意识到相当于有一个args(java.lang.String)的限定符。

If the first parameter is of the JoinPoint, ProceedingJoinPoint, or JoinPoint.StaticPart type, you may leave out the name of the parameter from the value of the "argNames" attribute. For example, if you modify the preceding advice to receive the join point object, the "argNames" attribute need not include it:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

The special treatment given to the first parameter of the JoinPoint, ProceedingJoinPoint, and JoinPoint.StaticPart types is particularly convenient for advice that do not collect any other join point context. In such situations, you may simply omit the "argNames" attribute. For example, the following advice need not declare the "argNames" attribute:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

这部分在说JointPoint接口以及它的子接口,实现类,在作为增强的参数表中的一员时,不需要额外准备,只要写好(JointPoint jp...)(其中JointPoint可用特定实现类或子接口换掉,具体看其它部分)即可。

关于这部分知识更多细节,见获知被增强的切点的相关信息

增强的执行顺序

@Before 前置增强

Before advice

Before advice is declared in an aspect using the @Before annotation:、

@AfterReturnning 返回后增强

After returning advice

After returning advice runs when a matched method execution returns normally. It is declared using the @AfterReturning annotation:

获取返回后增强的返回值该怎么写

Sometimes you need access in the advice body to the actual value that was returned. You can use the form of @AfterReturning that binds the return value for this:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

The name used in the returning attribute must correspond to the name of a parameter in the advice method. When a method execution returns, the return value will be passed to the advice method as the corresponding argument value. A returning clause also restricts matching to only those method executions that return a value of the specified type ( Object in this case, which will match any return value).

Please note that it is not possible to return a totally different reference when using after-returning advice.

@AfterThrowing 抛出异常后增强

After throwing advice

After throwing advice runs when a matched method execution exits by throwing an exception. It is declared using the @AfterThrowing annotation:

限制增强对应的异常类型/获取抛出的异常

Often you want the advice to run only when exceptions of a given type are thrown, and you also often need access to the thrown exception in the advice body. Use thethrowing attribute to both restrict matching (if you don't need restrict the exception type, use Throwable as the exception type otherwise) and bind the thrown exception to an advice parameter.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

The name used in the throwing attribute must correspond to the name of a parameter in the advice method. When a method execution exits by throwing an exception, the exception will be passed to the advice method as the corresponding argument value. A throwing clause also restricts matching to only those method executions that throw an exception of the specified type ( DataAccessException in this case).

@After 后置增强

After (finally) advice

After (finally) advice runs however a matched method execution exits. It is declared using the @After annotation. After advice must be prepared to handle both normal and exception return conditions. It is typically used for releasing resources, etc.

从文档中的括号也可以看出来,后置增强相当于对应try{}catch(){}finally{}中的finally增强。对于抛出异常导致的调用结束或正常返回的调用结束,它均能处理。它经常用来释放资源。

@Around 环绕增强

Around advice

The final kind of advice is around advice. Around advice runs "around" a matched method execution. It has the opportunity to do work both before and after the method executes, and to determine when, how, and even if, the method actually gets to execute at all. Around advice is often used if you need to share state before and after a method execution in a thread-safe manner (starting and stopping a timer for example). Always use the least powerful form of advice that meets your requirements (i.e. don’t use around advice if simple before advice would do).

环绕增强能在调用前增强,也能在调用后增强,更能同时实现两者,还能经过它决定被调用的方法到底会不会真的被调用。

如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用环绕增强。

在可以不用环绕增强的情景下,尽量不要使用环绕增强,否则会带来额外的开支。

使用环绕增强决定切点是否被真的调用

Around advice is declared using the @Around annotation. The first parameter of the advice method must be of type ProceedingJoinPoint. Within the body of the advice, calling proceed() on the ProceedingJoinPoint causes the underlying method to execute. The proceed method may also be called passing in an Object[] - the values in the array will be used as the arguments to the method execution when it proceeds.

增强的参数声明的第一个必须是ProceddingJointPoint的实例或其子类的实例(不过它自己其实是org.aspectj.lang.JoinPoint的实现类)。在增强的方法体中使用ProceddingJointPoint#proceed(Object ...)方法,才会使切点被真的调用。如果不使用此方法,则切点不会被真正调用。

ProceddingJointPoint#proceed(Object ...)的返回值就是切点的返回值,实际传入的参数(对应Object ...则应该是切点所需要的参数。

同一时机多个增强冲突时的执行策略

What happens when multiple pieces of advice all want to run at the same join point? Spring AOP follows the same precedence rules as AspectJ to determine the order of advice execution. The highest precedence advice runs first "on the way in" (so given two pieces of before advice, the one with highest precedence runs first). "On the way out" from a join point, the highest precedence advice runs last (so given two pieces of after advice, the one with the highest precedence will run second).

高优先级的增强在切点被调用前的时机的增强执行顺序中更靠前,在切点被调用后的时机的增强执行顺序中更靠后。

When two pieces of advice defined in different aspects both need to run at the same join point, unless you specify otherwise the order of execution is undefined. You can control the order of execution by specifying precedence. This is done in the normal Spring way by either implementing the org.springframework.core.Orderedinterface in the aspect class or annotating it with the Order annotation. Given two aspects, the aspect returning the lower value from Ordered.getValue() (or the annotation value) has the higher precedence.

When two pieces of advice defined in the same aspect both need to run at the same join point, the ordering is undefined (since there is no way to retrieve the declaration order via reflection for javac-compiled classes). Consider collapsing such advice methods into one advice method per join point in each aspect class, or refactor the pieces of advice into separate aspect classes - which can be ordered at the aspect level.

当两个相同优先级的增强在同一个切点的同一个时机时,它们的执行顺序顺序无法确定。如果这两个增强所在的切面不同,但你可以使用@Order注解(或实现org.springframework.core.OrderedgetValue()方法)在切面上来确定两个增强(其实是切面)的优先级。@Order的值更小的那个优先级更高。

但如果是在同一个切面,那就没法子了。而且既然是同一个切面,那么请你好好整理一下代码,避免这种情况出现吧,因为这样可能引起很糟糕的后果。


实话实说我惊呆了,到这里居然就结束了,那么五种增强冲突时怎么整?比如前置增强和环绕增强在调用前这一时机,谁先谁后,官方文档不告知的?秀啊!

经过网上资料的汇总和自己的实验,可以认为有以下结论:

  1. 前面所谓的优先级是同种类增强之间才用得上的,不同种类间的增强的执行顺序不能被调控。
  2. 在调用前这个时机上,环绕增强先于前置增强。可以这么理解——环绕增强有阻止切点被真实调用的能力,并且是默认阻止的。所以环绕增强必须要先于一切拿到切点。
  3. 后置增强后于环绕增强在调用后的增强。在调用后这个时机上,返回后增强和抛出异常后增强属于两个完全不同的时机(一个函数要么抛出异常结束,要么正常返回结束,不可能两者同时发生),一般不会冲突。而两者都后于后置增强。

获知被增强的切点的相关信息

获取切点:接口org.aspectj.lang.JoinPoint

Any advice method may declare as its first parameter, a parameter of type org.aspectj.lang.JoinPoint (please note that around advice is required to declare a first parameter of type ProceedingJoinPoint, which is a subclass of JoinPoint. The JoinPoint interface provides a number of useful methods such as getArgs()(returns the method arguments), getThis() (returns the proxy object), getTarget() (returns the target object), getSignature() (returns a description of the method that is being advised) and toString() (prints a useful description of the method being advised). Please do consult the javadocs for full details.

If the first parameter is of the JoinPoint, ProceedingJoinPoint, or JoinPoint.StaticPart type, you may leave out the name of the parameter from the value of the "argNames" attribute. For example, if you modify the preceding advice to receive the join point object, the "argNames" attribute need not include it:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

The special treatment given to the first parameter of the JoinPoint, ProceedingJoinPoint, and JoinPoint.StaticPart types is particularly convenient for advice that do not collect any other join point context. In such situations, you may simply omit the "argNames" attribute. For example, the following advice need not declare the "argNames" attribute:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

以上两段引用在官方文档中亦不在同一个地方,但整理认为它们所表达的意思只有一个:

任何增强的参数声明都可以不做额外准备地将第一个参数声明为(JointPoint jp),比如

@Aspect
public class MyAspect {
    @Before("execution(* *.IDemoTest(..))")
    public void advice(org.aspectj.lang.JoinPoint jp) throws Throwable {
        System.out.println("MyAspect advice");
        System.out.println(jp);
        System.out.println(jp.getSignature());
    }
}

Spring AOP 会自动地将切点,增强的相关信息封装为接口JointPoint的实例(如前面的环绕增强所用的,通常是一个ProceedingJointPoint,所以一般写参数的时候也是写ProceedingJointPoint而不是直接写这个接口)。同时接口JointPoint也提供了一些用于调取出被封装的信息的方法,这个实例必然会将之实现。那么具体有哪些信息以及怎么使用,请见官方文档

不过较为常用的方法其实文档(上面摘抄过来的引用)中已经给出了。

获取切点的参数

第一种方法就是使用上面一节,先获取切点相关信息,即JoinPoint得实例,然后通过JoinPoint#getArgs()方法获得切点的参数们,一个Object[]数组。

第二种方法,文档介绍的使用args限定符。

We’ve already seen how to bind the returned value or exception value (using after returning and after throwing advice). To make argument values available to the advice body, you can use the binding form of args. If a parameter name is used in place of a type name in an args expression, then the value of the corresponding argument will be passed as the parameter value when the advice is invoked. An example should make this clearer. Suppose you want to advise the execution of dao operations that take an Account object as the first parameter, and you need access to the account in the advice body. You could write the following:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

The args(account,..) part of the pointcut expression serves two purposes: firstly, it restricts matching to only those method executions where the method takes at least one parameter, and the argument passed to that parameter is an instance of Account; secondly, it makes the actual Account object available to the advice via the account parameter.

例子的@Before注解的前半段,是在引用com.xyz.myapp.SystemArchitecture.dataAccessOperation()上的@Pointcut注解所定义的切点规则。后半段则是再在前半段筛出来的切点中筛出第一个参数为Account之后有什么参数,有没有参数不限制的接入点。这里为什么第一个参数为Account,不是因为args(account,..)中写了 account, 然后 Spring 会自动把首字母大写,然后明白过来是 Account 类。而是因为这个accountpublic void validateAccount(Account account)中的account对应,于是 Spring 从 Account account 中得知args(account,..)中的account要求的是一个Account的实例。实际上将account改为dfaljlkdj,只要增强的参数表也跟着改为public void validateAccount(Account dfaljlkdj),那么一切就能正常工作。

上文说到,@Before注解的前半段是引用一个切点规则。那么能否直接让那个切点把后半段的事情也做了,我就一个引用完事呢?答案是可以,写法如下:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

仿照获取切点的参数,获取切点的注解,代理对象,源对象...

The proxy object ( this), target object ( target), and annotations ( @within, @target, @annotation, @args) can all be bound in a similar fashion. The following example shows how you could match the execution of methods annotated with an @Auditable annotation, and extract the audit code.

First the definition of the @Auditable annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

And then the advice that matches the execution of @Auditable methods:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

切点中有泛型

Spring AOP can handle generics used in class declarations and method parameters. Suppose you have a generic type like this:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

You can restrict interception of method types to certain parameter types by simply typing the advice parameter to the parameter type you want to intercept the method for:

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

首先,一个切点有泛型,那么它(切点是一个个方法)所属的 type 必然能接收泛型(如引用中的第一段代码),所以在 type 与方法之间的点前添加一个加号:Sample+.sampleGenericMethod(*),表明这个 type 要接收泛型。

令人遗憾的是,这个MyType并没有什么值得称道的功能——我还以为这个对应泛型里的T,所以我可以通过param.getClass()来获得被增强的切点的泛型的具体类型。但实际上这个MyType就是表示这个地方由你来填,没有什么功能。如果相不对泛型的类型做限制——实际上只不过是不对切点的第一个参数,即param的类型做限制,只不过在例子中paramT param,看起来就像是对泛型有限制一样——就填入Object

So you cannot define a pointcut like this:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

To make this work we would have to inspect every element of the collection, which is not reasonable as we also cannot decide how to treat null values in general. To achieve something similar to this you have to type the parameter to Collection and manually check the type of the elements.

不能使用Collection及其实现类和子接口加泛型来筛选参数的类型。


阳光号
129 声望5 粉丝