AOP:Aspect Oriented Programing,意为面向切面编程。AOP是OOP的补充。
我们先从一个示例开始:
public class Cacluate {
public int add(int num1,int num2){
return num1 + num2;
}
public int subtract(int num1,int num2){
return num1 - num2;
}
public int multiply(int num1,int num2){
return num1 * num2;
}
public int divide(int num1,int num2){
return num1 / num2;
}
}
这是一个做加减乘除运算的工具类,可以通过调用该类的方法计算两个数的运算值:
public static void main(String[] args) {
Cacluate cacluate = new Cacluate();
int addResult = cacluate.add(10,2);
int subtractResult = cacluate.subtract(10,2);
int multiplyResult = cacluate.multiply(10,2);
int divideResult = cacluate.divide(10,2);
}
现在需求来了,若是想在每次做运算之前输出一个日志信息,记录当前系统的运行情况,我们应该如何实现呢?一种方法是在每个方法中添加一行日志打印,但这和业务严重耦合。
AOP它通过横向抽取机制为这类无法通过纵向继承体系进行抽象的重复性代码提供了解决方案,AOP将日志操作横向抽取出来,再将日志操作融合到业务逻辑中实现功能。
AOP的相关概念
JoinPoint——连接点
何为连接点?连接点表示的是程序执行的某个特定的位置,比如你要在项目中加入日志的操作,你可以将其设置在类加载前、类加载后、方法执行前、方法执行后,而Spring只支持方法的连接点,即你只能在方法执行前后来设置你需要进行的操作。
PointCut——切点
切点又是什么呢?我们知道,对于方法执行前后的位置我们称之为连接点,但是,我们需要将日志操作设置在哪些类的哪些方法执行前后再执行呢?这一定位称为切点,通常需要通过切点表达式进行过滤。
Advice——通知
通知,也叫增强,当我们通过切点表达式指定了需要切入的位置后,Spring就会在每个切点的位置增强该方法,例如添加上你的日志操作。
Aspect——切面
切面包括横切逻辑的定义,也包括连接点的定义,Spring AOP将切面所定义的横切逻辑切入到切面所指定的连接点中,即它的作用就是将切点和通知结合定位到连接点上。
动态代理
需要了解的是,Spring AOP使用动态代理在运行期切入增强的代码,所以我们需要掌握动态代理的相关知识。
动态代理是反射的高级应用,JDK为我们提供了Proxy和InvocationHandler接口,通过该类和该接口生成代理对象实现方法的增强。
public interface ICar {
String use(String name);
}
public class Car implements ICar {
@Override
public String use(String name) {
System.out.println(name + "--->汽车使用汽油");
return name + " good car";
}
}
定义一个接口和接口的实现类,表示使用汽车需要汽油,但是汽车并不一定只能使用汽油,还能使用电力驱动,为此,可以使用动态代理增强该use(String name)方法:
public class CarInvocationHandler<T> implements InvocationHandler {
private T t;
public CarInvocationHandler(T t) {
this.t = t;
}
//使用反射对接口进行增强
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
for (Object a : args) {
System.out.println("参数a = " + a);
}
// 调用原来的方法
Object invokeReturn = method.invoke(t, args);
// 增强方法
System.out.println("增强方法 " + args[0] + "汽车还能使用电力");
return invokeReturn;
}
}
首先需要实现InvocationHandler接口的invoke方法,并调用invoke()方法,保证原有的功能不被破坏,然后再编写需要增强的逻辑,返回值是目标代理方法的返回值,没有就返回null即可,有了该接口的实现类后,接下来测试:
public class Demo {
public static void main(String[] args) {
ICar car = new Car();
CarInvocationHandler carInvocationHandler = new CarInvocationHandler(car);
Object proxy = Proxy.newProxyInstance(car.getClass().getClassLoader(), car.getClass().getInterfaces(), carInvocationHandler);
if (proxy instanceof ICar) {
ICar iCar = (ICar) proxy;
String use = iCar.use("比亚迪");
System.out.println("调用方法返回值 = " + use);
}
}
}
通过Proxy类创建代理对象,并按照CarInvocatioHandler实现类的规则进行增强,运行结果:
参数a = 比亚迪
比亚迪—>汽车使用汽油
增强方法 比亚迪汽车还能使用电力
调用方法返回值 = 比亚迪 good car
我们将这一过程类比到刚才的场景中去,要对四种计算方法设置日志操作,只需要分别对这四个方法进行动态代理,增强它们即可,然而jdk动态代理有一个缺陷,就是只能针对接口做代理,所以我们需要对计算方法做一个处理:
public interface CacluateDao {
int add(int num1,int num2);
int subtract(int num1,int num2);
int multiply(int num1,int num2);
int divide(int num1,int num2);
}
public class Cacluate implements CacluateDao{
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int subtract(int num1, int num2) {
return num1 - num2;
}
@Override
public int multiply(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
return num1 / num2;
}
}
public class CacluateInvocationHandler<T> implements InvocationHandler {
private T t;
public CacluateInvocationHandler(T t){
this.t = t;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int result = 0;
if(method.getName().equals("add")){
//增强方法
System.out.println("输出日志信息,方法名:" + method.getName() + ",参数:" + Arrays.asList(args));
//保持原有功能
method.invoke(t,args);
result = Integer.valueOf(args[0] + "") + Integer.valueOf(args[1] + "");
}
return result;
}
}
测试:
public static void main(String[] args) {
CacluateDao cacluate = new Cacluate();
CacluateInvocationHandler invocationHandler = new CacluateInvocationHandler(cacluate);
CacluateDao proxy = (CacluateDao) Proxy.newProxyInstance(cacluate.getClass().getClassLoader(), cacluate.getClass().getInterfaces(), invocationHandler);
int result = proxy.add(3, 4);
System.out.println(result);
}
运行结果:
输出日志信息,方法名:add,参数:[3, 4]
7
通知介绍
Advice,确切地说它应该被理解为增强,前面也一直在强调方法的增强,那么接下来我们来看看在Spring AOP中是如何去实现方法的增强的。
package com.yida.aop.common;
/**
* @author :zhangyifei
* @date :Created in 2022/8/26 11:20
* @description:
* @modified By:
* @version:
*/
public interface ICalculatorAop {
int add(int num1, int num2);
int subtract(int num1, int num2);
int multiply(int num1, int num2);
int divide(int num1, int num2);
}
package com.yida.aop.common.impl;
import com.yida.aop.common.ICalculatorAop;
import org.springframework.stereotype.Service;
/**
* @author :zhangyifei
* @date :Created in 2022/8/26 11:20
* @description:
* @modified By:
* @version:
*/
@Service
public class CalculatorAop implements ICalculatorAop {
@Override
public int add(int num1, int num2) {
System.out.println("========>add方法执行中…………num1 = " + num1 + ", num2 = " + num2);
return num1 + num2;
}
@Override
public int subtract(int num1, int num2) {
System.out.println("========>subtract法执行中…………num1 = " + num1 + ", num2 = " + num2);
return num1 - num2;
}
@Override
public int multiply(int num1, int num2) {
System.out.println("========>multiply法执行中…………num1 = " + num1 + ", num2 = " + num2);
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
System.out.println("========>divide法执行中…………num1 = " + num1 + ", num2 = " + num2);
return num1 / num2;
}
}
前置通知
首先我们从实现接口的束缚中脱离出来,然后使用Spring AOP进行增强,比如需要在计算方法之前输出日志信息,你就可以这样做:
@Component
@Aspect
public class LoggingAspectBefore {
/**
* com.yida.aop.common.impl.CalculatorAop.*(..)
* 包括CalculatorAop下所有的方法
*
* @param joinPoint
*/
@Before(value = "execution(* com.yida.aop.common.impl.CalculatorAop.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> params = Arrays.asList(joinPoint.getArgs());
System.out.println("执行在:" + methodName + "的方法,参数params = " + params);
}
}
测试
@SpringBootTest
class ICalculatorAopTest {
@Autowired
private ICalculatorAop iCalculatorAop;
@Test
void add() {
int add = iCalculatorAop.add(1, 3);
System.out.println("add = " + add);
int sub = iCalculatorAop.subtract(5, 3);
System.out.println("sub = " + sub);
int multiply = iCalculatorAop.multiply(2, 8);
System.out.println("multiply = " + multiply);
int divide = iCalculatorAop.divide(10, 5);
System.out.println("divide = " + divide);
}
}
后置通知
学会了前置通知,那么后面的内容就会非常简单了,比如后置通知,只是简单地修改一下注解名就可以了:
@Component
@Aspect
public class AspectAfter {
@After("execution(* com.yida.aop.common.ICalculatorAop.*(..))")
public void after(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("执行在" + name + "方法后的日志信息,方法参数为:" + args);
}
}
需要注意的是后置通知不管程序是否发生错误都会被执行
返回通知
返回通知与后置通知类似,区别在于,返回通知需要在程序正确执行后才会执行,若程序发生异常,则返回通知不会执行:
@Component
@Aspect
public class AspectAfterReturn {
@AfterReturning(value = "execution(* com.yida.aop.common.ICalculatorAop.*(..))", returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result) {
String name = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("执行在" + name + "方法[后]的日志信息,方法参数为:" + args + "\t 结果为:" + result);
}
}
运行结果:
返回通知是能够获取到方法的执行结果的,具体做法是在@AfterReturning中指定returning属性值,然后在方法的入参中定义一个与其相同的变量即可。
异常通知
异常通知,顾名思义,只有当程序发生异常时才会执行,异常通知能够获取到方法发生了什么异常:
@Aspect
@Component
public class AspectExceptionAfterThrow {
@AfterThrowing(value = "execution(* com.yida.aop.common.ICalculatorAop.*(..))", throwing = "e")
public void exceptionAfterThrow(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
List<Object> list = Arrays.asList(joinPoint.getArgs());
System.out.println("执行在" + methodName + "方法后的日志信息,方法参数为:" + list + ",异常信息为:" + e);
}
}
正常情况不执行:
异常情况:
环绕通知
环绕通知的功能比较强大,它能够通过一个方法实现之前的所有通知效果,直接看代码:
@Component
@Aspect
public class AspectAround {
@Around(value = "execution(* com.yida.aop.common.ICalculatorAop.*(..))")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null; // 返回值
String methodName = proceedingJoinPoint.getSignature().getName();
List<Object> args = Arrays.asList(proceedingJoinPoint.getArgs());
try {
// 前置通知
System.out.println(methodName + "【方法前】args = " + args);
// 执行目标方法
result = proceedingJoinPoint.proceed();
// 返回通知
System.out.println(methodName + "【方法后--返回通知】args = " + args + " 执行结果:" + result);
} catch (Throwable e) {
// 异常通知
System.out.println(methodName + "【方法后--异常通知】args = " + args + " 异常信息:" + e);
}
// 后置通知
System.out.println(methodName + "【方法后--后置通知】args = " + args);
// 返回结果
return result;
}
}
从环绕通知应该不难理解,为什么后置通知无论什么情况都会执行,且只有返回通知能够获取到方法执行结果,异常通知如何能够获取到异常信息,一目了然。
需要注意,环绕通知必须携带ProceedingJoinPoint参数并且必须有返回值。