AOP的前奏
-
动态代理的原理
使用一个代理将原本对象包装起来,然后用该代理对象”取代”原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
-
动态代理的方式
基于接口实现动态代理: JDK动态代理
基于继承实现动态代理: Cglib、Javassist动态代理
-
示例代码:
-
IProduct接口:
public interface IProduct {
/**
* 销售
* @param money
*/
void saleProduct(float money);
/**
* 售后
* @param money
*/
void afterService(float money);
}
- Product类:
public class Product implements IProduct {
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,得到钱"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,拿到钱"+money);
}
}
- 测试:
@Test
public void demo01() {
final Product product = new Product();
/**
* 基于接口实现动态代理
* ClassLoader:类加载器,用于加载字节码的,和被代理对象使用相同的类加载器,固定写法
* Class[]:字节码数组,用于让代理对象和被代理对象有相同的方法。固定写法。
* InvocationHandler:用于提供增强的代码,是让我们如何写代理,我们一般是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的,此接口的实现类都是谁用谁写
*/
IProduct proxyInstance = (IProduct)Proxy.newProxyInstance(product.getClass().getClassLoader(), product.getClass().getInterfaces(), new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//执行增强代码
Object returnVaule = null;
//1,获取方法执行的参数
Float money = (Float) args[0];
//2,判断当前方法是不是销售
if ("saleProduct".equals(method.getName())) {
returnVaule = method.invoke(product, money * 0.4f);
}
return returnVaule;
}
});
proxyInstance.saleProduct(100000f);
}
AOP概述
①,AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
-
面向对象 纵向继承机制(功能的添加)
-
面向切面 横向抽取机制(功能的抽取)
②,AOP编程操作的主要对象是切面(aspect),而切面用于模块化横切关注点(公共功能)。
③,在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
④,AOP的好处:
-
每个事物逻辑位于一个位置,代码不分散,便于维护和升级
-
业务模块更简洁,只包含核心业务代码
-
AOP图解
-
横切关注点
从每个方法中抽取出来的同一类非核心业务。
-
切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。
-
通知(Advice)
切面必须要完成的各个具体工作
-
目标(Target)
被通知的对象
-
代理(Proxy)
向目标对象应用通知之后创建的代理对象
-
连接点(Joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
-
切入点(pointcut)
定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
-
图解
-
简介
AspectJ:Java社区里最完整最流行的AOP框架。
在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。
-
用AspectJ注解声明切面
①,要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。
②,当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。
③,在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。
④,通知是标注有某种注解的简单的Java方法。
⑤,AspectJ支持5种类型的通知注解:
@Before
:前置通知,在方法执行之前执行@After
:后置通知,在方法执行之后执行@AfterRunning
:返回通知,在方法返回结果之后执行@AfterThrowing
:异常通知,在方法抛出异常之后执行@Around
:环绕通知,围绕着方法执行
AOP细节
-
作用:
通过表达式的方式定位一个或多个具体的连接点。
-
语法细节:
切入点表达式的语法格式:
execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))
-
举例说明:
表达式 | 含义 |
---|---|
execution(* com.atguigu.spring.ArithmeticCalculator.*(…)) | ArithmeticCalculator接口中声明的所有方法。第一个“”代表任意修饰符及任意返回值。第二个“”代表任意方法。“…”匹配任意数量、任意类型的参数。若目标类、接口与该切面类在同一个包中可以省略包名。 |
- 在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
表达式 | 含义 |
---|---|
execution (* .add(int,…)) || execution( *.sub(int,…)) | 任意类中第一个参数为int类型的add方法或sub方法 |
!execution (* *.add(int,…)) | 匹配不是任意类中第一个参数为int类型的add方法 |
- 切入点表达式应用到实际的切面类中
-
概述
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中。
-
JoinPoint
-
概述
①,在具体的连接点上要执行的操作。
②,一个切面可以包括一个或者多个通知。
③,通知所使用的注解的值往往是切入点表达式。
-
前置通知
在方法执行之前执行的通知,使用
@Before
注解 -
后置通知
后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候,使用
@After
注解 -
返回通知
①,无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
②,使用@
AfterReturning
注解,在返回通知中访问连接点的返回值- 在返回通知中,只要将returning属性添加到
@AfterReturning
注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称 - 必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
- 原始的切点表达式需要出现在pointcut属性中
- 在返回通知中,只要将returning属性添加到
-
异常通知
①,只在连接点抛出异常时才执行异常通知
②,将throwing属性添加到
@AfterThrowing
注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。③,如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
-
环绕通知
①,环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
②,对于环绕通知来说,连接点的参数类型必须是
ProceedingJoinPoint
。它是JoinPoint
的子接口,允许控制何时执行,是否执行连接点。③,在环绕通知中需要明确调用
ProceedingJoinPoint
的proceed()
方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。④,注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用
joinPoint.proceed();
的返回值,否则会出现空指针异常。
①,在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
②,在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
③,切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
④,其他通知可以通过方法名称引入该切入点
①,在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
②,切面的优先级可以通过实现Ordered
接口或利用@Order
注解指定。
③,实现Ordered
接口,getOrder()
方法的返回值越小,优先级越高。
④,若使用@Order
注解,序号出现在注解中
- 示例代码:
- 导入jar包
- aop.xml配置文件
<?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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.aop"></context:component-scan>
<!-- 开启aspectJ的自动代理功能 -->
<aop:aspectj-autoproxy />
</beans>
- MathI接口:
public interface MathI {
int add(int i, int j);
int sub(int i, int j);
int div(int i, int j);
}
- MathImpl类:
@Component
public class MathImpl implements MathI {
@Override
public int add(int i, int j) {
return i +j;
}
@Override
public int sub(int i, int j) {
return i - j;
}
@Override
public int div(int i, int j) {
return i/j;
}
}
- MyLoggerAspect类:
@Component
@Aspect//标注当前类为切面
@Order(1)//定义切面作用的优先级,值越小优先级越高,默认值为int的最大值2^31-1
public class MyLoggerAspect {
@Pointcut(value = "execution(* com.aop.*.*(..))")
public void pointcutTest(){
}
/**
* @Before:将方法指定为前置通知,作用于方法执行之前
* 必须设置value,其值为切入点表达式
*/
// @Before(value = "execution(public int com.aop.MathImpl.add(int, int))")
// @Before(value = "execution(* com.aop.*.*(..))")
@Before(value = "pointcutTest()")
public void beforeMethod(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();//获取方法的参数
String methodName = joinPoint.getSignature().getName();//获取方法名
System.out.println("方法名:"+methodName+"方法的参数:"+ Arrays.toString(args));
}
/**
* @After:将方法标注为后置通知
* 后置通知:作用于方法的finally语句块,即不管有没有异常都会执行
*/
@After(value = "execution(* com.aop.*.*(..))")
public void aferMethod(){
System.out.println("后置通知");
}
/**
* @AfterReturning:将方法标注为返回通知,用于方法执行之后
* 可通过returning设置接收方法返回值的变量名
* 要想在方法中使用,必须在方法的形参中设置和变量名相同的参数名的参数
*/
@AfterReturning(value = "execution(* com.aop.*.*(..))", returning = "result")
public void aferReturning(JoinPoint joinPoint, Object result){
String method = joinPoint.getSignature().getName();
System.out.println("返回通知,方法名"+method+"结果"+result);
}
/**
* @AfterThrowing:将方法标注为异常通知(例外通知),作用于方法抛出异常
* 可通过throwing设置接收方法返回的异常信息
* 在参数列表中可通过具体的异常类型,来对指定的异常信息进行操作
*/
@AfterThrowing(value = "execution(* com.aop.*.*(..))",throwing = "ex")
public void aferThrowing(Exception ex){
System.out.println("异常信息"+ex);
}
@Around(value = "execution(* com.aop.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint){
Object result = null;
try {
//前置通知
System.out.println("===前置通知===");
result = joinPoint.proceed();
//返回通知
System.out.println("===返回通知===");
return result;
} catch (Throwable throwable) {
throwable.printStackTrace();
//异常通知
System.out.println("===异常通知===");
}finally {
//后置通知
System.out.println("===后置通知===");
}
return -1;
}
}
- AopTest 类:
@Component
public class AopTest {
public void test(){
System.out.println("来测试一下");
}
}
- 切面类AspectTest:
@Component
@Aspect
@Order(200)
public class AspectTest {
@Before(value = "execution(* com.aop.*.*(..))")
public void before(){
System.out.println("AspectTest ==== 前置通知");
}
}
- 测试类DemoTest :
public class DemoTest {
@Test
public void demo01(){
ApplicationContext ac = new ClassPathXmlApplicationContext("aop.xml");
MathI mathImpl = ac.getBean("mathImpl", MathI.class);
System.out.println(mathImpl.add(1,2));
System.out.println(mathImpl.div(1,0));
ac.getBean("aopTest",AopTest.class).test();
}
}
以XML方式配置切面
- 除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
-
在bean配置文件中,所有的Spring AOP配置都必须定义在
<aop:config>
元素内部。对于每个切面而言,都要创建一个<aop:aspect>
元素来为具体的切面实现引用后端bean实例。 -
切面bean必须有一个标识符,供
<aop:aspect>
元素引用。
①,切入点使用<aop:pointcut>
元素声明。
②,切入点必须定义在<aop:aspect>
元素下,或者直接定义在<aop:config>
元素下。
-
定义在
<aop:aspect>
元素下:只对当前切面有效 -
定义在
<aop:config>
元素下:对所有切面都有效
③,基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。
①,在aop名称空间中,每种通知类型都对应一个特定的XML元素。
②,通知元素需要使用<pointcut-ref>
来引用切入点,或用<pointcut>
直接嵌入切入点表达式。
③,method属性指定切面类中通知方法的名称