Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

AspectJ编程学习笔记

10
Apr
2011

AspectJ编程学习笔记

By Alex
/ in Architecture,Java
/ tags AOP, AspectJ, Spring, 学习笔记
1 Comment
AOP基本概念
名词  含义
切面(方面,Aspect) 一个关注点的模块化,这个关注点实现可能横切(crosscutting)多个对象切面的例子包括:事务控制、日志记录、权限控制等在AspectJ中,切面表现为Java类,其源码具有AspectJ的特殊语法增强,使用ajc编译器编译
连接点(Joinpoint) 程序执行过程中明确的点,例如方法的调用开始、结束,或者特定的异常被抛出,横切(crosscutting)在连接点发生
通知(Advice) 在特定的连接点,AOP框架执行的操作
切入点(Pointcut) 切入点是这样的一种程序构造:包含一个连接点的集合、以及收集的连接点的上下文信息(例如方法参数、被执行方法和对象)。通知在这些切入点被触发。AOP框架允许开发者以多种方式定义切入点
引入(Introduction) 添加方法、字段、接口、注解等到被通知的类
目标对象(Target Object) 包含连接点的对象,也被称作被通知或被代理对象
AOP代理(AOP Proxy) AOP框架创建的对象,以目标对象为基础,织入了通知逻辑代理主要包括动态代理、字节码增强两种类型
AspectJ基础知识
  1. AspectJ编译器能识别任何普通的Java代码,可以使用ajc编译.java文件
  2. 织入方式:
    源代码织入 织入器作为编译器的一部分,处理源代码,支持经典语法和注解语法。生成的字节码符合JVM规范,需要使用ajc代替javac
    字节码织入 传递给织入器的是字节码。使用这种方式时,包含编译普通Java类、编译切面,织入3个步骤。
    加载时织入 传递给织入器的是Java类字节码、切面类,以及aop.xml配置文件。
  3. AspectJ暴露的连接点。只能在这些连接点上应用通知:
      暴露的连接点 代码表现
    方法 执行
    Execution
    方法体:该连接点环绕整个方法体的执行过程。这也是Spring AOP唯一支持的接入点类型。通知被织入方法体
    切入点语法:execution(方法签名)
    调用
    Call
    方法调用:该接入点发生在方法被调用时。通知被织入调用者代码处
    切入点语法:call(方法签名)
    构造器 执行
    Execution
    对象构造逻辑:该连接点环绕整个构造器的执行过程
    切入点语法:execution(构造器签名)
    调用
    Call
    对象构造调用
    切入点语法:call(构造器签名)
    字段访问 读访问
    Read access
    读取类或对象的字段
    切入点语法:get(字段签名)
    写访问
    Read access
    写入类或对象的字段
    切入点语法:set(字段签名)
    异常处理 处理器
    Handler
    处理异常的catch块
    切入点语法:handler(类型签名)
    初始化 类初始化
    Class initialization
    类加载,包括静态成员初始化部分
    切入点语法:staticinitialization(类型签名)
    对象初始化
    Object initialization
    在构造器中的对象初始化。包含:从一个父构造器返回,到当前构造器执行完成的部分(不包括调用父构造器的部分)
    切入点语法:initialization(构造器签名)
    Spring的@Configurable即为此例 。下面的例子解释了连接点的范围:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class SavingsAccount extends Account
    {
        private boolean isOverdraft;
        private int     minimumBalance;
        //构造器1
        public SavingsAccount( int accountNumber,
                                     boolean isOverdraft )
        {
            super( accountNumber );
            //对象初始化连接点:构造器1,2
            this.isOverdraft = isOverdraft;
        }
        //构造器2
        public SavingsAccount( int accountNumber )
        {
            //对象初始化连接点:构造器2
            this( accountNumber, false );
            //对象初始化连接点:构造器2
            this.minimumBalance = 25;
        }
    }
    对象预初始化
    pre-initialization
    在构造器中的对象初始化之前:用的很少,从当前构造器调用,到父构造器调用结束为止
    切入点语法:preinitialization(构造器签名)
    通知 执行
    Execution
    通知的执行:环绕某个通知的整个执行。可以对通知进行通知
    切入点语法:adviceexecution()
  4. AspectJ切入点声明语法
    Java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 切入点声明语法,包含5个部分:
    * 访问标识符:例如 public
    * 关键字:pointcut
    * 切入点名称()
    * 切入点类型:例如execution、call,参考:第3点
    * 切入点签名:根据切入点类型不同,可能是类型签名、方法签名、字段签名
    */
    public pointcut secureAccess(): execution(* MessageCommunicator.deliver(..));

    AspectJ支持仅有切入点名称,而没有类型和签名的切入点,通常使用在抽象切面中。
    AspectJ支持匿名切入点,作为通知签名的一部分存在。
    AspectJ支持使用 !、 && 、|| 来作为切入点的运算符

  5. AspectJ通知定义语法:由通知声明、切入点定义、通知体三部分组成
    组成 说明
    整体语法形式:
    Java
    1
    2
    3
    4
    [@AdviceName("通知的名称")]
    [返回值] 通知类型([上下文信息]) [returning|throwing] [throws 通知抛出的异常] : 切入点定义([上下文信息]) {
        通知体
    }
    通知声明 通知类型可以是:before, after 或 around,其中around需要声明返回值通知声明可以指定通知体可用的上下文信息,包括:执行对象通知声明可以指定通知体可能抛出的异常
    切入点定义 位于冒号之后,任何匹配切入点定义的连接点上都会执行本通知
    通知体 可以访问一系列特殊的变量或关键字,例如:proceed、thisJoinPoint、thisJoinPointStaticPart、 thisEnclosingJoinPointStaticPart
  6. AspectJ支持的通知类型
    通知类型 说明
    Before advice 在连接点执行之前执行,如果抛出异常,那么连接点不被执行
    After advice 在连接点执行之后执行,根据连接点的执行结果,具有3个子类型:After (finally):在连接点执行之后执行,不论其结果,通知声明语法:after()After returning:在连接点执行成功后执行,通知声明语法:after() returning(<ReturnType returnObject>)After throwing:在连接点执行失败后执行,通知声明语法:after() throwing(<ExceptionType exceptionObject>)
    Around advice 环绕连接点的执行过程,具有修改连接点执行上下文的能力,可以用来:
    1. 在连接点之前/之后添加额外的逻辑,例如性能分析
    2. 跳过原先逻辑还执行备选的逻辑,例如缓存。只要不调用proceed(),即跳过
    3. 使用try-catch包裹原先逻辑,提供异常处理策略,例如事务管理
AspectJ切入点签名语法详解
通配符

通配符用于匹配一系列的连接点。

 * 匹配点号.范围内的任意数目的字符在类型签名中:表示部分包名或者部分类名在其他地方:表示部分的方法或者字段名
 .. 匹配任意数目的字符,包括任意数量的点号在类型签名中:表示任意直接、间接的子包在方法签名中:表示任意数量的方法参数
 + 作为类型签名的后缀,表示包含任意的子类型
类型签名语法

这里的类型泛指:类、接口、注解、切面、基本类型
基本类型签名示例:

签名 说明
Account 仅匹配Account类型
*Account 匹配任何类名以Account结尾的类型
java.*.Date 匹配java的直接子包中的Date
java..* 匹配java包及其任意子包中的任意类型
javax..*Model+ 匹配java包及其任意子包中的任意以Model结尾的类型,及其子类型

基于注解的类型签名示例:
由于Java不支持注解的继承,所有注解名后不会出现加号。注意注解必须运行时可见( RetentionPolicy.RUNTIME )

签名 说明
@Secured Account 标注了@Secured注解的Account类型
@Sensitive * 标注了@Sensitive注解的任何类型
@Business* Customer+ Customer及其子类型,且标注了Business开头的注解

基于泛型的类型签名示例:

签名 说明
Map<Long,Account> 仅仅匹配Map<Long,Account>类型
*<Account> 任何把Account作为其泛型参数的类型
Collection<@Sensitive *> Collection类型,其泛型参数类必须被标注了@Sensitive
Collection<? extends Account> Collection类型,其泛型参数类必须是Account或者其子类
Collection<? super Account> Collection类型,其泛型参数类必须是Account的父类

联合类型签名示例:

签名 说明
!Collection 除了Collection的任何类型
Collection || Map Collection或者Map类型
java.util.RandomAccess+ && java.util.List+ 任何同时实现了这两个接口的子类,例如ArrayList
!@Secured * 任何没有标注@Secured 的类型
@Secured @Sensitive * 任何同时标注了@Secured @Sensitive的类型
@(Secured || Sensitive) * 任何标注了@Secured @Sensitive之一的类型
方法和构造器签名语法

构造器签名与方法类似,但是:

  1. 由于构造器没有返回值,因此不得在签名中指定返回值
  2. 由于构造器不能为静态,故不得添加static关键字
  3. 由于构造器没有名称,必须使用new作为其名称部分


基本方法签名示例:

签名 说明
public void Account.set*(*) Account类中,包含单个参数、返回类型为void、以set开头的公共方法
public void Account.*() Account类中,任何返回类型为void、没有参数的公共方法
public * Account.*() Account类中,任何没有参数的公共方法
public * Account.*(..) Account类中,包含任意参数(包括0参)的公共方法
* Account.*(..) Account类中,包含任意参数(包括0参)的方法
* *.*(..)* *(..) 任意方法
!public * Account.*(..) 任何Account类的非公共方法
* *(..) throws SQLException 任何声明抛出SQLException的方法
* Account+.*(..) Account及其子类的任何方法
* java.io.Reader.read(char[],..) java.io.Reader类的read方法,第一个参数必须为char[]
* javax..*.add*Listener(EventListener+) javax的任意子包中的类的方法,必须以add开头、Listener结尾,且包含唯一的参数EventListener类型及其子类型
* java.io.PrintStream.printf(String,Object...) java.io.PrintStream的printf方法,第一个参数为字符串,第二个参数为Object...
Account AccountService.*(..) AccountService类中任何返回AccountService的方法
public Account.new() 匹配Account类的0参公共构造器

基于注解的方法签名示例:

注意注解必须运行时可见( RetentionPolicy.RUNTIME )

签名 说明
@Secured * *(..) 任何标注了@Secured的方法
@Secured @Transactional * *(..) 任何同时标注了@Secured、@Transactional 的方法
@(Secured || Transactional) * *(..) 任何标注了@Secured或者@Transactional 的方法
(@Sensitive *) *(..) 任何返回值类型上标注了@Sensitive的方法
* (@BusinessEntity *).*(..) 任何标注了@BusinessEntity的类的任何方法
* *(@RequestParam (*)) 任何包含了单个参数,且参数上标注了@RequestParam 的方法,例如:void show(@RequestParam Long id)
* *(@Sensitive *)* *((@Sensitive *)) 任何包含了单个参数,且参数的类型上标注了@RequestParam 的方法,例如:void create(ClassAnnotatedWithSensitive obj)
* *(@RequestParam(@Sensitive *)) void create(@RequestParam ClassAnnotatedWithSensitive obj)
字段签名语法

字段签名中,使用get表示读字段,set表示写字段:

字段签名示例:

注意注解必须运行时可见( RetentionPolicy.RUNTIME )

签名 说明
private double Account.balance Account类的私有double类型的balance字段
* Account.* Account类的任何字段
* Account+.* Account类及其子类的任何字段
@Sensitive * * 字段上标注了@Sensitive的任何字段
(@Sensitive *) *.* 字段的类型上标注了@Sensitive的任何字段
* (@Sensitive *).* 标注了@Sensitive的类的任何字段
AspectJ切入点的实现

AspectJ提供几种切入点指示器(designators),配合上面所述的切入点签名,即组成切入点。
切入点通过两种方式与连接点进行匹配:

  1. 类型限定的切入点:直接映射到某个类型的连接点,参考AspectJ暴露的连接点一节中的“切入点语法”部分
  2. 无类型的切入点:根据指定的信息,来选择特定的连接点。这些信息可以是:连接点上下文的运行时类型、控制流,或者此法作用域(lexical scope)
类型限定的切入点

匹配:指定类型和签名的连接点
Spring:仅支持execution类型
静态确定:是

无类型的切入点

不去限定连接点的类型,还是通过若干种规则去匹配多种连接点。

Spring虽然仅仅支持execution一种类型的连接点,但是却支持多种方式来选择它们

基于控制流的切入点
这一类的切入点匹配 处于其他切入点的控制流之中 的连接点。这句话比较难以理解,以贷款业务为例,假设Account.debit()方法调用Account.getBalance()进行余额查询,那么,可以说:getBalance在debit的控制流之中。类似的,字段访问、异常处理,也可以处于某个切入点的控制流之中。
Spring不支持这一类切入点,不能静态确定(需要运行时判断)
包含两种切入点类型:cflow、cflowbelow,后者不包括启动控制流的连接点本身

切入点 说明
cflow(execution(* Account.debit(..))) 匹配所有这样的连接点:位于Account的任何debit方法的控制流之中,包括debit方法本身
cflowbelow(execution(* Account.debit(..))) 匹配所有这样的连接点:位于Account的任何debit方法的控制流之中,不包括debit方法本身
cflow(execution(@Transactional * *(..))) 匹配所有位于配置@Transactional方法的控制流之中的任何连接点
cflow(transacted()) 匹配切入点transacted所选择的任何连接点的控制流之中的连接点

下面以事务管理的场景为例,来说明控制流切入点的用法:

Account.java
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cc.gmem.aj.cflow;
public class Account
{
    //两个方法均具有事务注解,但是,我们需要在“最外层”来启动事务
    //需要忽略内层的事务注解
    @Transactional
    public void debit()
    {
        getBalance();
    }
    @Transactional
    public void getBalance()
    {
    }
}

TransactionAspect.aj
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public aspect TransactionAspect
{
    //匹配所有标注了@Transactional的方法执行
    private pointcut transacted() : execution(@Transactional * *(..));
    //匹配这样的@Transactional方法:不在@Transactional方法的所确定的控制流的内部
    private pointcut topLevelTransacted() : transacted() && !cflowbelow(transacted());
 
    Object around() : topLevelTransacted() {
        System.out.println( "启动事务" );
        Object ret = proceed();
        System.out.println( "结束事务" );
        return ret;
    }
}

基于词法结构的切入点
匹配:在指定类型、方法、构造器内的连接点
Spring:支持within(),不支持withincode()
可以静态确定
包含两种切入点类型:within(类型签名)、withincode(构造器签名|方法签名)

切入点  说明
within(Account) Account类,及其嵌套类中的任何连接点
within(Account+) Account类(或者子类),及其嵌套类中的任何连接点
within(@javax.persistence.Entity *) 标注了的@Entity类,及其嵌套类中的任何连接点
withincode(* Account.debit(..)) Account的debit方法中的任何连接点

执行对象切入点
根据运行时对象类型来选择连接点。并且,可以收集执行对象——双重功能。

包含两种类型:this(<Type or ObjectIdentifier>)、target(<Type or ObjectIdentifier>),前者匹配this引用为指定类型的连接点;后者匹配方法调用target为指定类型的连接点。

该类切入点不支持类型通配,也不支持泛型。该类切入点不会匹配任何静态方法的执行连接点。

切入点  说明
this(Account) 任何连接点,只要表达式 this instnaceof Account为真,即匹配
target(Account) 任何(通常是方法调用类型)连接点,只要被调用的方法所属对象 instanceof Account,即匹配

注意区别:
execution(* Account.*(..)) 匹配Account类的任何静态、成员方法
execution(* *.*(..))  && this(Account):匹配Account及其子类的任何成员方法
参数切入点
该类切入点根据连接点的参数对象的运行时类型来匹配。并且,可以收集匹配的参数对象。
Spring支持此类切入点。无法静态确定。
所谓参数对象,根据连接点不同,含义也不同:

  1. 对于方法、构造器连接点,参数对象即方法参数
  2. 对于异常处理连接点,参数对象即被处理的异常
  3. 对于字段写入连接点,参数对象即被写入的值

该类切入点的形式为:args(Type or ObjectIdentifier, ..)

切入点 说明
args(Account, .., int) 任何方法或者构造器,第一个参数的运行时类型为Account或其子类型,最后一个参数的运行时类型为int
args(RemoteException) 任何具有单个RemoteException类型参数的连接点。

基于注解的切入点

注意:注解本身被标注的注解影响AOP行为:

  1. @Retention(RetentionPolicy.RUNTIME):只有设置为运行时保留,才能在运行时进行匹配性判断
  2. @Inherited:只有标注了此注解,子编程元素(类、方法)才能继承父元素上标注的注解

AspectJ支持根据类型、方法、字段所标注的注解来匹配连接点

切入点 说明
@this(TypePattern or ObjectIdentifier) 任何连接点,只要this的类上具有TypePattern指定的注解注意:如果注解是@Inherited的,父类上有指定注解即可
@target(TypePattern or ObjectIdentifier) 任何连接点,只要target的类上具有TypePattern指定的注解
@args(TypePattern or ObjectIdentifier, ..) 任何连接点,只要参数的类上具有TypePattern指定的注解
@within(TypePattern or ObjectIdentifier) 任何连接点,只要在具有TypePattern指定的注解的类型的词法作用域内
@withincode(TypePattern or ObjectIdentifier) 任何连接点,只要在具有TypePattern指定的注解的构造器或方法的词法作用域内
@annotation(TypePattern or ObjectIdentifier) 任何连接点,只要目标上具有指定的注解目标的含义:
  1. 对于方法、构造器、通知执行连接点:目标为对应的程序元素
  2. 对于字段访问、异常处理连接点:目标为被访问的字段或者异常
  3. 对于初始化、预初始化连接点:目标为匹配的起始调用(first-called)构造器
  4. 对于静态初始化连接点:目标为被初始化的类

条件检查切入点
此类切入点在连接点上的一些条件检查。通常和其它切入点联合使用

切入点 说明
if(debug) 任何debug静态字段(位于切面类)设置为true的连接点
if(System.currentTimeMillis() > triggerTime) 任何发生在triggerTime以后的连接点
if(circle.getRadius() < 5) 任何连接点,只要circle的radius小于5。circle必须是被收集的上下文对象,或者切面的静态字段。
AspectJ切面类基本结构

本段代码示意经典的AspectJ语法:

SecurityAspect.aj
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cc.gmem.aj;
 
//切面的声明
public aspect SecurityAspect
{
 
    private Authenticator auth = new Authenticator();
 
    //切入点的声明
    pointcut secureAccess(): execution(* MessageCommunicator.deliver(..));
    //通知声明:针对secureAccess切入点的before通知
    before() : secureAccess() {
        System.out.println( "Checking and authenticating user" );
        auth.doAuthenticate();
    }
}

本段代码示意基于注解的AspectJ语法:
基于注解的语法,可以作为普通Java类编译,并且使用LTW(加载时织入)机制,在类被加载到JVM中时进行织入。

SecurityAspect.java
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cc.gmem.aj;
 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
 
@Aspect
public class SecurityAspect
{
 
    private Authenticator auth = new Authenticator();
 
    @Pointcut ( "execution(* cc.gmem.aj.MessageCommunicator.deliver(..))" )
    public void secureAccess()
    {
    }
 
    @Before ( "secureAccess()" )
    public void secure()
    {
        System.out.println( "Checking and authenticating user" );
        auth .doAuthenticate();
    }
}
AspectJ切面示例

环绕通知的简单例子,注意proceed()的使用:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public aspect SecurityAspect
{
    private Authenticator auth = new Authenticator();
 
    pointcut secureAccess(): execution(* MessageCommunicator.deliver(..));
 
    Object around() : secureAccess() {
        long start = System.nanoTime();
        //伪关键字proceed,用于继续执行被通知的目标方法
        Object ret = proceed();
        //thisJoinPointStaticPart是通知可用变量之一,可以获取连接点的静态信息,例如方法名
        System.out.println( thisJoinPointStaticPart.getSignature() + " took " + ( end - start ) + " nanoseconds" );
        return ret;
    }
}

上下文收集的简单例子:

ConnectionAspect.aj
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cc.gmem.aj;
 
import java.sql.Connection;
import java.sql.SQLException;
 
public aspect ConnectionAspect
{
    //匹配Connection的任何抛出SQLException方法的调用
    //被调用(target)的Connection对象收集为connection变量。
    pointcut connectionOperation( Connection connection )//独立定义的切入点,必须使用这种参数声明来传递上下文信息给通知
        : call(* Connection.*(..) throws SQLException) && target(connection);
 
    Object around( Connection connection ) : connectionOperation(connection) {//传递了上下文
        long startTime = System.nanoTime();
        Object ret = proceed();
        //thisJoinPoint是通知体可用的变量之一
        System.out.println( "Operation " + thisJoinPoint + " on " + connection + " took " + ( System.nanoTime() - startTime ) );
        return ret;
    }
}

上下文收集注解的例子:

PermissionAspect.aj
Java
1
2
3
4
5
6
7
8
9
before(Secured secured ) :
    //匹配任何方法执行
    execution(* *(..))
    //匹配连接点目标————即方法本身————上具有@secured注解的连接点
    && @annotation(secured)
{
    //注解可以像普通对象一样被使用
    checkPermission(secured.permission());
}

 

上下文收集的另外一个例子,注意传递给目标的参数被收集,可被通知体使用:

AccountAspect.aj
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package cc.gmem.aj;
 
public aspect AccountAspect
{
 
    boolean processOverdraft( Account account, float amount )
    {
        return true;
    }
 
    void around( Account account, float amount ) throws InsufficientBalanceException
    //匹配任何对Account.debit(float) throws InsufficientBalanceException方法的调用
        : call(void Account.debit(float) throws InsufficientBalanceException)
        && target(account) && args(amount)//收集的上下文信息作为局部变量使用
    {
        try
        {
            proceed( account, amount );//传递给目标方法的参数,可以任意修改
        }
        catch ( InsufficientBalanceException ex )
        {
            //进行透支处理
            if ( !processOverdraft( account, amount ) )
            {
                throw ex;
            }
        }
    }
}
进行加载时织入(LTW)

在classpath的META-INF/aop.xml中,声明需要织入的切面:

aop.xml
XHTML
1
2
3
4
5
<aspectj>
    <aspects>
        <aspect name="cc.gmem.aj.SecurityAspect"/>
    </aspects>
</aspectj>

通过指定JVM启动参数-javaagent来启用LTW:

Shell
1
-javaagent:%ASPECTJ_HOME%\lib\aspectjweaver.jar

如果使用Spring,可以通过配置,避免修改JVM启动参数

AspectJ横切构造(crosscutting construct)
通用横切构造

AspectJ支持连接点、切入点、切面等构造。

动态横切构造:修改行为

AspectJ的动态横切构造依赖于“通知”实现。通知包括Before、After、Around几种。

收集连接点上下文

连接点上下文包括两种类型:

 连接点牵涉的对象 切入点:this(), target(), args()可以用于收集相应的对象可以使用类型(Type)或者对象名称(ObjectIdentifier)两种方式来收集,对于后者,必须在通知声明处声明相应的对象
连接点关联的注解 切入点:@this(), @target(), @args(), @annotation(),@within(), @withincode()可用于收集相应的注解

通过反射访问连接点上下文

作为备选方式,AspectJ提供了基于反射机制的访问静态(不会随着连接点执行而变化的)、动态连接点上下文信息的方法。这种方式可以完全替换上一条进行动态上下文收集,但是具有以下缺点:

  1. 性能较为低下
  2. 缺少静态语法检查
  3. 比较笨重

通过AspectJ提供的通知体可用的特殊变量来使用这种反射API:

变量 说明
thisJoinPoint 变量类型为JoinPoint。可以访问目标对象、执行对象、参数的信息,包括相应的注解信息。可以通过getStaticPart()方法访问静态信息
thisJoinPointStaticPart 变量类型为JoinPoint.StaticPart。可以访问源码位置、连接点类型、连接点签名。
thisEnclosingJoinPointStaticPart 变量类型为JoinPoint.StaticPart。用于访问包围的连接点静态上下文,所谓“包围的连接点”依连接点类型而不同,例如:
  1. 对于方法调用,包围连接点是caller的执行
  2. 对于异常处理,包围连接点是try-catch块所在方法

两种访问连接点上下文的方式的对照表

切入点语法  反射方式
pointcut pc(Account acc) : this(acc) Account acc = (Account)thisJoinPoint.getThis()
pointcut pc(Account acc): target(acc) Account acc = (Account)thisJoinPoint.getTarget()
pointcut pc(Account acc,Customer cust): args(acc, cust) Object[] arguments = thisJoinPoint.getArgs();
Account acc = (Account)arguments[0];
Customer cust = (Customer)arguments[1];
poincut pc(Secure sec) : @this(annot); Secure sec = thisJoinPoint.getThis().getClass().
getAnnotation(Secure.class)
poincut pc(Secure sec) : @target(sec); Secure sec = thisJoinPoint.getTarget().getClass().
getAnnotation(Secure.class);
pointcut pc(Secure sec, Transactional tx) : @args(sec, tx); Object[] arguments = thisJoinPoint.getArgs();
Secure sec = arguments[0].getClass().
getAnnotations(Secure.class);
Transactional tx = arguments[1].getClass().
getAnnotations(Transactional.class);
poincut pc(Sensitive sens) : @annotation(sec); FieldSignature sig =
(FieldSignature)thisJoinPointStaticPart.getSignature();
Sensitive sens = sig.getField().getAnnotation(Sensitive.class);
pointcut pc(Secure sec) : @within(sec); thisJoinPointStaticPart.getSignature().
getDeclaringType().getAnnotation(Secure.class);
pointcut pc(Secure sec) : @withincode(sec); MethodSignature sig =
(MethodSignature)thisJoinPointStaticPart.getSignature();
Secure sec = sig.getMethod().getAnnotation(Secure.class);
静态横切构造:修改结构

静态横切总是在动态横切之前应用。

静态横切构造由:跨类型声明(inter-type declaration ,ITD,亦称“引入introduction)”)和织入时声明(weave-time declarations)组成
跨类型声明(引入)
可以修改类、接口、或者切面的静态结构,例如增加一个字段,添加一个接口。在一个切面中,声明其他类型的结构,故曰ITD。下面是一个例子:

TrackingAspect.aj
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package cc.gmem.aj;
 
public aspect TrackingAspect
{
 
    //声明MessageCommunicator实现接口AccessTracked,效果和Java标准语法一样
    declare      parents: MessageCommunicator implements AccessTracked;
 
    //声明AccessTracked类型的一个字段,所有它的子类型自动获取此字段
    private long AccessTracked.lastAccessedTime;
 
    //声明AccessTracked类型的两个方法,所有它的子类型自动继承这些方法
    public void AccessTracked.updateLastAccessedTime()
    {
        lastAccessedTime = System.currentTimeMillis();
    }
 
    public long AccessTracked.getLastAccessedTime()
    {
        return lastAccessedTime;
    }
 
    before( AccessTracked accessTracked ) : //声明了一个通知局部变量accessTracked
        execution(* AccessTracked+.*(..)) //AccessTracked子类型的任何方法
        && !execution(* AccessTracked.*(..))//除了AccessTracked本身的方法
        && this(accessTracked)//this对象收集为accessTracked变量
    {
        accessTracked.updateLastAccessedTime();
    }
 
    private static interface AccessTracked
    {
    }
}

成员引入的规则

  1. 只能引入private、public成员,前者只能被切面类访问,后者可以被系统中所有类访问
  2. 如果是private的,多个切面可以引入同名的成员
  3. 切面可以引入字段、函数、构造器到类或者接口。特别的,可以引入方法的实现到接口——缺省实现
  4. 如果切面引入一个和类中同名的方法,那么,类中的那个方法被保留
  5. 成员引入只能针对一个类型

接口缺省实现的例子

Nameable.java
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cc.gmem.aj;
 
public interface Nameable
{
    public void setName( String name );
 
    public String getName();
    //缺省实现
    static aspect Impl
    {
        private String Nameable.name;
 
        public void Nameable.setName( String name )
        {
            this.name = name;
        }
 
        public String Nameable.getName()
        {
            return this.name;
        }
    }
}

使用ITD来实现Java的Mixin机制

Mixin是一种程序构造,允许在已有类中混入特定的逻辑,Java语言没有语言级别的机制,下面的例子示意了如何使用AspectJ进行混入

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//一个标记性接口,用于混入功能
public interface BeanSupport {}
//一个简单的POJO,需要混入:属性变更监听的功能
public class Customer implements BeanSupport
{
    private String address;
    public String getAddress()
    {
        return address;
    }
    public void setAddress( String address )
    {
        this.address = address;
    }
}

通过下面的代码,即可为Customer或者任何实现了BeanSupport的类型混入属性变更的功能,而没有任何的侵入性。

BeanMakerAspect.aj
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public aspect BeanMakerAspect
{
 
    private PropertyChangeSupport BeanSupport.propertyChangeSupport;
    public void BeanSupport.addPropertyChangeListener( PropertyChangeListener listener )
    {
        propertyChangeSupport.addPropertyChangeListener( listener );
    }
    public void BeanSupport.removePropertyChangeListener( PropertyChangeListener listener )
    {
        propertyChangeSupport.removePropertyChangeListener( listener );
    }
    pointcut beanCreation( BeanSupport bean )
    //匹配BeanSupport及其子类型的初始化连接点
    : initialization(BeanSupport+.new(..)) && this(bean);
 
    pointcut beanPropertyChange( BeanSupport bean, Object newValue )
    //匹配BeanSupport及其子类型的任何Setter方法的执行
    : execution(void BeanSupport+.set*(*))
    && args(newValue) && this(bean);
    //BeanSupport初始化成功后,创建一个propertyChangeSupport字段的实例
    after( BeanSupport bean ) returning : beanCreation(bean) {
        bean.propertyChangeSupport = new PropertyChangeSupport( bean );
    }
    //Setter调用后,发布相应的事件
    void around( BeanSupport bean, Object newValue )  : beanPropertyChange(bean, newValue)
    {
        String methodName = thisJoinPointStaticPart.getSignature().getName();
        String propertyName = Introspector.decapitalize( methodName.substring( 3 ) );
        Object oldValue = getPropertyValue( bean, propertyName );
        proceed( bean, newValue );
        bean.propertyChangeSupport.firePropertyChange( propertyName, oldValue, newValue );
    }
}

修改类层次结构

AspectJ提供两种需要类层次的形式:

1
2
3
4
5
6
7
8
declare parents : [TypePattern] implements [InterfaceList];
declare parents : [TypePattern] extends [Class or InterfaceList];
//举例:所有实体类实现BeanSupport接口
public aspect EntityBeanParticipationAspect {
    declare parents: @Entity * implements BeanSupport;
}
//hasMethod、hasField,任何包含指定模式方法或者字段的类:
declare parents: hasmethod(@Observed * *(*)) implements BeanSupport;

引入注解

AspectJ支持为字段、类、方法等添加注解

语法形式 说明
declare @method: <Method signature pattern>:<Annotation>; 引入注解给AccountService的任何方法,例如:declare @method: * AccountService.*(..):
@Transactional(Propagation.Required);
declare @constructor: <Constructor signature pattern>:<Annotation>; 引入注解给构造器,例如:declare @constructor: AccountService+.new():
@ConfigurationOnly;
declare @field: <Field signature pattern>:<Annotation>; 引入注解给字段,例如:declare @field: * MissileInformation.*:
@Classified;
declare @type: <Type signature pattern>:<Annotation>; 引入注解给类型,例如:declare @type: banking..* :
@PrivacyControlled;

简化异常处理

允许不强制捕获受查异常,声明方式如下:

1
2
//这里声明不需要强制捕获的异常
declare soft : <ExceptionTypePattern> : <pointcut>;

织入时声明——警告或者错误信息

可以在织入时,根据一定的模式来生成错误或者警告信息,举例:

Java
1
2
3
4
5
6
//当doAuthenticate方法在SecurityAspect以外调用时,生成警告
declare      warning:
    call(void Authenticator.doAuthenticate()) //切入点:方法被调用的地方,与execution不同
    && !within(SecurityAspect)  : //切入点,不在SecurityAspect内
        
        "Authentication should be performed only by SecurityAspect";
在Spring中使用AspectJ
使用Spring AOP,结合AspectJ风格的切面声明

这种场景下,本质上还是Spring基于JDK动态代理的AOP机制,只是借用AspectJ的语法。

applicationContext.xml
XHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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-2.5.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
    ">
    <!-- 提示Spring自动为目标对象创建代理 -->
    <aop:aspectj-autoproxy />
    <!-- 声明普通Bean -->
    <bean id="messageCommunicator" class="cc.gmem.aj.MessageCommunicator" />
    <!-- 声明切面Bean,注意,必须使用注解语法定义切面 -->
    <bean id="securityAspect" class="cc.gmem.aj.SecurityAspect" />
</beans>
使用spring-aspects切面库

可以基于AspectJ,而不是Spring AOP来实现:事务管理、缓存、任务调度等常用AOP场景,以避免Spring AOP的固有缺陷。
特别是使用@Configurable注解来支持非Spring Bean的依赖注入,必须基于AspectJ完成。

applicationContex.xml
XHTML
1
2
3
4
5
6
7
8
<!-- 支持非受管对象(Spring Bean)的依赖注入和生命周期注解 -->
<context:spring-configured />
<!-- 支持基于AspectJ的事务管理 -->
<tx:annotation-driven transaction-manager="txManager" mode="aspectj" />
<!-- 支持基于AspectJ的缓存机制-->
<cache:annotation-driven cache-manager="cacheManager" mode="aspectj" />
<!-- 支持基于AspectJ的任务调度和执行 -->
<task:annotation-driven  scheduler="taskScheduler" executor="taskExecutor" mode="aspectj" />
← Spring与Quartz的任务调度比较
MySQL知识集锦 →
1 Comment On This Topic
  1. 回复
    NotMeBug
    2021/03/19

    "如果使用Spring,可以通过配置,避免修改JVM启动参数" 怎么弄?原理是什么

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Spring对JMS的支持
  • Spring知识集锦
  • 基于JavaConfig方式的Spring+Hibernate集成
  • Spring对WebSocket的支持
  • Spring与Quartz的任务调度比较

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2