idea下aspectj程序运行示例
有些同学可能想自己编写aspect程序进行测试练习,博主在这简单介绍运行环境的搭建,首先博主使用的idea的IDE,因此只对idea进行介绍。首先通过maven仓库下载工具包aspectjtools-1.8.9.jar,该工具包包含ajc核心编译器,然后打开idea检查是否已安装aspectJ的插件:
配置项目使用ajc编译器(替换javac)如下图:
如果使用maven开发(否则在libs目录自行引入jar)则在pom文件中添加aspectJ的核心依赖包,包含了AspectJ运行时的核心库文件:
<dependency>
<groupId>aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.12</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
新建文件处创建aspectJ文件,然后就可以像运行java文件一样,操作aspect文件了。
这里先进行一个简单案例的演示。编写一个HelloWord的类,然后利用AspectJ技术切入该类的执行过程。
/** * Created by zejian on 2017/2/15. */ public class HelloWord { public void sayHello(){ System.out.println("hello world !"); } public static void main(String args[]){ HelloWord helloWord =new HelloWord(); helloWord.sayHello(); } }
编写AspectJ类,注意关键字为aspect(MyAspectJDemo.aj,其中aj为AspectJ的后缀),含义与class相同,即定义一个AspectJ的类
/** * Created by zejian on 2017/2/15. * 切面类 */ public aspect MyAspectJDemo { /** * 定义切点,日志记录切点 */ pointcut recordLog():call(* HelloWord.sayHello(..)); /** * 定义切点,权限验证(实际开发中日志和权限一般会放在不同的切面中,这里仅为方便演示) */ pointcut authCheck():call(* HelloWord.sayHello(..)); /** * 定义前置通知! */ before():authCheck(){ System.out.println("sayHello方法执行前验证权限"); } /** * 定义后置通知 */ after():recordLog(){ System.out.println("sayHello方法执行后记录日志"); } }
ok~,运行helloworld的main函数:
对于结果不必太惊讶,完全是意料之中。我们发现,明明只运行了main函数,却在sayHello函数运行前后分别进行了权限验证和日志记录,事实上这就是AspectJ的功劳了。
AOP中抽象概念(切入点(pointcut)、通知(advice)、切面(aspect)、织入(weaving))解释
对aspectJ有了感性的认识后,再来聊聊aspectJ到底是什么?AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器),可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。
在案例中,我们使用aspect关键字定义了一个类,这个类就是一个切面,它可以是单独的日志切面(功能),也可以是权限切面或者其他,在切面内部使用了pointcut定义了两个切点,一个用于权限验证,一个用于日志记录,而所谓的切点就是那些需要应用切面的方法,如需要在sayHello方法执行前后进行权限验证和日志记录,那么就需要捕捉该方法,而pointcut就是定义这些需要捕捉的方法(常常是不止一个方法的),这些方法也称为目标方法,最后还定义了两个通知,通知就是那些需要在目标方法前后执行的函数,如before()即前置通知在目标方法之前执行,即在sayHello()方法执行前进行权限验证,另一个是after()即后置通知,在sayHello()之后执行,如进行日志记录。到这里也就可以确定,切面就是切点和通知的组合体,组成一个单独的结构供后续使用,下图协助理解。
这里简单说明一下切点的定义语法:关键字为pointcut,定义切点,后面跟着函数名称,最后编写匹配表达式,此时函数一般使用call()或者execution()进行匹配,这里我们统一使用call()
pointcut 函数名 : 匹配表达式
案例:recordLog()是函数名称,自定义的,* 表示任意返回值,接着就是需要拦截的目标函数,sayHello(..)的..,表示任意参数类型。这里理解即可,后面Spring AOP会有关于切点表达式的分析,整行代码的意思是使用pointcut定义一个名为recordLog的切点函数,其需要拦截的(切入)的目标方法是HelloWord类下的sayHello方法,参数不限。
pointcut recordLog():call(* HelloWord.sayHello(..));
关于定义通知的语法:首先通知有5种类型分别如下:
- before 目标方法执行前执行,前置通知
- after 目标方法执行后执行,后置通知
- after returning 目标方法返回时执行 ,后置返回通知
- after throwing 目标方法抛出异常时执行 异常通知
- around 在目标函数执行中执行,可控制目标函数是否执行,环绕通知
语法:
[返回值类型] 通知函数名称(参数) [returning/throwing 表达式]:连接点函数(切点函数){
函数体
}
案例如下,其中要注意around通知即环绕通知,可以通过proceed()方法控制目标函数是否执行。
/** * 定义前置通知 * * before(参数):连接点函数{ * 函数体 * } */ before():authCheck(){ System.out.println("sayHello方法执行前验证权限"); } /** * 定义后置通知 * after(参数):连接点函数{ * 函数体 * } */ after():recordLog(){ System.out.println("sayHello方法执行后记录日志"); } /** * 定义后置通知带返回值 * after(参数)returning(返回值类型):连接点函数{ * 函数体 * } */ after()returning(int x): get(){ System.out.println("返回值为:"+x); } /** * 异常通知 * after(参数) throwing(返回值类型):连接点函数{ * 函数体 * } */ after() throwing(Exception e):sayHello2(){ System.out.println("抛出异常:"+e.toString()); } /** * 环绕通知 可通过proceed()控制目标函数是否执行 * Object around(参数):连接点函数{ * 函数体 * Object result=proceed();//执行目标函数 * return result; * } */ Object around():aroundAdvice(){ System.out.println("sayAround 执行前执行"); Object result=proceed();//执行目标函数 System.out.println("sayAround 执行后执行"); return result; }
切入点(pointcut)和通知(advice)的概念已比较清晰,而切面则是定义切入点和通知的组合如上述使用aspect关键字定义的MyAspectJDemo,把切面应用到目标函数的过程称为织入(weaving)。在前面定义的HelloWord类中除了sayHello函数外,还有main函数,以后可能还会定义其他函数,而这些函数都可以称为目标函数,也就是说这些函数执行前后也都可以切入通知的代码,这些目标函数统称为连接点,切入点(pointcut)的定义正是从这些连接点中过滤出来的,下图协助理解。
AspectJ的织入方式及其原理概要
经过前面的简单介绍,我们已初步掌握了AspectJ的一些语法和概念,但这样仍然是不够的,我们仍需要了解AspectJ应用到java代码的过程(这个过程称为织入),对于织入这个概念,可以简单理解为aspect(切面)应用到目标函数(类)的过程。对于这个过程,一般分为动态织入和静态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术,这里主要重点分析一下静态织入,ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
关于ajc编译器,是一种能够识别aspect语法的编译器,它是采用java语言编写的,由于javac并不能识别aspect语法,便有了ajc编译器,注意ajc编译器也可编译java文件。为了更直观了解aspect的织入方式,我们打开前面案例中已编译完成的HelloWord.class文件,反编译后的java代码如下:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.zejian.demo; import com.zejian.demo.MyAspectJDemo; //编译后织入aspect类的HelloWord字节码反编译类 public class HelloWord { public HelloWord() { } public void sayHello() { System.out.println("hello world !"); } public static void main(String[] args) { HelloWord helloWord = new HelloWord(); HelloWord var10000 = helloWord; try { //MyAspectJDemo 切面类的前置通知织入 MyAspectJDemo.aspectOf().ajc$before$com_zejian_demo_MyAspectJDemo$1$22c5541(); //目标类函数的调用 var10000.sayHello(); } catch (Throwable var3) { MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574(); throw var3; } //MyAspectJDemo 切面类的后置通知织入 MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574(); } }
显然AspectJ的织入原理已很明朗了,当然除了编译期织入,还存在链接期(编译后)织入,即将aspect类和java目标类同时编译成字节码文件后,再进行织入处理,这种方式比较有助于已编译好的第三方jar和Class文件进行织入操作,由于这不是本篇的重点,暂且不过多分析。
参考资料
http://blog.csdn.net/javazejian/article/details/56267036#神一样的aspectj-aop的领跑者