Spring AOP快速上手
什么是AOP
AOP全称是aspect-oriented programing 面向切面编程。用于解决横向关注点的问题,横向关注点是指多个模块或者模块中的多个功能需要共享的功能,如日志记录、事务管理、安全控制等等。即重复性的代码抽象出来,形成可复用的代码模块。
AOP的核心术语
Joinpoint(连接点):程序执行的某个特定位置,比如(类开始初始化前,初始化后,方法前,方法后,异常后) 一个类或者一段程序代码拥有一些具有边界的特定点,这些特定点称为连接点。
Pointcut(切入点):每个程序都拥有多个连接点,比如一个类有2个方法,这2个方法就是连接点,连接点就是程序中具体的事物,AOP 通过切点来定位特定的连接点,连接点相当于数据库中的记录,而切点相当于是查询记录的条件。在程序中,切点是连接点位置的集合。
Advice(通知/增强):就是我们要具体做的事情,也就在原有的方法之上添加新的能力。
Target(目标对象):就是我们要增强的类。
Introduction(引入):特殊的增强,为类添加属性和方法。哪怕这个类没有任何的属性和方法我们也可以通过aop去添加方法和属性实现逻辑。
Weaving(织入):就是把增强添加到目标类具体的连接点上的过程。
Proxy(代理):一个类被AOP织入增强后产生的结果类,它是原类和增强后的代理类,根据代理不同的方式,代理类可能是和原类具有相同接口的类,也可能是原类的子类。
Aspect(切面):切面由切点和增强组成,他既包含横切的定义,也包括了连接点的定义,spring aop就是负责实施切面的框架,他将切面定义为横切逻辑织入到切面所指定的连接点。
Spring AOP的动态代理模式
当目标类有接口的情况使用JDK动态代理和cglib动态代理,没有接口时只能使用cglib动态代理。
Spring AOP是基于动态代理实现的。对于动态代理,首先得有一个代理类,然后再在目标类上实现代理。根据目标类是否实现了接口来完成动态代理。而在Spring AOP中,根据目标类是否实现了接口分为以下两种情况:
JDK动态代理
JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口。
如果目标类实现了接口,我们可以使用JDK代理实现动态代理(基于接口实现动态代理)。这时候要求代理类需要实现InvocationHander接口:
InvocationHandler(真正地给目标类的目标方法进行增强)
method.invoke(目标类,目标方法参数)
Proxy.newProxyInstance();
//定义接口,指明需要代理的方法
public interface Subject {
void doLogin();
}
//让A实现接口Subject
public class A implements Subject{
@Override
public void doLogin() {
System.out.println("doLogin>>>>>>>>>>>>>>>");
}
public void doLogout(){
System.out.println("doLogout>>>>>>>>>>>>>");
}
}
接下来我们需要实现InvocationHandler接口,它是代理逻辑执行器,不是代理类,它的作用是实现代理类的逻辑:
有一些刚刚了解动态代理的小伙伴会把这个实现类误当做是代理类,InvocationHandler的实现类就是用来实现代理逻辑的,通过重写invoke()方法,对被代理的方法进行增强(比如:上面代码中,我们可以在执行代理方法前后加入其他业务逻辑)。
获取代理对象
通过java.lang.reflect.Proxy
的静态方法newProxyInstance(...)获取我们想要的代理对象
newProxyInstance()
需要的参数如下:
- ClassLoader loader:类加载器;
- Class<?>[] interfaces:代理类需要实现的接口集合(就是被代理类实现的接口);
- InvocationHandler h:代理类会根据传进来的接口去实现需要实现的接口方法,但是接口方法的内部逻辑需要依赖InvocationHandler(就是上面实现InvocationHandler接口的类的invoke()中的内容);
CGLIB动态代理
cglib动态代理动态生成的代理类默认会和目标在在相同的包下,会继承目标类。
如果目标类没有实现接口,我们可以使用CGLIB实现动态代理(基于继承父类而实现代理对象)。当然,就算目标类实现了接口,我们同样可以使用CGLIB来实现动态代理,其本质是使用继承来实现增强的。要使用CGLIB来实现动态代理,要求代理类需要实现MethodInteceptor接口,代理类产生的对象可以通过Enhancer来产生目标对象的代理对象:
MethodInteceptor
method.invoke(); //采用反射机制来完成方法调用
Enhancer enhancer = new Enhancer();
enhancer.setSupperClass();
enhancer.setCallBack();
enhancer.create();
创建需要被代理的类
public class Student2 {
public String getName() {
System.out.println("我叫红领巾");
return "我叫红领巾";
}
public Integer getAge() {
System.out.println("14");
return 14;
}
}
生成代理类 需实现MethodInterceptor接口
public class MyProxy implements MethodInterceptor {
/**
* Enhancer.create(superClass,callback)
* superClass: 生成代理对象的父类
* callback:设置enhancer的回调对象
**/
public <T> T getProxy(Class<T> clazz){
return (T) Enhancer.create(clazz,this);
}
/**
* target:cglib生成的代理对象
* method:被代理对象方法
* args:方法入参
* methodProxy: 代理方法
**/
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("前置");
//用MethodProxy 调用父类方法。
Object returnObject = methodProxy.invokeSuper(target,args);
System.out.println("后置");
return returnObject;
}
}
获取代理对象
public class ProxyTest {
public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/admin");
MyProxy myProxy = new MyProxy();
Student2 student2 = myProxy.getProxy(Student2.class);
student2.getName();
}
}
Spring AOP开发步骤
首先我们需要引入aop的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- 提供了与aspects集成的功能 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
基于xml的AOP配置
在Spring的配置文件中配置:
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.mcode.xmlaop"/>
<!--配置aop五种通知类型-->
<aop:config>
<!--配置切面类-->
<aop:aspect ref="logAspect">
<!--配置切入点-->
<aop:pointcut id="pointcut"
expression="execution(* com.mcode.xmlaop.CalculatorImpl.*(..))"/>
<!--配置五种通知类型-->
<!--前置通知-->
<aop:before method="beforeMethod"
pointcut="execution(public int com.mcode.xmlaop.CalculatorImpl.*(..))"/>
<!--后置通知-->
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
<!--返回通知-->
<aop:after-returning method="afterReturningMethod" pointcut-ref="pointcut" returning="result"/>
<!--异常通知-->
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointcut" throwing="ex"/>
<!--环绕通知-->
<aop:around method="aroundMethod" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
创建切面类并配置:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* Description: 切面类
*/
@Component //加入ioc容器
public class LogAspect {
// 前置
public void beforeMethod(JoinPoint joinPoint) {
//获取连接点的签名信息
String methodName = joinPoint.getSignature().getName();
//获取目标方法到的实参信息
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
}
// 后置
public void afterMethod(JoinPoint joinPoint) {
//获取连接点的签名信息
String methodName = joinPoint.getSignature().getName();
//获取目标方法到的实参信息
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->后置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
}
// 返回
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名称:" + methodName + ",返回结果:" + result);
}
// 异常
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名称:" + methodName + ",异常信息:" + ex);
}
// 环绕
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String argString = Arrays.toString(args);
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
//目标方法的执行,目标方法的返回值一定要返回给外界调用者,否则会报错
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
}
基于注解的AOP配置
1)准备被代理的目标资源
接口:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
接口实现类:
import org.springframework.stereotype.Component;
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
//为了测试,模拟异常出现
//int a = 1/0;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
2)创建切面类并配置
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect //切面类
@Component //ioc容器
public class LogAspect {
//设置切入点和通知类型
//切入点表达式: execution(访问修饰符 增强方法返回类型 增强方法所在类全路径.方法名称(方法参数))
//通知类型:
// 前置 @Before(value="切入点表达式配置切入点")
//@Before(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
@Before("execution(public int com.mcode.annotationaop.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
}
// 后置 @After()
@After("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->后置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
}
// 返回 @AfterReturning
@AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",
returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名称:" + methodName + ",返回结果:" + result);
}
// 异常 @AfterThrowing 获取到目标方法异常信息
//目标方法出现异常,这个通知执行
@AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing =
"ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名称:" + methodName + ",异常信息:" + ex);
}
// 环绕 @Around()
@Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String argString = Arrays.toString(args);
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
//调用目标方法
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
}
3)添加Spring配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.mcode.annotationaop")
@EnableAspectJAutoProxy //开启AspectJ的自动代理,为目标对象自动生成代理
public class SpringConfig {
}
@EnableAspectAutoJAutoProxy属性:
- proxyTargetClass:表示动态代理实现方式,如果值设置true,表示需要代理类都基于CGLIB来实现;默认情况下值是设置成false,表示如果原类如果定义了接口则通过JDK.Proxy实现否则基于CGLIB来实现。
- exposeProxy:exposeProxy=true Spring会把当前的代理对象存放在ThreadLocal中,可以通过AopContext.currentProxy()获取。
各种通知
- 前置通知:使用@Before注解标识,在被代理的目标方法前执行
- 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用@Around注解标识,使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
各种通知的执行顺序:
- Spring版本5.3.x以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
切入点表达式
1)作用
2)语法细节
- 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限。
- 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
- 例如:*.Hello匹配com.Hello,不匹配com.harvey.Hello
- 在包名的部分,使用“*…”表示包名任意、包的层次深度任意
- 在类名的部分,类名部分整体用*号代替,表示类名任意
- 在类名的部分,可以使用*号代替类名的一部分
- 例如:*Service匹配所有名称以Service结尾的类或接口
- 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分
- 例如:*Operation匹配所有方法名以Operation结尾的方法
- 在方法参数列表部分,使用(…)表示参数列表任意
- 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
- 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
- 例如:execution(public int …Service.(…, int)) 正确
- 例如:execution( int *…Service.(…, int)) 错误
重用切入点表达式
1)声明
@Pointcut("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public void pointCut(){}
2)在同一个切面中使用
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
3)在不同切面中使用
@Before("com.mcode.annotationaop.LogAspect.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
获取通知的相关信息
1)获取连接点信息
获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参。
@Before("execution(public int com.mcode.annotationaop.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
//获取连接点的签名信息
String methodName = joinPoint.getSignature().getName();
//获取目标方法到的实参信息
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名称:" + methodName + ",参数:" + Arrays.toString(args));
}
2)获取目标方法的返回值
@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值
// 返回 @AfterReturning
@AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名称:" + methodName + ",返回结果:" + result);
}
3)获取目标方法的异常
@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常
@AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing ="ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名称:" + methodName + ",异常信息:" + ex);
}
环绕通知
@Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String argString = Arrays.toString(args);
Object result = null;
try {
System.out.println("环绕通知-->目标对象方法执行之前");
//目标方法的执行,目标方法的返回值一定要返回给外界调用者,否则会报错
result = joinPoint.proceed();
System.out.println("环绕通知-->目标对象方法返回值之后");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("环绕通知-->目标对象方法出现异常时");
} finally {
System.out.println("环绕通知-->目标对象方法执行完毕");
}
return result;
}
切面(Aspect)的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
@Order(1) //优先级
public class LogAspect {
}