AspectJ切面编程的配置与语法详解

AspectJ是JAVA面向切面编程AOP的软件包,用来解决日志记录、性能监测、权限认证、访问控制等非核心逻辑通用功能模块的复用。

  • 目录结构
  1. XML文件结构
  2. AspectJ组件
  3. 切入点表达式
  4. 通知参数获取
  5. 演示项目代码
  • 知识结构图解


    AspectJ知识点2.png

1 XML配置文件结构

与Spring框架的bean定义XML文件一样,可以定义BEAN对象或切面配置,切面配置节点,BEAN对象节点,这里重点研究切面配置节点。
AspectJ重要概念:切面(aspect)、切入点(pointcut)、通知(before、after、afterReturning、afterThrowing、around)等。


     # 配置aspectj自动代理,可选,一般与aop:config节点二选一
     #切面配置
         # 配置切入点,可选,0个或多个
         # 配置只有一个切入点和一个通知的切面,可选
         # 配置1个切面,可选,0个或多个
             # 配置切入点,可选,0个或多个
             # 前置通知,可选,0个或多个
             # 后置通知,可选,0个或多个
             # 方法返回后通知,可选,0个或多个
             # 方法抛出异常后通知,可选,0个或多个
        
    

     #定义BEAN对象,可选,0个或多个
         # 构造函数参数,可选,0个或多个
         # 定义对象属性,可选,0个或多个
    

切面(Aspect),切面由切入点和通知组成,一个配置文件可以定义若干个切面。

切入点(Pointcut),一个切面定义0或多个切入点。
通知(Advice),一个切面定义1个或多个通知,常用通知类型,before-前置通知,after-后置通知,around-环绕通知,afterReturning-方法返回后通知,afterThrowing-异常抛出后通知。


2 AspectJ组件

切面、切入点与通知,都支持XML或注解方式配置。

2.1 切面-Aspect

切面对应XML节点,对应注解@Aspect。个人理解,切面设置对象是实现某个功能模块的类,如日志记录、权限验证等。

  • 切面接口
public @interface Aspect{
    String value() default ""
}

通过代码,可以看到@Aspect注解有一个属性value,可以保存类似XML节点的id等内容。注解范例如下:

@Aspect(value="mylog")
public class Logging{
    ...
}
  • XML范例

    

XML方式有id与ref两个属性,ref属性需要指定被引用的BEAN对象,也就是通知方法所在的BEAN对象的id。例如,自己实现一个记录日志的切面对象,bean对象的id为logging,这里的引用属性设置为logging。
注意:后续在该切面内定义的所有通知,method属性是被引用对象的方法名称

2.2 切入点-Pointcut

2.2.1 命名方式

根据切入点是否有名称分成匿名切入点与命名切入点。区别:命名方式支持在其他位置引用,匿名方式不可以。

2.2.1.1 命名切入点

  • XML配置

    
    
    
        
    

的id属性赋值,值可以根据程序需求与环境自定义,在通知定义中引用,上述例子id赋值为setValue,在后续通知中用pointcut-ref中引用。

  • 注解配置
@Aspect
public class Logging {
    @Pointcut(value="execution(public * com.toipr..SimpleCalculator.*(int, int))")
    private void calculator(){
    }

    @Before(value="calculator()")
    public void beforeAdvice(JoinPoint jp){
        Object target = jp.getTarget();
        System.out.println("target object class is " + target.getClass().getName());

        Object[] args = jp.getArgs();
        if(args!=null && args.length>0){
            System.out.println("going to setup profile. name=" + args[0].toString());
        }
    }
}

上述例子,申明私有方法calculator(用@Pointcut注解),这个就是切入点的命名,在前置通知@Before注解中引用(@Before(value="calculator()")),更复杂的命名与引用形式后续总结。

2.2.1.2 匿名切入点

public class Logging{
    @Before(value="execution(public * com.toipr..UserInfo.set*(*)))")
    public void beforeAdvice(JoinPoint jp){
        Object target = jp.getTarget();
        System.out.println("target object : " + target.toString());
    }
}

上述例子,直接在通知方法beforeAdvice上使用注解@Pointcut申明切入点,属于匿名切入点,在其他通知中无法复用。

2.2.2 配置方式

支持XML或注解两种配置方式。

2.2.2.1 XML配置

节点用来定义一个切入点,XML节点有两个属性,id为切入点唯一标识,expression为切入点表达式。切入点XML节点示例:



2.2.2.2 注解配置

@Pointcut,包含两个属性value与argNames,value为切入点表达式,argNames为参数。

  • 切入点注解接口
public @interface Pointcut{
    /* 返回切入点表达式, 如: execution=(public * com.toipr..UserInfo.set(*)) */
    String value() default ""

    /* 返回切入点参数,多个参数用半角逗号隔开,例如:a,b */
    String argNames() default ""
}
  • 注解方式示例代码
public class Logging{
    @Before(value="execution(public * com.toipr..UserInfo.set*(*)))")
    public void beforeAdvice(JoinPoint jp){
        Object target = jp.getTarget();
        System.out.println("target object : " + target.toString());
    }
}

2.3 通知-Advice

典型的切面通知有before-前置通知、after-后置通知、afterReturning-方法返回后通知、afterThrowing-抛出异常后通知、around-环绕通知等,配置方式与参数大同小异。

  • 通知时机示意图


    切面连接点示意图.png

2.3.1 before-前置通知

前置通知,顾名思义,就是在方法主体代码执行前接收调用通知。前置通知的注解为@Before,XML节点标签为

  • 注解定义
public @interface Before{
    /*通知表达式*/
    String value() default ""
    /*通知参数命名,多个参数用半角分号隔开*/
    String argNames() default ""
}
  • 注解配置
public class Logging{
    @Before(value="execution(public * com.toipr..UserInfo.set*(*)))")
    public void beforeAdvice(JoinPoint jp){
        Object target = jp.getTarget();
        System.out.println("target object : " + target.toString());
    }
}
  • XML配置
    标签名称,节点有method(必选)、pointcut、pointcut-ref与arg-names四个属性。参考样例如下:

2.3.2 after后置通知

与before-前置通知基本一致。

2.3.3 afterReturning返回后通知

@Before@After不一样,@AfterReturning通知有一个重要的returning配置项,在方法调用返回后触发通知。

  • 注解定义
public @interface AfterReturning {
    String value() default ""
    String argNames() default ""
    
    /* 切入点表达式,设置后覆盖value属性 */
    String pointcut() default ""
    /* 返回值的参数命名 */
    String returning() default ""
}
  • 注解配置
@Aspect
public class Logging{
    @AfterReturning(pointcut="calculator()", returning = "retVal")
    public void afterReturningAdvice(Object retVal){
        if(retVal==null){
            return;
        }
        System.out.println("Returning value=" + retVal.toString());
    }
}

注意:returning属性配置返回值参数名称,对应函数的参数名称必须一致,并且,有的函数没有返回值。

  • XML配置

    
    

注意:与注解形式相比,XML多了pointcut-ref与pointcut两个属性,pointcut-ref用于引用命名切入点,pointcut用于申明专用的匿名切入点。

2.3.4 AfterThrowing异常后通知

捕获异常后通知@AfterThrowing,顾名思义,在捕获方法调用抛出异常后触发通知。

  • 注解定义
public @interface AfterThrowing {
    String value() default ""
    String argNames() default ""
    
    /* 切入点表达式,设置后覆盖value属性 */
    String pointcut() default ""
    /* 抛出异常的参数命名 */
    String throwing() default ""
}
  • 注解配置
@Aspect
public class Logging{
    @AfterThrowing(pointcut="calculator()",  throwing = "ex")
    public void afterThrowingAdvice(IllegalArgumentException ex){
        System.out.println("There has been an exceptin:" + ex.toString());
    }
}
  • XML配置

2.3.5 around环绕通知

环绕通知@Around,顾名思义,在一个方法的执行前与执行后触发通知,注解接口定义与@Before一致,XML节点标签。借助外部调用参数或线程局部变量,可实现性能监控与分析功能。

3 切入点表达式

切入点表达式知识点包括语法与切入点指示符两个部分。

3.1 表达式语法

3.1.1 表达式结构

注解? 修饰符? 返回值类型? 类型申明? 方法匹配(参数列表) 异常列表?

  • 注解,可选,用于匹配方法上申明的注解,如@Documented等。注意:注解类型必须是全限定名称
  • 修饰符,可选,如public / protected / private等,public代表匹配所有公有方法,用通配符*表示不做任何限定。
  • 返回值类型必选,可填写如何类型,如java.lang.String,用通配符*表示返回任意类型。
  • 类型申明,可选,可填写任何类型,匹配类/接口的全路径,用通配符*或..模式匹配。例如:com.toipr.*与com.toipr..等。
  • 方法匹配必选,可以用方法全名、精确匹配,也可以用*通配符模糊匹配,只有“*"匹配任意方法,用“前缀+*”匹配名称以特定文本前缀开头的方法,用“*+后缀”匹配名称以特定文本后缀结尾的方法。例如:find*表示以前缀“find”开头的方法。
  • 参数列表必选,“()”匹配无任何参数的方法,“(*)”匹配只有一个参数、类型任意的方法,“(..)”匹配任意多个参数的方法,“(type, ..)”表示第一个参数为type、后接多个参数的方法,“(.., type)”匹配最后一个参数类型为type、前面有任意多个参数的方法,参数类型type可以是java.lang.String,也可以自由指定。注意:参数类型名称必须为全限定名称
  • 异常列表,可选,以"throws 异常全限定名称(1个或多个)"表示,多个异常用半角分号隔开,如throws java.lang.ClassCastException。注意:异常名称必须是全限定名称

3.1.2 通配符

表达式支持三个常用通配符:*、..和+。

  • 星号通配符*

匹配任意数量的字符。

  • 加号通配符+

匹配某个类型的子类型,一般作为后缀放到接口或类名称后边,如com.toipr..Calculator+.div(..)匹配com.toipr包下任意层级、从Calculator派生的子类的div方法。

  • 双点通配符..

匹配任意数量字符的重复。最常用的两个作用:1) 类/接口匹配,表示匹配任意层级包名称; 2) 方法参数匹配,表示匹配任意多个参数。

3.1.3 逻辑运算符

AspectJ表达式与JAVA一样,使用&&、||、与!三个逻辑运算符,使用括号对表达式分组。&&表示逻辑与,||表示逻辑或,!表示逻辑非,也可以用and / or / not替换,尤其在XML Schema配置中,避免使用&等转义字符。

3.2 切入点指示符

@Pointcut切入点指示符用于匹配切入的对象与方法,有execution / within / target / this / args / get / set / call / handler等数十个切入点指示符,其中,使用最多最重要的是execution / within / args / target指示符。

序号 指示符 用途
1 execution 用于匹配方法的执行连接点。
2 args 用于指定参数列表与类型来限定匹配方法的执行连接点。
3 target 用于匹配当前目标对象类型的执行连接点。
4 within 用于限定在某个包内或某个类型下所有方法的执行连接点。

3.2.1 execution指示符

使用execution("expression")的形式配置,匹配切面连接的方法。应用示例如下表:

序号 模式 模式含义
1 public * com.toipr..UserInfo.*(..) 匹配com.toipr包下任意层级的类UserInfo的任意公有方法
2 public * *(..) 匹配任意公有方法
3 public * com.toipr..*.*(..) 匹配com.toipr包下的任意公有方法
4 public * com.toipr..UserInfo.setName(*) 匹配com.toipr包下任意层级UserInfo类的的公有方法setName,并且,方法只有一个参数、类型任意。
5 public * com.toipr..UserInfo.setName(java.lang.String) 匹配com.toipr包下任意层级UserInfo类的的公有方法setName,并且,方法只有一个参数、类型为java.lang.String。
6 @java.lang.Deprecated * *(..) 匹配所有持有@Deprecated注解的方法
7 @java.lang.Deprecated @java.lang.Documented * *(..) 匹配同时持有@Deprecated和@Documented注解的所有方法。
8 @(java.lang.Deprecated || java.lang.Documented) * *(..) 匹配持有@Deprecated或@Documented注解的所有方法
9 public java.lang.String com.toipr..UserInfo.get*() 匹配com.toipr包下任意层级UserInfo类、以get开头的无参数公有方法,且方法返回值类型为字符串java.lang.String类型
10 * com.toipr..UserInfo+.*(java.lang.String, ..) 匹配com.toipr包下任意层级、派生自UserInfo的第一个参数为java.lang.String类型的任意方法
11 @RequestMapping * com.toipr..*.*(..,java.util.Map) 匹配在com.toipr包下,持有@RequestMapping注解,且最后一个参数是Map类型的所有方法。
12 public * com.toipr..Calculator.*(..) throws java.lang.Exception 匹配com.toipr包下任意层级Calculator类/接口的、申明抛出java.lang.Exception异常的任意公有方法。

3.2.2 args指示符

参数指示符,匹配方法的参数类型是否一致,不一致则排除,使用args(参数类型列表)的形式文本串表达。

  • 参数类型数量:参数类型列表可以有一个或多个,多个类型用半角分号隔开。
  • 参数类型名称:必须使用全限定名称,如字符串类型String必须用java.lang.String表示。
    应用示例

args(java.lang.String):匹配只有一个字符串String类型的方法。
args(java.lang.String, int) : 配置有两个参数,第一个参数为字符串类型、第二个参数为整型的方法。
args(java.lang.String, ..) : 匹配第一个参数为String类型、后接任意多参数的方法。
args(.., java.lang.String) : 匹配最后一个参数为String类型、前面有任意多参数的方法。

3.2.3 target指示符

目标对象指示符target,使用目标对象的类型匹配,采用"target(类型限定表达式)"。注意:类型限定表达式不支持通配符,类型名称必须是全限定名称

  • 应用示例

target(com.toipr.aop.bean.Calculator):匹配com.toipr.aop.bean.Calculator的任意方法。

3.2.4 within指示符

包或类型限定指示符within,采用"within(类型限定表达式)",匹配切面连接的包路径或类。与target指示符不一样,这里的表达式支持通配符。

  • 应用示例

within(com.toipr.aop..*):匹配com.toipr.aop包下任意类方法。
within(com.toipr.aop..Calculator+):匹配com.toipr.aop包下,任意从Calculator派生的类方法。



4 通知参数JoinPoint

搞清楚如何配置切入点和通知后,我最关心的是,我如何获取切入点方法的参数问题,无论在日志记录、权限验证等功能,都很可能使用到切入点方法参数。
切面对象的通知函数可以把JoinPoint接口作为方法的第一个参数,调用接口方法的getTarget方法获取方法所在类的实例,调用getArgs获取该方法的调用入参列表。

4.1 接口定义

连接点相关接口有三个JoinPoint、ProceedingJoinPoint与StaticPart。

  • JoinPoint连接点
public interface JoinPoint {
    /* 获取切面对象,直接用this更高效安全 */
    Object getThis();
    /* 获取被切入方法所在对象类的实例 */
    Object getTarget();
    /* 获取被切入方法的调用入参列表 */
    Object[] getArgs();
    /* 获取静态方法辅助信息 */
    StaticPart getStaticPart();

    /* 获取方法签名 */
    Signature getSignature();
    /* 获取源代码位置信息,不一定能正常获取 */
    SourceLocation getSourceLocation();

    public interface StaticPart {
        /* 获取静态方法的ID */
        int getId();
        /* 获取静态调用的类型 */
        String getKind();
        /* 获取方法签名 */
        Signature getSignature();
        /* 获取源代码位置信息 */
        SourceLocation getSourceLocation();
    }

    static String METHOD_EXECUTION = "method-execution";
    static String METHOD_CALL = "method-call";
    static String CONSTRUCTOR_EXECUTION = "constructor-execution";
    static String CONSTRUCTOR_CALL = "constructor-call";
    static String FIELD_GET = "field-get";
    static String FIELD_SET = "field-set";
    static String STATICINITIALIZATION = "staticinitialization";
    static String PREINITIALIZATION = "preinitialization";
    static String INITIALIZATION = "initialization";
    static String EXCEPTION_HANDLER = "exception-handler";
    static String SYNCHRONIZATION_LOCK = "lock";
    static String SYNCHRONIZATION_UNLOCK = "unlock";
    static String ADVICE_EXECUTION = "adviceexecution"; 
}

通过JoinPoint连接点的接口定义来看,最常用的应该就是getTarget与getArgs方法,getSourceLocation在某些功能比较有用。注意:getThis函数获取切面代理对象,一般可以在通知函数中用this替代,更高效安全。

  • ProceedingJoinPoint
    用于环绕通知的处理,继承JoinPoint接口,增加了proceed两个重要方法,用于执行被切入方法。
public interface ProceedingJoinPoint extends JoinPoint {
    /* 执行被切入方法 */
    Object proceed() throws Throwable;
    Object proceed(Object[] args) throws Throwable;

    /* 没用过 */
    void set$AroundClosure(AroundClosure arc);
    void stack$AroundClosure(AroundClosure arc);
}

4.2 使用示例

JoinPoint连接点

@Aspect
public class Logging {
    @Before(value="calculator()")
    public void beforeAdvice(JoinPoint jp){
        Object target = jp.getTarget(); //获取被切入方法所在的目标对象实例
        System.out.println("target object class is " + target.getClass().getName());

        Object[] args = jp.getArgs(); //获取被切入方法的调用入参列表
        if(args!=null && args.length>0){
            System.out.println("going to setup profile. name=" + args[0].toString());
        }
    }
}
  • 目标对象。调用JoinPoint连接点的getTarget方法,可以获得被切入方法所在对象类的实例,后续可以采用类型转换、反射技术等方法访问该对象的属性与方法。
  • 入参列表。调用JointPoint连接点的getArgs方法,可以获得被切入方法的调用入参列表,可以实现参数检查、日志记录等功能。

ProceedingJoinPoint,用于处理环绕通知

@Aspect
public class Logging {
    @Around(value="calculator()")
    public Object aroundAdvice(ProceedingJoinPoint jp){
        Object retVal = null;
        Date tmBeg = new Date();
        try {
            retVal = jp.proceed();
            System.out.println("aroundAdvice result=" + retVal.toString());
        }catch(Throwable ex){
            ex.printStackTrace();
        }
        Date tmEnd = new Date();
        System.out.println("Time elapsed :" + (tmEnd.getTime() - tmBeg.getTime()));
        return retVal;
    }
}

这个例子简单的演示性能监测功能,在方法执行前记录开始时间,方法执行结束后记录结束时间,可以记录一些执行时间异常的调用,分析相关参数及时了解性能差异原因。注意:该方法一定要将proceed方法的直接结果返回,否则会产生异常

5 测试项目源代码

第一步 创建MAVEN项目
用MAVEN模板quickstart创建一个简单项目。

  • 项目名称,自己随意取
  com.toipr.aop
  AopStudy
  1.0.0.1
  • 依赖配置
    
      org.springframework
      spring-context
      5.2.4.RELEASE
    

    
      org.springframework
      spring-aop
      5.2.4.RELEASE
    

    
      org.aspectj
      aspectjrt
      1.9.5
    

    
      org.aspectj
      aspectjweaver
      1.9.5
    

第二步 添加com.toipr.aop.bean.SimpleCalculator类

package com.toipr.aop.bean;

public class SimpleCalculator implements Calculator {
    public int add(int a, int b){
        return (a + b);
    }
    
    public long div(int a, int b){
        return (a / b);
    }
}

第三步 添加切面对象com.toipr.log.Logging类,切面通知的响应函数在这里实现

package com.toipr.aop.log;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

import java.util.Date;

public class Logging {
    public void beforeAdvice(JoinPoint jp){
        System.out.println("-----beforeAdvice-----");
        Object target = jp.getTarget(); //获取被切入方法所在目标对象
        Object[] args = jp.getArgs(); //获取被切入方法调用入参函数
        if(args!=null && args.length==2){
            System.out.println("a=" + args[0].toString() + "\tb=" + args[1].toString());
        }
    }

    public void afterAdvice(){
        System.out.println("-----afterAdvice-----");
    }

    public void afterReturningAdvice(Object retVal){
        System.out.println("-----afterReturningAdvice-----");
        if(retVal==null){
            return;
        }
        System.out.println("Returning value=" + retVal.toString());
    }

    public void afterThrowingAdvice(Throwable ex){
        System.out.println("-----afterThrowingAdvice-----");
        System.out.println("There has been an exceptin:" + ex.toString());
    }

    public Object aroundAdvice(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("-----enter aroundAdvice-----");
        Date tmBeg = new Date();
        Object retVal = jp.proceed();
        if(retVal!=null) {
            System.out.println("aroundAdvice result=" + retVal.toString());
        }

        Date tmEnd = new Date();
        System.out.println("Time elapsed :" + (tmEnd.getTime() - tmBeg.getTime()));
        System.out.println("-----leave aroundAdvice-----");
        return retVal;
    }
}

第四步 配置beans.xml




    
    
    

    
    

    
        
        
            
            

            
            
            
            
            
        
    

第五步 集成调试,打完收工

package com.toipr.aop;

import com.toipr.aop.bean.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * AOP AspectJ知识点练习
 * @author 明月照我行@
 */
public class App 
{
    public static void main( String[] args ) throws Exception
    {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/beans.xml");

        Calculator calc = (Calculator)ctx.getBean("simpleCalculator");
        System.out.println("result=" + calc.add(5, 8)); //正常流程
        Thread.sleep(1000);
        calc.div(5, 0); //除0错,异常流程
        Thread.sleep(1000);
    }
}
  • 执行输出结果
-----beforeAdvice-----
a=5 b=8
-----enter aroundAdvice-----
aroundAdvice result=13
Time elapsed :0
-----leave aroundAdvice-----
-----afterReturningAdvice-----
Returning value=13
-----afterAdvice-----
result=13
Disconnected from the target VM, address: '127.0.0.1:53952', transport: 'socket'
-----beforeAdvice-----
a=5 b=0
-----enter aroundAdvice-----
-----afterThrowingAdvice-----
There has been an exceptin:java.lang.ArithmeticException: / by zero
-----afterAdvice-----
Exception in thread "main" java.lang.ArithmeticException: / by zero

6 结尾

断断续续,学习好多同行总结的文章,我在学习的基础上重新梳理,再次分享给后来学习者。

你可能感兴趣的:(AspectJ切面编程的配置与语法详解)