spring AOP的学习

时间:2023-03-08 23:03:49
spring AOP的学习

1.Spring常用的概念

Joinpoint(连接点):

所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。

Pointcut(切入点):

所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。

Advice(通知/增强):

所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。

通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。

Introduction(引介):

引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。

Target(目标对象):

代理的目标对象。

Weaving(织入):

是指把增强应用到目标对象来创建新的代理对象的过程。

spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

Proxy(代理):

一个类被AOP织入增强后,就产生一个结果代理类。

Aspect(切面):

是切入点和通知(引介)的结合。

2.使用xml配置Spring AOP实现

<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置service -->
<bean id="customerService" class="com.itheima.service.impl.CustomerServiceImpl"></bean>
<!-- 基于xml的aop配置步骤 :要想使用spring的aop,必须导入aop的jar包-->
<!-- 第一步:把通知类交给spring来管理 -->
<bean id="logger" class="com.itheima.utils.Logger"></bean> <!-- 第二步:导入aop名称空间,并且使用aop:config开始aop的配置 -->
<aop:config>
<!-- 定义通用的切入点表达式,如果写在aop:aspct标签外部,则表示所有切面可用 -->
<aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))" id="pt1"/> <!-- 第三步:使用aop:aspect配置切面 id属性用于给切面提供一个唯一标识。ref属性:用于应用通知Bean的id-->
<aop:aspect id="logAdvice" ref="logger">
<!-- 第四步:配置通知的类型,指定增强的方法何时执行。method属性:用于指定增强的方法名称 pointcut属性:用于指定切入点表达式。-->
<!-- 切入点表达式:
关键字:execution(表达式)
表达式写法:
访问修饰符 返回值 包名.包名...类名.方法名(参数列表)
全匹配方式:
public void com.itheima.service.impl.CustomerServiceImpl.saveCustomer()
访问修饰符可以省略
void com.itheima.service.impl.CustomerServiceImpl.saveCustomer()
返回值可以使用通配符,表示任意返回值。通配符是*
* com.itheima.service.impl.CustomerServiceImpl.saveCustomer()
包名可以使用通配符,表示任意包。但是,有几个包就需要写几个*
*.*.*.*.CustomerServiceImpl.saveCustomer()
包名可以使用..表示当前包及其子包
* com..CustomerServiceImpl.saveCustomer()
类名和方法名都可以使用通配符
* com..*.*()
参数列表可以使用具体类型,来表示参数类型
基本类型直接写类型名称:int
引用类型必须是包名.类名。 java.lang.Integer
参数列表可以使用通配符,表示任意参数类型,但是必须有参数
* com..*.*(*)
参数列表可以使用..表示有无参数均可,有参数可以是任意类型
* com..*.*(..)
全通配方式:
* *..*.*(..)
实际开发中,我们一般情况下,我们都是对业务层方法进行增强:
所以写法:* com.itheima.service.impl.*.*(..) -->
<!-- 配置前置通知: 永远在切入点方法执行之前执行
<aop:before method="beforePrintLog" pointcut-ref="pt1"/>-->
<!-- 配置后置通知: 切入点方法正常执行之后执行
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"/>-->
<!-- 配置异常通知: 切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"/>-->
<!-- 配置最终通知:无论切入点方法是否正常执行,它都会在其后面执行
<aop:after method="afterPrintLog" pointcut-ref="pt1"/> --> <!-- 配置环绕通知 -->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"/>
<!-- 定义通用的切入点表达式:如果是写在了aop:aspect标签内部,则表示只有当前切面可用
<aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))" id="pt1"/>-->
</aop:aspect>
</aop:config>
</beans>

环绕通知出现的问题

    /**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而环绕通知里的代码执行了。
* 分析:
* 由动态代理可知,环绕通知指的是invoke方法,并且里面有明确的切入点方法调用。而我们现在的环绕通知没有明确切入点方法调用。
* 解决:
* spring为我们提供了一个接口:ProceedingJoinPoint。该接口可以作为环绕通知的方法参数来使用。
* 在程序运行时,spring框架会为我们提供该接口的实现类,供我们使用。
* 该接口中有一个方法,proceed(),它的作用就等同于method.invoke方法,就是明确调用业务层核心方法(切入点方法)
*
* 环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制通知方法什么时候执行的方式。
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。前置");
rtValue = pjp.proceed();
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。后置");
} catch (Throwable e) {
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。异常");
e.printStackTrace();
}finally{
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。最终");
} return rtValue;
}

3.使用注解配置spring AOP的实现

    1.添加Spring对注解AOP的支持,使用aspectj

<!-- 开启spring对注解AOP的支持 -->
<aop:aspectj-autoproxy/>

    2.实现代码

package com.itheima.utils;

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.stereotype.Component; /**
* 一个用于记录日志的类
* @author zhy
*
*/
@Component("logger")
@Aspect//配置了切面
public class Logger { @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){} /**
* 前置通知
*/
//@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置:Logger中的beforePrintLog方法开始记录日志了。。。。");
} /**
* 后置通知
*/
//@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置:Logger中的afterReturningPrintLog方法开始记录日志了。。。。");
} /**
* 异常通知
*/
//@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常:Logger中的afterThrowingPrintLog方法开始记录日志了。。。。");
} /**
* 最终通知
*/
//@After("pt1()")
public void afterPrintLog(){
System.out.println("最终:Logger中的afterPrintLog方法开始记录日志了。。。。");
} /**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而环绕通知里的代码执行了。
* 分析:
* 由动态代理可知,环绕通知指的是invoke方法,并且里面有明确的切入点方法调用。而我们现在的环绕通知没有明确切入点方法调用。
* 解决:
* spring为我们提供了一个接口:ProceedingJoinPoint。该接口可以作为环绕通知的方法参数来使用。
* 在程序运行时,spring框架会为我们提供该接口的实现类,供我们使用。
* 该接口中有一个方法,proceed(),它的作用就等同于method.invoke方法,就是明确调用业务层核心方法(切入点方法)
*
* 环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制通知方法什么时候执行的方式。
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。前置");
rtValue = pjp.proceed();
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。后置");
} catch (Throwable e) {
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。异常");
e.printStackTrace();
}finally{
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。最终");
} return rtValue;
} }

    3.使用全注解方式实现

package com.dyh.ioc.base;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component; //即使用jdk默认代理模式,AspectJ代理模式是CGLIB代理模式
//如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
//如果目标对象实现了接口,可以强制使用CGLIB实现AOP (此例子我们就是强制使用cglib实现aop)
//如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换 //用于定义配置类,可替换xml配置文件
@Configuration
//开启AspectJ 自动代理模式,如果不填proxyTargetClass=true,默认为false,
@EnableAspectJAutoProxy(proxyTargetClass=true)
//扫描注入类
@ComponentScan(basePackages = "com.dyh.ioc.*")
@Component
@Aspect
public class AopAspectConfiguration {
//声明切入点
//第一个*表示 方法 返回值(例如public int)
//第二个* 表示方法的全限定名(即包名+类名)
//perform表示目标方法参数括号两个.表示任意类型参数
//方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,
//我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么
//execution表示执行的时候触发
@Pointcut("execution(* *(..))")
public void point(){
//该方法就是一个标识方法,为pointcut提供一个依附的地方
} @Before("point()")
public void before(){
System.out.println("Before");
}
@After("point()")
public void after(){
System.out.println("After");
}
}

-----------------------

  spring AOP 和 AspectJ 都可以实现 切面编程 ,因此spring也可以集成AspectJ来实现切面编程,两者没什么太大的关系

  Spring AOP有两种实现方式:

    基于接口的动态代理(Dynamic Proxy)

    基于子类化的CGLIB代理

  区别在于两者实现AOP的底层原理不太一样:

  • Spring AOP: 基于代理(Proxying)
  • AspectJ: 基于字节码操作(Bytecode Manipulation)

-------------------------------------------------------------------------------------------------

AspectJ是一套独立的面向切面框架,

优点:支持静态织入代码,性能更优.

缺点:静态织入丧失了灵活,需要学习新的语法.

Spring Aop是SprinG基于Java Proxy进行的一层封装,Spring中无论是XML(注解)定义的切面信息,都只是对Meta数据的定义,最终核心类:CglibProxy/JdyDynamicAopProxy.

优点:动态织入,产生了新的Class.能做到动态的增加或减少切面.

缺点:动态织入带来的性能会有所下降,如果,控制不当,会产生大量重复Class.造成GC頻繁. CglibAopProxy中就用到了Cglib动态产生字节码,因为:InvocationHandler只能支持Interface.