代理模式:属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。代理方式可以解决附加功能代码干扰核心代码和不方便统一维护的问题。
AOP(Aspect Oriented Programming):面向切面编程,是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。使用AOP,可以在不修改原来代码的基础上添加新功能。
AOP主要应用场景:
AOP术语名词
横切关注点:从每个方法中抽取出来的同一类非核心业务。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
通知(增强)方法:每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法
连接点 joinpoint:指那些被拦截到的点。在 Spring 中,指可以被动态代理拦截目标类的方法
切入点 pointcut:定位连接点的方式,或者可以理解成被选中的连接点。是一个表达式,比如 execution(* com.spring.service.impl.*.*(..))
。符合条件的每个方法都是一个具体的连接点。
切面 aspect:切入点和通知的结合。是一个类。
目标 target:被代理的目标对象。
代理 proxy:向目标对象应用通知之后创建的代理对象。
织入 weave:指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
关系梳理:
底层技术组成:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.6</version>
</dependency>
<aop:aspectj-autoproxy />
@EnableAspectJAutoProxy
注解@Aspect // 表示是切面类
@Component
public class MyAdvice {
@Pointcut("execution(public void com.aop.controller.HelloController.hello())") // 切入点
public void pointcut() {}
@Before("pointcut()") // 也可使用 value = 切入点表达式 的方式
public void before(JoinPoint joinPoint) {
System.out.println("前置通知触发");
}
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("后置通知触发");
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null;
try {
System.out.println("环绕前置");
result = proceedingJoinPoint.proceed();
System.out.println("环绕后置");
} catch (Throwable t) {
System.out.println("环绕异常");
throw new RuntimeException(t);
}
return result;
}
}
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知(增强)方法。
@Before
:在被代理的目标方法前执行@AfterReturning
:在被代理的目标方法成功结束后执行,异常终止不执行@AfterThrowing
:在被代理的目标方法异常结束后执行,异常终止才执行@After
:在被代理的目标方法最终结束后执行,无论是否异常@Around
:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置JoinPoint 接口 :需要获取方法签名、传入的实参等信息时,可以在通知方法声明 JoinPoint 类型的形参。
getSignature()
:获取方法签名对象,包含一个方法的全部声明信息,调用该对象 getName()
方法即可获取方法名称getArgs()
:获取参数列表环绕通知特别点:使用 ProceedingJoinPoint
接口作为形参,调用 process()
方法表示执行被代理方法
方法返回值 :在 @AfterReturning
注解的方法中,获取目标方法的返回值
returning
:通过注解的该属性,指定返回值在方法形参中的名称Object
异常对象值 :在 @AfterThrowing
注解的方法中,获取目标方法的异常对象值
throwing
:通过注解的该属性,指定异常值在方法形参中的名称Throwable
AOP切点表达式(Pointcut Expression)是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。
以下按顺序指明位置对应功能
execution(表达式)
固定开头*
开头即可com.aop.controller
com.aop.*
com.aop..
含当前层*..
HelloController
*
*Controller
*..*
(String,int)
,无参 ()
(..)
任意参数,可有可无(..String..)
有一个参数是 String 类型在第 2 节中即用了切点表达式的提取,使用 @Pointcut(切点表达式)
需要添加到一个无参数无返回值方法上即可,引用时写 全限定类名.方法名()
即可,相同类间可省略全限定类名。
@Pointcut("execution(public void com.aop.controller.HelloController.hello())") // 切入点
public void pointcut() {}
@Before("pointcut()") // 也可使用 value = 切入点表达式 的方式
public void before(JoinPoint joinPoint) {
System.out.println("前置通知触发");
}
【建议】:将切点表达式统一存储到一个类中进行集中管理和维护
使用 @Order
注解,值越小,前置通知越先执行,后置通知越晚执行,默认值 Integer.MAX_VALUE
。
就像在前置通知的最后放了一面镜子表示被代理类的执行,先从队列最前面的前置通知执行,直到镜子执行完。镜子中镜像对应着后置通知顺序,在前置通知中顺序最靠前的将最后执行。
实际意义:当有多个切面嵌套执行时,要慎重考虑切面的执行顺序。
情景1:
测试:根据实现类类型获取bean,无法获取,且输出如下;如改成根据接口获取,将可以获取到,且获取到的对象是 jdk动态代理对象
Bean named 'helloController' is expected to be of type 'com.aop.controller.HelloController' but was actually of type 'jdk.proxy2.$Proxy34'
解释:应用了切面后,真正放在IOC容器中的是代理类的对象,目标类并没有放到 IoC容器中
情景2:
测试:根据实现类类型直接获取bean,可以获取到,获取到 cglib 代理对象,
class org.xzx.controller.HelloController$$SpringCGLIB$$0
解释:因为 cglib 代理实现基于继承,故可以满足 instance of HelloController
条件