(1)静态AOP实现:AOP框架在编译阶段即实现对目标类的修改(增强),生成静态的AOP代理类(生成*.class文件已经被改掉了,需要使用特定的编译器)。以AspectJ为代表。
(2)动态AOP实现:AOP框架在运行阶段动态生成AOP代理(在内存中动态地生成AOP代理类:JDK动态代理或cglib),以实现对目标对象的增强。以Spring AOP为代表。
性能上来看: 静态AOP实现的性能好。动态AOP实现需要在运行阶段额外地执行动态生成,有了运行开销。
方便性来看: 动态AOP更方便,因为它不需要额外的编译器。
2、 AspectJ
AspectJ是一个基于Java语言的AOP框架
下载和安装AspectJ步骤:
①在软件所在处打开cmd窗口,使用 java -jar aspectj-1.8.0.jar指令
②进入安装界面,安装即可
成功安装了AspectJ之后,将会在AspectJ的安装路径下看到如下文件结构:
- bin:该路径下存放了aj、aj5、ajc、ajdoc、ajbrowser等命令,其中ajc命令最常用,它的作用类似于javac,用于对普通Java类进行编译时增强。
- docs:该路径下存放了AspectJ的使用说明、参考手册、API文档等文档。
- lib:该路径下的4个JAR文件是AspectJ的核心类库。
- 相关授权文件。
③将...\aspectj1.8\bin路径添加到PATH环境变量中,
将...\aspectj1.8\lib\aspectjrt.jar和...\aspectj1.8\lib\aspectjtools.jar添加到CLASSPATH环境变量中。
3、初试——通过记事本使用AspectJ
eg:
(1)创建三个类:
TargetOne.java
package cony.aspect.service;
public class TargetOne
{
public void info(){
System.out.println("目标方法1");
}
}
TargetTwo.java
package cony.aspect.service;
public class TargetTwo
{
public void info(){
System.out.println("目标方法2");
}
}
Main.java
package cony.aspect.service;
public class Main
{
public static void main(String[] args){
TargetOne one = new TargetOne();
one.info();
TargetTwo two = new TargetTwo();
two.info();
}
}
(2)①使用前
测试:
②使用后
增加一个类:
package cony.aspect;
public aspect AuthAspect
{
before():execution(* cony.aspect.service.*.*(..))
{
System.out.println("====模拟执行权限检查====");
}
}
测试:
此时会发现一个问题:多出了一个"====模拟执行权限检查====",这是因为执行切面的是cony.aspect.service包下面所有类的所有方法,也就是包括了Main类,所以,只要更改Main的包不与执行切面包相同即可。
更改后再次运行:
再增加一个around切面类
TxAspect.java
package cony.aspect;
public aspect TxAspect{
Object around():execution(* cony.aspect.service.*.*(..))
{
System.out.println("---模拟开启事务---");
//回调目标方法
Object rvt = proceed();
System.out.println("---模拟关闭事务---");
return rvt;
}
}
测试:
4、AOP相关概念
① 切面(Aspect):业务流程运行的某个特定步骤,也就是应用运行过程的关注点,关注点可能横切多个对象,所以常常也称为横切关注点。
② 连接点(Joinpoint):程序执行过程中明确的点。如方法的调用,或者异常的抛出。Spring AOP中,连接点总是方法的调用。
③ 增强处理(Advice):AOP框架在特定的切入点执行的增强动作。处理有“around”、“before”和“after”等类型。
④ 切入点(Pointcut):可以插入增强处理的连接点。简而言之,当某个连接点满足指定要求时,该连接点将被添加增强处理,该连接点也就变成了切入点。
⑤ 引入(Introduction):为被修改的类添加方法或成员变量。Spring允许引入新的接口到任何被处理的对象。
⑥ 目标对象:被AOP框架进行增强处理的对象,也被称为被增强的对象。
⑦ AOP代理:AOP框架创建的对象,简单地说,代理就是对目标对象的加强。
Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理。
前者为实现接口的目标对象的代理,后者为不实现接口的目标对象的代理。
⑧ 织入(Weaving):将增强处理添加到目标对象中、并创建一个被增强的对象(AOP代理)的过程就是织入。
织入有两种实现方式:编译时增强(例如AspectJ)和运行时增强(例如CGLIB)。
Spring和其他纯Java AOP框架一样,在运行时完成织入。
5、AOP编程
(1)在eclipse中使用AOP和aspect需要先导包
(2)步骤:
① 需要有目标方法
Dog.java
public class Dog {
public void printDog(){
System.out.println("======我是金毛犬======");
}
}
User.java
public class User {
public void printUser(){
System.out.println("=======我是小米========");
}
}
② 需要Advice
i.定义Aspect类。为了把一个类变成aspect,有3种方式。
A、注解 B、XML配置 C、将class改成aspect
ii.在Aspect类定义方法,并将该方法专程Advice,有3种方式。
A、注解 B、XML配置 C、将方法签名改成特殊语法
③ 把advice放到目标方法的指定位置。
通过pointcut或pointcut-ref属性来指定。
用注解时要注意:
a、需要添加<aop:aspectj-autoproxy/>元素。
b、强制Spring去处理@AspectJ的注解。
eg1——使用xml配置:
aspect类:
public class AspectTest1 {
public void beforeAdv(){
System.out.println("---模拟开启事务---");
}
}
Spring配置文件(需要导入aop命名空间):
测试:
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) appContext.getBean("user");
Dog dog = (Dog) appContext.getBean("dog");
user.printUser();
dog.printDog();
eg2——使用注解配置:
aspect类:
@Aspect
public class AspectTest1 {
//execution(* cony.domain.*.*(..))表示执行返回类型不限、cony.domain包下的任意类的任意方法形参不限
@Before(value = "execution(* cony.domain.*.*(..))")
public void beforeAdv(){
System.out.println("---模拟开启事务---");
}
}
Spring配置文件(需要导入context命名空间):
eg3——使用将class改成aspect:
参考:3、初试——通过记事本使用AspectJ例子
6、Advice
Advice一共分成5种:Before、AfterReturning、AfterThrowing、After、Around
不管配置哪种Advice,都需要指定2个属性:
- method:指定将哪个方法转换成Advice。如果用注解就不需要该属性,因此注解放在方法上。
- pointcut或pointcut-ref:pointcut直接指定切入点表达式;pointcut-ref引用已有的切入点表达式。
(1)Before——在目标执行之前织入的advice。
(2)AfterReturning——在目标方法成功完成之后织入的advice。
可额外指定一个returning属性,该属性值有2个作用:
- Advice方法可通过该属性指定的值来访问目标方法的返回值。
- Advice方法声明该返回值类型时,限制目标方法的返回值必须是指定的类型。否则该Advice不会织入。
如果不想对目标方法的返回值进行限制,可将该返回值声明为Object
eg:
①修改5中Dog类为:
public class Dog {
public int printDog(){
System.out.println("======我是金毛犬======");
return 1;
}
}
②新增一个aspect类
public class AspectTest2 {
public void afterReturn(int rvt){
System.out.println("---模拟记录日志---,返回值为"+rvt);
}
}
③
Spring配置文件
<bean id="user" class="cony.domain.User"/>
<bean id="dog" class="cony.domain.Dog"/>
<bean id="aspect1" class="cony.aspect.AspectTest1"/>
<bean id="aspect2" class="cony.aspect.AspectTest2"/>
<aop:config>
<aop:pointcut expression="execution(* cony.domain.*.*(..))" id="myPc"/>
<aop:aspect ref="aspect1">
<aop:before method="beforeAdv" pointcut-ref="myPc"/>
</aop:aspect>
<aop:aspect ref="aspect2">
<aop:after-returning method="afterReturn"
pointcut-ref="myPc" returning="rvt"/>
</aop:aspect>
</aop:config>
④测试:
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) appContext.getBean("user");
Dog dog = (Dog) appContext.getBean("dog");
user.printUser();
dog.printDog();
注意:
(3)AfterThrowing——在目标方法出现异常时织入的advice。
这种Advice类似于catch块。
可额外指定一个throwing属性,该属性值有2个作用:
- Advice方法可通过该属性指定的值来访问目标方法抛出的异常。
- Advice方法声明该返回值类型时,限制目标方法的抛出的异常必须是指定的类型。否则该Advice不会织入。
如果不想对目标方法抛出的异常进行限制,可将该异常声明为Exception或Throwable
eg:
①修改5中User类为:
public class User {
public void printUser(){
System.out.println("=======我是小米========");
}
public void deleteUser(Integer id){
if(id < 0){
throw new IllegalArgumentException("删除用户id不能小于0");
}
System.out.println("模拟删除用户:"+id);
}
}
②
新增一个aspect类
public class AspectTest3 {
public void repair(Throwable e){
System.out.println("~~~模拟对异常的修复,异常为:"+e);
}
}
③
Spring配置文件新增:
<bean id="aspect3" class="cony.aspect.AspectTest3"/>
<aop:config>
<aop:pointcut expression="execution(* cony.domain.*.*(..))" id="myPc"/>
<aop:aspect ref="aspect3">
<aop:after-throwing method="repair"
pointcut-ref="myPc" throwing="e"/>
</aop:aspect>
</aop:config>
④测试:
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) appContext.getBean("user");
Dog dog = (Dog) appContext.getBean("dog");
dog.printDog();
user.printUser();
user.deleteUser(-2);
(4)After——不管目标方法是成功完成,还是异常结束,该Advice都会织入
这种Advice类似于finally块。
①新增一个aspect类
public class ReleaseAspect {
public void release(){
System.out.println("---模拟对资源的释放---");
}
}
②
Spring配置文件新增:
<bean id="aspect4" class="cony.aspect.ReleaseAspect"/>
<aop:config>
<aop:aspect ref="aspect4">
<aop:after method="release" pointcut-ref="myPc"/>
</aop:aspect>
</aop:config>
③测试:
正常情况下测试:
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) appContext.getBean("user");
Dog dog = (Dog) appContext.getBean("dog");
dog.printDog();
user.printUser();
user.deleteUser(1);
异常情况下测试:
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) appContext.getBean("user");
Dog dog = (Dog) appContext.getBean("dog");
dog.printDog();
user.printUser();
user.deleteUser(-1);
(5)Around
这种Advice功能最强大,它既可访问、修改目标方法调用参数,也可访问、修改目标方法的返回值。
它甚至可以完全阻止目标方法的执行。
它既可以在目标方法之前织入,也可以在目标方法之后织入。
弱点:它是线程不安全,能用其他Advice的,就不要用Around advice
Around advice方法必须满足如下3点:
a. 必须声明返回值类型。
b. 必须声明一个类型为ProceedingJoinPoint类型的形参。
c. 必须调用ProceedingJoinPoint形参的proceed()方法,这就是回调目标方法。如果没有这一行,目标方法不会执行。
eg:
①删除除了before的advice类,新增一个aspect类
public class TxAspect {
/**
* 环绕通知使用 @Around 注解来声明。
* 通知的第一个参数必须是 ProceedingJoinPoint 类型。
* 在通知体内,调用 ProceedingJoinPoint 的 proceed() 方法将会导致潜在的连接点方法执行。
*/
public Object aroundAdv(ProceedingJoinPoint jp) throws Throwable{
System.out.println("###模拟开启事务###");
/**
* ProceedingJoinPoint继承了JoinPoint,JoinPoint包含了如下方法
* getArgs:获取目标方法调用参数
* getKind:获取连接点的类型。
* getSignature:获取目标方法的方法签名
* getTarget:获取目标对象
* getThis:获取AOP代理
*/
Object[] args = jp.getArgs();
// 如果调用参数至少有1个,且第一个参数为String类型
if(args!=null && args.length>0 && args[0] instanceof String){
args[0] = "前缀:"+args[0];
}
//---------- 以上在目标方法之前织入---------------
Object rvt = jp.proceed(args); // 回调原来的目标方法,如果去掉此行,目标方法就不会执行
//---------- 以下在目标方法之后织入---------------
System.out.println("###模拟结束事务###");
if(rvt!=null && rvt instanceof Integer){
return (Integer)rvt * (Integer)rvt;
}
return rvt;
}
}
②
修改User类
public class User {
public void addUser(String name){
System.out.println("=======我是 "+name+"========");
}
public void deleteUser(Integer id){
if(id < 0){
throw new IllegalArgumentException();
}
System.out.println("模拟删除用户:"+id);
}
}
修改Dog类
public class Dog {
public Integer printDog(Integer id){
System.out.println("======我是金毛犬======"+id);
return 3;
}
}
③Spring配置文件新增:
<bean id="txAspect" class="cony.aspect.TxAspect"/>
<aop:config>
<aop:pointcut expression="execution(* cony.domain.*.*(..))" id="myPc"/>
<aop:aspect ref="txAspect">
<aop:around method="aroundAdv" pointcut-ref="myPc"/>
</aop:aspect>
</aop:config>
④测试:
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) appContext.getBean("user");
Dog dog = (Dog) appContext.getBean("dog");
System.out.println(dog.printDog(2));
System.out.println();
user.addUser("coco");;
user.deleteUser(1);
7、 访问目标方法的调用参数
(1)通过为Advice方法增加一个JoinPoint类型的形参。
JoinPoint有个getArgs()即可获取所有形参。
(2)通过args切入点指示符。
args(a,..) —— 它还会对目标方法的形参类型进行限制。
eg:
①aspect类
public class AspectTest1 {
// 如果你在args(a,..)指定什么参数,此处就可通过它们来访问目标方法调用参数
public void beforeAdv(String a,String b){
System.out.println("---模拟开启事务---,第一个参数为"+a+";第二个参数为"+b);
}
}
②
Spring配置文件
<bean id="user" class="cony.domain.User"/>
<bean id="dog" class="cony.domain.Dog"/>
<bean id="aspect1" class="cony.aspect.AspectTest1"/>
<aop:config>
<aop:pointcut expression="execution(* cony.domain.*.*(..)) and args(a,b,..)" id="myPc"/>
<aop:aspect ref="aspect1">
<aop:before method="beforeAdv" pointcut-ref="myPc"/>
</aop:aspect>
</aop:config>
③
User.java
public class User {
public void addUser(String name,String pass){
System.out.println("=======添加用户========"+name);
}
public void deleteUser(Integer id){
System.out.println("模拟删除用户:"+id);
}
}
Dog.java
public class Dog {
public int printDog(){
System.out.println("======我是金毛犬======");
return 1;
}
}
④测试
ApplicationContext appContext = new ClassPathXmlApplicationContext("beans.xml");
Dog dog = (Dog) appContext.getBean("dog");
dog.printDog();
User user = (User) appContext.getBean("user");
user.addUser("小米","123");
user.deleteUser(1);
由结果可以看到只有addUser方法被织入了,因为在aspect类中规定两个参数都是String类型,而且在 Spring配置文件中规定参数为两个及以上。
8、 AOP的切入点指示符
格式:execution([访问权限] 返回值类型 包.类.方法(形参) [throws 异常])
默认情况下,都可用*作为通配符。形参列表支持2个通配符, ..代表任意个任意类型的参数; *代表一个任意类型的参数。
eg:
(*,java.lang.String) 2个形参,且第二个形参必须是String
(..,java.lang.String) 1~N个形参,最后一个形参必须是String
(*, ..) 1~N个任意类型的形参。
(..) 0~N个任意类型的形参。
target(类型) ——要求目标对象必须是指定类型。
this(类型) —— 要求AOP代理对象必须是指定类型。
args(a,b) —— 要求目标方法必须有匹配的形参。
bean(beanid) —— 只为特定Bean的所有方法织入增强处理。原生AspectJ并不支持,只有用Spring才支持。
切入点表达式还支持 and、or、not这3个逻辑运算符。