目录
一、SpringAOP
-
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。他是一种可以在不修改原来的核心代码的情况下给程序动态统一进行增强的一种技术。
-
SpringAOP: 批量对Spring容器中bean的方法做增强,并且这种增强不会与原来方法中的代码耦合。
-
实现AOP的方式有两种,一种是基于注解的aop,另一种是基于xml的aop
二、基于注解的AOP
2.1 导入依赖
需要添加SpringIOC相关依赖和AOP相关依赖。
<!--SpringIOC相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<!--AOP相关依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
2.2 AOP实现
2.2.1 开启组件扫描和AOP注解支持,将相关bean注入到容器中
<?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">
<!--
AOP的注意事项:
切面类和目标类都需要交给IOC容器管理
切面类必须通过@Aspect注解标识为一个切面
在Spring的配置文件中设置<aop:aspectj-autoproxy />开启基于注解的AOP
-->
<context:component-scan base-package="com.lx.aop.annotation"></context:component-scan>
<!--开启基于注解的aop-->
<aop:aspectj-autoproxy />
</beans>
2.2.1 创建切面类
实现一个日志功能,在方法执行的前后添加功能
@Component
@Aspect //将当前组件标识为切面
public class LoggerAspect {
@Pointcut("execution(* com.lx.aop.annotation.impl.CalcuatorImpl.*(..))")
public void pointCut(){}
//@Before("execution(public int com.lx.aop.annotation.impl.CalcuatorImpl.add(int,int))")
//@Before("execution(* com.lx.aop.annotation.impl.CalcuatorImpl.*(..))")
@Before("pointCut()")
public void beforeAdvice(JoinPoint joinPoint){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
//获取连接点所对应的方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args));
}
@After("pointCut()")
public void afterAdvice(JoinPoint joinPoint){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",执行完毕");
}
/**
I
*在返回通知中若要获取目标对象方法的返回值
*只需要通过@AfterReturning注解的returning属性
*就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数
**/
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint,Object result){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",结果:"+result);
}
/*
* 在异常通知中若要获取目标对象方法的异常
只需要通过AfterThrowing注解的throwing属性
就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数
* */
@AfterThrowing(value = "pointCut()",throwing = "ex")
public void afterThrowAdvice(JoinPoint joinPoint,Exception ex){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",异常:"+ex);
}
@Around("pointCut()")
//环绕通知的方法的返回值-定要和目标对象方法的返回值一致
public Object aroundAdvice(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("环绕通知--->前置通知");
//表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知--->返回通知");
}catch (Throwable throwable){
System.out.println("环绕通知--->异常通知");
}finally {
System.out.println("环绕通知--->后置通知");
}
return result;
}
}
下面将会介绍上述代码的各个功能的描述
2.3 AOP核心概念
- Joinpoint(连接点):所谓连接点是指那些可以被增强到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
- Pointcut(切入点):所谓切入点是指被增强的连接点(方法)
- Advice(通知/ 增强):所谓通知是指具体增强的代码
- Target(目标对象):被增强的对象就是目标对象
- Aspect(切面):是切入点和通知(引介)的结合
- Proxy (代理):一个类被 AOP 增强后,就产生一个结果代理类
2.4 切点确定
- 切点表达式
可以使用切点表达式来表示要对哪些方法进行增强。
写法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))- 访问修饰符可以省略,大部分情况下省略
- 返回值类型、包名、类名、方法名可以使用星号* 代表任意
- 包名与类名之间一个点 . 代表当前包下的类,两个点 … 表示当前包及其子包下的类
- 参数列表可以使用两个点 … 表示任意个数,任意类型的参数列表
例如:
execution(* com.lx.service.*.*(..)) 表示com.lx.service包下任意类,方法名任意,参数列表任意,返回值类型任意 execution(* com.lx.service..*.*(..)) 表示com.lx.service包及其子包下任意类,方法名任意,参数列表任意,返回值类型任意 execution(* com.lx.service.*.*()) 表示com.lx.service包下任意类,方法名任意,要求方法不能有参数,返回值类型任意 execution(* com.lx.service.*.delete*(..)) 表示com.lx.service包下任意类,要求方法不能有参数,返回值类型任意,方法名要求已delete开头
- 切点函数@annotation
我们也可以在要增强的方法上加上注解。然后使用@annotation来表示对加了什么注解的方法进行增强。
写法:@annotation(注解的全类名)
例如:
定义注解如下
给需要增强的方法增加注解@Target({ElementType.METHOD})//该注解可以加在方法上 @Retention(RetentionPolicy.RUNTIME) public @interface InvokeLog { }
切面类中使用@annotation来确定要增强的方法@Service public class PhoneService { @InvokeLog public void deleteAll(){ System.out.println("PhoneService中deleteAll的核心代码"); } }
@Component @Aspect public class MyAspect { // 用Pointcut注解中的属性来指定对哪些方法进行增强 @Pointcut("@annotation(com.lx.aspect.InvokeLog)") public void pt(){} /* 用@Before注解来指定该方法中是增强的代码,并且是在被增强方法执行前执行的 @Before的属性写上加了@Pointcut注解的方法: 方法名() */ @Before("pt()") public void methodbefore(){ System.out.println("方法被调用了"); } }
2.5 通知分类
在切面中,需要通过指定的注解将方法标识为通知方法
- @Before:前置通知,在目标对象方法执行之前执行
- @After:后置通知,在目标对象方法的finally子句中执行
- @AfterReturning:返回通知,在目标对象方法返回值之
- @AfterThrowing:异常通知,在目标对象方法的catch 字句中执行
- @Around:环绕通知,围绕着目标方法执行
2.6 获取被增强方法相关信息
我们实际对方法进行增强时往往还需要获取到被增强代码的相关信息,比如方法名,参数,返回值,异常对象等。
我们可以在除了环绕通知外的所有通知方法中增加一个JoinPoint类型的参数。这个参数封装了被增强方法的相关信息。我们可以通过这个参数获取到除了异常对象和返回值之外的所有信息。
@Before("pt()")
public void methodbefore(JoinPoint jp){
Object[] args = jp.getArgs();//方法调用时传入的参数
Object target = jp.getTarget();//被代理对象
MethodSignature signature = (MethodSignature) jp.getSignature();//获取被被增强方法签名封装的对象
System.out.println("Before方法被调用了");
}
案例:
需求:要求让所有service包下类的所有方法被调用前都输出全类名,方法名,以及调用时传入的参数
@Component
@Aspect
public class PrintLogAspect {
//对哪些方法增强
@Pointcut("execution(* com.lx.service..*.*(..))")
public void pt(){}
//怎么增强
@Before("pt()")
public void printLog(JoinPoint joinPoint){
//输出 被增强的方法所在的类名 方法名 调用时传入的参数 joinPoint.getSignature().getName() joinPoint.getArgs()
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//类名
String className = signature.getDeclaringTypeName();
//方法名
String methodName = signature.getName();
//调用时传入的参数
Object[] args = joinPoint.getArgs();
System.out.println(className+"=="+methodName+"======"+ Arrays.toString(args));
}
}
如果需要获取被增强方法中的异常对象或者返回值则需要在方法参数上增加一个对应类型的参数,并且使用注解的属性进行配置。这样Spring会把你想获取的数据赋值给对应的方法参数。
@AfterReturning(value = "pt()",returning = "ret")//使用returning属性指定了把目标方法返回值赋值给下面方法的参数ret
public void AfterReturning(JoinPoint jp,Object ret){
System.out.println("AfterReturning方法被调用了");
}
@AfterThrowing(value = "pt()",throwing = "t")//使用throwing属性指定了把出现的异常对象赋值给下面方法的参数t
public void AfterThrowing(JoinPoint jp,Throwable t){
System.out.println("AfterReturning方法被调用了");
}
相信你肯定觉得上面的获取方式特别的麻烦难以理解。就可以使用下面这种万能的方法。
直接在环绕通知方法中增加一个ProceedingJoinPoint类型的参数。这个参数封装了被增强方法的相关信息。
该参数的proceed()方法被调用相当于被增强方法被执行,调用后的返回值就相当于被增强方法的返回值。
例如:
//环绕通知的方法的返回值-定要和目标对象方法的返回值一致
public Object aroundAdvice(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("环绕通知--->前置通知");
//表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知--->返回通知");
}catch (Throwable throwable){
System.out.println("环绕通知--->异常通知");
}finally {
System.out.println("环绕通知--->后置通知");
}
return result;
}
2.7 小结
-
切入点表达式:设置在标识通知的注解的value属性中
execution(public int com.lx.aop.annotation.impl.CalcuatorImpl.add(int,int))
execution(* com.lx.aop.annotation.impl.CalcuatorImpl.*(..))
第一个*
表示任意的访问修饰符和返回值类型
第二个*
表示类中任意的方法
…表示任意的参数列表
类的地方也可以使用*
,表示包下所有的类 -
重用切入点表达式
@Pointcut声明一个公共的切入点表达式@Pointcut("execution(* com.lx.aop.annotation.impl.CalcuatorImpl.*(..))") public void pointCut(){}
使用方式:
@Before("pointCut()")
-
获取连接点的信息
- 在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点所对应方法的信息
- 获取连接点所对应方法的签名信息
Signature signature = joinPoint. getsignature();
- 获取连接点所对应方法的参数
object[] args = joinPoint. getArgs();
-
切面的优先级
可以通过@order注解的value属性设置优先级,默认值Integer的最大值
@order注解的value属性值越小,优先级越高
三、基于xml的AOP
3.1 定义切面类
@Component
public class LoggerAspect {
public void beforeAdvice(JoinPoint joinPoint){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
//获取连接点所对应的方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect,方法:"+signature.getName()+",参数:"+ Arrays.toString(args));
}
public void afterAdvice(JoinPoint joinPoint){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",执行完毕");
}
public void afterReturningAdvice(JoinPoint joinPoint,Object result){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",结果:"+result);
}
public void afterThrowAdvice(JoinPoint joinPoint,Exception ex){
//获取连接点所对应的方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect,方法:"+signature.getName()+",异常:"+ex);
}
public Object aroundAdvice(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("环绕通知--->前置通知");
//表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知--->返回通知");
}catch (Throwable throwable){
System.out.println("环绕通知--->异常通知");
}finally {
System.out.println("环绕通知--->后置通知");
}
return result;
}
}
3.2 配置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: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.lx.aop.xml"></context:component-scan>
<!--xml配置aop-->
<aop:config>
<!--设置一个公共的切入点表达式-->
<aop:pointcut id="pt" expression="execution(* com.lx.aop.xml.impl.CalcuatorImpl.*(..))"/>
<!--将ioc容器中的某个bean设置为切面-->
<aop:aspect ref="loggerAspect">
<aop:before method="beforeAdvice" pointcut-ref="pt"></aop:before>
<aop:after method="afterAdvice" pointcut-ref="pt"></aop:after>
<aop:after-returning method="afterReturningAdvice" pointcut-ref="pt" returning="result"></aop:after-returning>
<aop:after-throwing method="afterThrowAdvice" pointcut-ref="pt" throwing="ex"></aop:after-throwing>
<aop:around method="aroundAdvice" pointcut-ref="pt"></aop:around>
</aop:aspect>
<aop:aspect ref="validateAspect" order="1">
<aop:before method="beforeAdvice" pointcut-ref="pt"></aop:before>
</aop:aspect>
</aop:config>
</beans>
四、多切面顺序问题
在实际项目中我们可能会存在配置了多个切面的情况。这种情况下我们很可能需要控制切面的顺序。
我们在默认情况下Spring有它自己的排序规则。(按照类名排序)
默认排序规则往往不符合我们的要求,我们需要进行特殊控制。
如果是注解方式配置的AOP可以在切面类上加@Order注解来控制顺序。@Order中的属性越小优先级越高。
如果是XML方式配置的AOP,可以通过调整配置顺序来控制。
例如:
@Component
@Aspect
@Order(2)
public class APrintLogAspect {
//省略无关代码
}
@Component
@Aspect
@Order(1)
public class CryptAspect {
//省略无关代码
}
五、AOP原理-动态代理
实际上Spring的AOP其实底层就是使用动态代理来完成的。并且使用了两种动态代理分别是JDK的动态代理和Cglib动态代理。
所以我们接下去来学习下这两种动态代理,理解下它们的不同点。
5.1 JDK动态代理
JDK的动态代理使用的java.lang.reflect.Proxy这个类来进行实现的。要求被代理(被增强)的类需要实现了接口。并且JDK动态代理也只能对接口中的方法进行增强。
public static void main(String[] args) {
AIControllerImpl aiController = new AIControllerImpl();
//使用动态代理增强getAnswer方法
//1.JDK动态代理
//获取类加载器
ClassLoader cl = Demo.class.getClassLoader();
//被代理类所实现接口的字节码对象数组
Class<?>[] interfaces = AIControllerImpl.class.getInterfaces();
AIController proxy = (AIController) Proxy.newProxyInstance(cl, interfaces, new InvocationHandler() {
//使用代理对象的方法时 会调用到invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy 是代理对象
//method 是当前被调用的方法封装的Method对象
//args 是调用方法时传入的参数
//调用被代理对象的对应方法
//判断 当前调用的是否是getAnswer方法
if(method.getName().equals("getAnswer")){
System.out.println("增强");
}
Object ret = method.invoke(aiController, args);
return ret;
}
});
String answer = proxy.getAnswer("hello world");
System.out.println(answer);
}
5.2 Cglib动态代理
使用的是org.springframework.cglib.proxy.Enhancer类进行实现的。
public class CglibDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//设置父类的字节码对象
enhancer.setSuperclass(AIControllerImpl.class);
enhancer.setCallback(new MethodInterceptor() {
//使用代理对象执行方法是都会调用到intercept方法
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//判断当前调用的方法是不是getAnswer方法 如果是进行增强
if ("getAnswer".equals(method.getName())){
System.out.println("被增强了");
}
//调用父类中对应的方法
Object ret = methodProxy.invokeSuper(o, objects);
return ret;
}
});
//生成代理对象
AIControllerImpl proxy = (AIControllerImpl) enhancer.create();
// System.out.println(proxy.getAnswer("你好吗?"));
System.out.println(proxy.fortuneTelling("你好吗?"));
}
}
5.3 小总结
- JDK动态代理要求被代理(被增强)的类必须要实现接口,生成的代理对象相当于是被代理对象的兄弟。
- Cglib的动态代理不要求被代理(被增强)的类要实现接口,生成的代理对象相当于被代理对象的子类对象。
Spring的AOP默认情况下优先使用的是JDK的动态代理,如果使用不了JDK的动态代理才会使用Cglib的动态代理。
5.4 切换默认动态代理方式
有的时候我们需要修改AOP的代理方式。
我们可以使用以下方式修改:
如果我们是采用注解方式配置AOP的话:
设置aop:aspectj-autoproxy标签的proxy-target-class属性为true,代理方式就会修改成Cglib
<aop:aspectj-autoproxy proxy-target-class="true"/>
如果我们是采用xml方式配置AOP的话:
设置aop:config标签的proxy-target-class属性为true,代理方式就会修改成Cglib
<aop:config proxy-target-class="true">
</aop:config>