AOP
(一)动态代理
1. 方式
- 基于接口实现动态代理: JDK动态代理
- 基于继承实现动态代理: Cglib、Javassist动态代理
2. 一个方法
Object proxyObject = Proxy.newProxyInstance(ClassLoader classLoader, Class[] interfces, InvocationHandler h)
方法的作用:动态的创建实现了interfaces数组中所有指定接口的实现类对象:
参数:
- ClassLoader:类加载器,把
.class
文件加载到内存,形成Class对象。 - Class[] interfaces:指定要实现的接口们
- InvocationHandler:代理对象的所有方法(个别不执行,getClass())都会调用InvocationHandler的invoke()方法
3. InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args)
这个invoke()方法什么时候被调用:在调用代理对象所实现接口中的方法时。
invoke()方法的参数:
- Object proxy:当前对象,即代理对象(在调用谁的方法)
- Method method:当前被调用的方法(目标方法)
- Object[] args:实参(传入目标方法中需要传入的实际参数)
4. 动态代理作用
最终是学习AOP(面向切面编程),它与装饰者模式有点相似,它比装饰者模式还要灵活!
目标对象:被增强的对象
代理对象:需要目标对象,然后在目标对象上添加了增强后的对象!
目标方法:增强的内容
代理对象 = 目标对象 + 增强
5. 代码实现
public Object getProxy(){
//获取当前类的类加载器,用来加载代理对象所属类
ClassLoader loader = this.getClass().getClassLoader();
//获取目标对象实现的所有接口的Class,代理类回合目标类实现相同的接口,最终通过代理对象实现功能
Class[] interfaces = mathImpl.getClass().getInterfaces();
return Proxy.newProxyInstance(loader,interfaces,new InvocationHander(){
//代理对象实现功能的方式
@Override
public Object invoke(Object proxy,Method method,Object[] args)throws Throwale{
try{
MyLogger.before(method.getName(), Arrays.toString(args));//增强的功能
Object result = method.invoke(mathImpl, args);//动态代理对象实现功能
MyLogger.after(method.getName(), result);//增强的功能
return result;
} catch (Exception e) {
MyLogger.throwing();
e.printStackTrace();
} finally {
System.out.println("哪都有我");
}
return null;
}
});
}
(二)AOP概述
AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
-
面向对象 纵向继承机制
-
面向切面 横向抽取机制
AOP编程操作的主要对象是切面(aspect),而切面用于模块化横切关注点(公共功能)。在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
(三)AOP术语
横切关注点
每个方法中抽取出来了的同一类非核心业务
切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。
通知(Advice)
切面必须要完成的各个具体工作,就是在其它类中的横切关注点。
分为五种通知
① @Before:前置通知,在方法执行之前执行
② @After:后置通知,在方法执行之后执行
③ @AfterRunning:返回通知,在方法返回结果之后执行
④ @AfterThrowing:异常通知,在方法抛出异常之后执行
⑥ @Around:环绕通知,围绕着方法执行
目标(Target)
被通知的对象,就是被的代理的对象
代理(Proxy)
向目标对象应用通知之后创建的代理对象
连接点(Joinpoint)
就是横切关注点抽取的位置,例如:类某个方法调用前、调用后、方法捕获到异常后等
切入点(pointcut)
相对于代理类来说的,就是找到连接点的位置,在此位置切入方法。
切入表达式
@Before(value = "execution(public int com.atguigu.spring.aop.MathImpl.add(int, int))")
(四)AspectJ
AspectJ:Java社区里最完整最流行的AOP框架。
1. 在Spring中启用AspectJ注解支持
1)导包
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
2)引入aop命名空间
3)配置
<aop:aspectj-autoproxy>
当Spring IOC容器侦测到bean配置文件中的<aop:aspectj-autoproxy>元素时,会自动为 与AspectJ切面匹配的bean创建代理
<context:component-scan base-package="com.atguigu.spring.aop"></context:component-scan>
<!-- 开启aspectJ的自动代理功能 -->
<aop:aspectj-autoproxy />
2. 用AspectJ注解声明切面
-
要在 Spring 中声明 AspectJ 切面,只需要在IOC容器中将切面声明为 bean 实例
-
当在SpringIOC 容器中初始化 AspectJ 切面之后,SpringIOC 容器就会为那些与 AspectJ 切面相匹配的 bean 创建代理
-
在 AspectJ注解中,切面只是一个带有@Aspect 注解的 Java 类,他往往要包含很多通知。
-
通知是标识有某种注解的简单的Java 方法
-
AspectJ 支持5中类型的通知注解:
① @Before:前置通知,在方法执行之前执行
② @After:后置通知,在方法执行之后执行
③ @AfterRunning:返回通知,在方法返回结果之后执行
④ @AfterThrowing:异常通知,在方法抛出异常之后执行
⑥ @Around:环绕通知,围绕着方法执行
(五)AOP细节
1. 切入点表达式
@Before(value="execution(* com.atguigu.spring.aop.*.*(..))")
* 第一个*代表任意的访问修饰符和返回值类型
* 第二个*代表任意类
* 第三个*代表类中任意方法
* ..代表任意的参数列表
注意:在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来
2. 当前连接点细节
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中。
@Before(value="execution(* com.atguigu.spring.aop.*.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();//获取方法的参数
String methodName = joinPoint.getSignature().getName();//获取方法名
System.out.println("method:"+methodName+",arguments:"+Arrays.toString(args));
}
3. 通知
-
在具体的连接点上要执行的操作。
-
一个切面可以包括一个或者多个通知。
-
通知所使用的注解的值往往是切入点表达式。
1)前置通知
-
前置通知:在方法执行之前执行的通知
-
使用@Before注解
2)后置通知
-
后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候。作用于方法的finally语句块,即不管有没有异常都会执行
-
使用@After注解
3)返回通知
- @AfterReturning:将方法标注为返回通知
- 返回通知:作用于方法执行之后,就是try{} 语句块中的最后一句
- 在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
- 要想在方法中使用,必须在方法的形参中设置和变量名相同的参数名的参数(必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值)
4)异常通知
-
异常通知:@AfterThrowing:将方法标注为异常通知(例外通知),只在连接点抛出异常时才执行异常通知
-
将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
-
如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
5)环绕通知
-
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
-
对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
-
在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
-
注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。
代码
package com.atguigu.spring.aop;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect//标注当前类为切面
@Order(1)//定义切面作用的优先级,值越小优先级越高,默认值为int的最大值
public class MyloggerAspect {
//重用切入点定义
@Pointcut(value="execution(* com.atguigu.spring.aop.*.*(..))")
public void test() {}
/**
* @Before:将方法指定为前置通知
* 必须设置value,其值为切入点表达式
* 前置通知:作用于方法执行之前
*/
//@Before(value = "execution(public int com.atguigu.spring.aop.MathImpl.add(int, int))")
@Before(value="test()")
public void beforeMethod(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();//获取方法的参数
String methodName = joinPoint.getSignature().getName();//获取方法名
System.out.println("method:"+methodName+",arguments:"+Arrays.toString(args));
}
/**
* @After:将方法标注为后置通知
* 后置通知:作用于方法的finally语句块,即不管有没有异常都会执行
*/
//@After(value="execution(* com.atguigu.spring.aop.*.*(..))")
@After(value="test()")
public void afterMethod() {
System.out.println("后置通知");
}
/**
* @AfterReturning:将方法标注为返回通知
* 返回通知:作用于方法执行之后
* 可通过returning设置接收方法返回值的变量名
* 要想在方法中使用,必须在方法的形参中设置和变量名相同的参数名的参数
*/
//@AfterReturning(value="execution(* com.atguigu.spring.aop.*.*(..))", returning="result")
@AfterReturning(value="test()", returning="result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("method:"+methodName+",result:"+result);
}
/**
* @AfterThrowing:将方法标注为异常通知(例外通知)
* 异常通知(例外通知):作用于方法抛出异常时
* 可通过throwing设置接收方法返回的异常信息
* 在参数列表中课通过具体的异常类型,来对指定的异常信息进行操作
*/
//@AfterThrowing(value="execution(* com.atguigu.spring.aop.*.*(..))", throwing="ex")
@AfterThrowing(value="test()", throwing="ex")
public void afterThrowingMethod(ArithmeticException ex) {
System.out.println("有异常了,messages:"+ex);
}
//环绕通知
@Around(value="execution(* com.atguigu.spring.aop.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
//前置通知
System.out.println("前置通知");
result = joinPoint.proceed();//执行方法
//返回通知
System.out.println("返回通知");
return result;
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
//异常通知
System.out.println("异常通知");
} finally {
//后置通知
System.out.println("后置通知");
}
return -1;
}
}
4. 重用切入点定义 和 指定切面的优先级
优先级
-
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
-
切面的优先级可以通过实现Ordered接口或利用@Order注解指定。
-
实现Ordered接口,getOrder()方法的返回值越小,优先级越高。
-
若使用@Order注解,序号出现在注解中
(六)以XML方式配置切面
除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
1. 配置
在bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>
元素内部。对于每个切面而言,都要创建一个<aop:aspect>
元素来为具体的切面实现引用后端bean实例。
切面bean必须有一个标识符,供<aop:aspect>
元素引用。
2. 声明切入点
-
切入点使用
<aop:pointcut>
元素声明。 -
切入点必须定义在
<aop:aspect>
元素下,或者直接定义在<aop:config>
元素下。
① 定义在<aop:aspect>
元素下:只对当前切面有效
② 定义在<aop:config>
元素下:对所有切面都有效
- 基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。
3. 声明通知
-
在aop名称空间中,每种通知类型都对应一个特定的XML元素。
-
通知元素需要使用
<pointcut-ref>
来引用切入点,或用<pointcut>
直接嵌入切入点表达式。 -
method属性指定切面类中通知方法的名称
<context:component-scan base-package="com.atguigu.spring.aopxml"></context:component-scan>
<!-- 配置 -->
<aop:config>
<!-- 声明切面 -->
<aop:aspect ref="myLogger">
<!-- 声明切入点 -->
<aop:pointcut expression="execution(* com.atguigu.spring.aopxml.*.*(..))" id="cut"/>
<!-- 声明通知的两种方式 -->
<!-- <aop:before method="before" pointcut="execution(* com.atguigu.spring.aopxml.*.*(..))"/> -->
<aop:before method="before" pointcut-ref="cut"/>
</aop:aspect>
</aop:config>