前言
前面荔枝已经梳理了Spring框架中的IOC部分的知识,接下来荔枝继续梳理Spring框架的另一大重点:AOP面向切面编程。在这篇文章中,荔枝会着重弄清楚AOP的概念并对实现AOP的两种方式进行梳理,同时荔枝也会相应给出代码样例。毕竟荔枝始终觉得只有文字的描述是苍白无力的,有代码其实理解起来会更快哈哈哈。
文章目录
前言
一、代理模式和AOP概念
1.1 代理模式
1.2 AOP概念
二、基于注解方式实现AOP
2.1 动态代理分类和依赖引入
2.2 切入点表达式以及五种通知类型
2.3 重用切入点表达式
2.4 切面的优先级
三、基于XML方式实现AOP
总结
一、代理模式和AOP概念
在开发中我们常常考虑将不同功能的程序分离出来降低系统的耦合度,也就是解耦操作。也就是说,通常在针对一些附加功能的时候我们需要一种方法把子类中重复的代码注入到父类中。
1.1 代理模式
代理模式是设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来一一解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用,同时让附加功能能够集中在一起也有利于统一维护。代理主要分为两种:静态代理和动态代理。
静态代理
静态代理解决了高耦合的问题,通过一个代理类构造方法获取执行类的对象并调用其add方法实现接口功能,并在前后加上附加功能比如日志文件。
package com.crj.aop;public class CalculatorStaticProxy implements Calaulator{//将代理目标对象传递进来private Calaulator calaulator;public CalculatorStaticProxy(Calaulator calaulator) {this.calaulator = calaulator;}@Overridepublic int add(int i, int j) {//输出日志System.out.println("[日志]add方法开始,参数是:i="+i+"、j="+j);calaulator.add(i,j);//输出日志System.out.println("[日志]add方法结束");return 0;}
}
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得声明多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现,这就需要使用动态代理技术了。
动态代理
Java种支持使用Proxy.newProxyInstance(classLoader,interfaces,invocationHandler)来实现接口的动态代理,需要三个参数:需代理类的类加载器、需代理类的实现接口数组和重写的代理方法。其中第三个参数实现目标的代理方法借助InvocationHandler类种的invoke方法来实现。
动态代理类
package com.crj.spring6.aop;import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;//实现动态代理
public class ProxyFactory {//目标对象private Object target;public ProxyFactory(Object target) {this.target = target;}//返回代理对象public Object getProxy(){/*** 参数说明* Proxy.newProxyInstance()方法* 1.类加载器ClassLoader loader 加载动态生成代理类的类加载器* 2.Class[] interfaces 目标对象实现的所有接口的class类型数组* 3.IncocationHandler 设置代理对象实现目标对象方法的过程*///类加载器ClassLoader classLoader = target.getClass().getClassLoader();//目标对象实现的所有接口的class类型数组Class<?>[] interfaces = target.getClass().getInterfaces();//使用匿名内部类来实现接口InvocationHandler invocationHandler = new InvocationHandler(){@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {/*** 参数* 第一个参数:代理对象* 第二个参数:需要重写的目标对象的方法* 第三个参数:method方法里面的参数*///方法调用之前输出日志System.out.println("[动态代理日志]"+method.getName()+"参数:"+ Arrays.toString(args));//调用目标方法Object result = method.invoke(target, args);//方法调用之后输出日志System.out.println("[动态代理日志]"+method.getName()+"结果:"+ result);return result;}};return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);}}
测试类
package com.crj.spring6.aop;public class TestCal {public static void main(String[] args) {//创建代理对象ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());//获取代理对象Calaulator proxy = (Calaulator)proxyFactory.getProxy();proxy.add(1,2);}
}
1.2 AOP概念
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率,使程序是松耦合的。
相关术语
横切关注点
分散在各个模块中解决同一类问题的,或者说是从每个方法种抽取出来的同类非核心也无就乘坐横切关注点,比如:日志管理、用户验证等。换句话说,有几个附加功能就有几个横切关注点。
通知(增强)
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法,有五种通知类型:
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行
- 异常通知:在被代理的目标方法异常结束后执行
- 后置通知:在被代理的目标方法最终结束后执行
- 环绕通知:使用try..catch.finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
切面
封装通知方法的类
目标
被代理的目标对象
代理
向目标对象应用通知之后创建的代理对象
连接点
允许使用通知的地方
切入点
定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。如果把连接点看作数据库中的记录,那么切入点就是查询记录的SQL语句。Spring的AOP技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。
二、基于注解方式实现AOP
我们知晓,AOP面向切面编程的底层也是通过动态代理来实现的,所以对于动态代理我们除了了解基本的代理分类之外还需要明确AOP种如何使用动态代理的方式来实现AOP。
2.1 动态代理分类和依赖引入
AOP动态代理有两种:JDK动态代理和CGlib动态代理,分别对应有接口和无接口的动态代理。JDK动态代理会生成接口实现类的代理对象,而CGlib动态代理是通过继承被代理对象并重写的方式实现的。
pom.xml文件
项目的环境中即pom.xml文件中需要引入aop和aspects依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.crj</groupId><artifactId>spring6</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules><module>spring-first</module><module>spring6-ioc-xml</module><module>spring6-ioc-annotation</module><module>spring6-aop</module></modules><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><!-- 此处引入spring依赖--><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.2</version></dependency><!-- 引入junit依赖--><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.6.3</version></dependency><!-- 引入log4j2的依赖--><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.19.0</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j2-impl</artifactId><version>2.19.0</version></dependency><dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>2.1.1</version></dependency><!--spring aop依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>6.0.2</version></dependency><!--spring aspects依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.0.2</version></dependency></dependencies>
</project>
除了环境总配置之外,还需要在项目中resources目录下的xml文件中配置开启组件扫描和aspectj自动代理。
<?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/aop http://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--开启组件扫描--><context:component-scan base-package="com.crj.spring6.aop.annotationAop"></context:component-scan><!--开启aspectj自动代理,为目标对象生成代理--><aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
2.2 切入点表达式以及五种通知类型
在配置完环境依赖后,我们需要在切面类中定义通知的类型并设置切入点,Java中同个定义了几个注解来实现设置通知类型:
- 前置通知 @Before(value = “切入点表达式”)
- 返回通知 @AfterReturning
- 异常通知 @AfterThrowing
- 后置通知 @After()
- 环绕通知 @Around()
需要注意切入点表达式的结构:execution(权限修饰符 方法返回值类型 切入点方法的全类名.方法名(方法参数列表))
前置通知
package com.crj.spring6.aop.annotationAop;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 {//设置切入点和通知类型//通知类型:// 前置通知 @Before(value = “切入点表达式”)// 返回通知 @AfterReturning// 异常通知 @AfterThrowing// 后置通知 @After()// 环绕通知 @Around()//前置通知@Before(value = "execution(public int com.crj.spring6.aop.annotationAop.CalculatorImpl.add(..))")public void beforeMethod(JoinPoint joinPoint){String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();System.out.println("Logger-.->前置通知,方法名:"+methodName+",参数:"+ Arrays.toString(args));}
}
返回通知
返回通知中我们在注解中可以通过returning属性获取执行目标增强方法后的返回值。
package com.crj.spring6.aop.annotationAop;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 {//返回通知@AfterReturning(value = "execution(* com.crj.spring6.aop.annotationAop.CalculatorImpl.*(..))",returning = "result")public void afterReturnMethods(JoinPoint joinPoint,Object result){//增强方法的名字String methodName = joinPoint.getSignature().getName();//增强方法的参数Object[] args = joinPoint.getArgs();//增强方法的返回值 resultSystem.out.println("Logger-.->返回通知,方法名:"+methodName+",参数:"+ Arrays.toString(args)+",目标方法的返回值:"+result);}}
异常通知
在异常通知中可以通过throwing属性获得目标增强方法执行异常抛出的信息。
package com.crj.spring6.aop.annotationAop;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 {//异常通知@AfterThrowing(value = "execution(* com.crj.spring6.aop.annotationAop.CalculatorImpl.*(..))",throwing = "ex")public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex){String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();System.out.println("Logger-.->异常通知,方法名:"+methodName+",参数:"+ Arrays.toString(args)+",异常方法的信息:"+ex);}
}
环绕通知
环绕通知需要注意的是@Around注解下的方法参数类型选择的是ProceedingJoinPoint,借助该类型对象的proceed的方法来调用目标函数。
package com.crj.spring6.aop.annotationAop;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 {//环绕通知@Around(value = "execution(* com.crj.spring6.aop.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 throwable){System.out.println("环绕通知——>目标方法出现异常之后执行");}finally {System.out.println("环绕通知——>后置通知");}return result;}
}
2.3 重用切入点表达式
通过上述的四个通知例子我们发现,切入点表达式除了在一些特殊的场景需求下,其余的情况总是一样的,为了避免代码的冗余,我们可以通过重用切入点表达式来提高程序的可复用性。
//前置通知
@Before(value = "Pointcut()")
public void beforeMethod(JoinPoint joinPoint){String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();System.out.println("Logger-.->前置通知,方法名:"+methodName+",参数:"+ Arrays.toString(args));
} @Pointcut(value = "execution(* com.crj.spring6.aop.annotationAop.CalculatorImpl.*(..))")
public void pointCut(){}
注意:上述是在同一个切面中的定义,在不同的切面中的话需要在通知的切入点表达式加上重用函数的全类名
2.4 切面的优先级
相同目标方法上同时存在多个切面时,可以通过切面的优先级控制切面的内外嵌套顺序:
- 优先级高的切面:外面
- 优先级低的切面:里面
主要是通过使用@Order注解来设定切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
三、基于XML方式实现AOP
这里只需要将前面的注解部分去掉,并通过一个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/aop http://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--开启组件扫描--><context:component-scan base-package="com.crj.spring6.aop.xmlaop"></context:component-scan><!--配置AOP的五种通知类型--><aop:config><!--配置切面类--><aop:aspect ref="logAspect"><!--配置切入点--><aop:pointcut id="pointcut" expression="execution(* com.crj.spring6.aop.annotationAop.CalculatorImpl.*(..))"/><!--配置五种通知类型--><!--前置--><aop:before method="beforeMethod" pointcut-ref="pointcut"></aop:before><!--后置--><aop:after method="aroundMethod" pointcut-ref="pointcut"></aop:after><!--返回--><aop:after-returning method="afterReturnMethods" returning="result" pointcut-ref="pointcut"></aop:after-returning><!--异常--><aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointcut"></aop:after-throwing><!--环绕--><aop:around method="aroundMethod" pointcut-ref="pointcut"></aop:around></aop:aspect></aop:config>
</beans>
总结
哈哈,学到这里荔枝总算把最最重要的部分学完了,马上就可以上手项目实操了,真实有点迫不及待呢哈哈哈哈哈哈~~~希望荔枝梳理的会对有需要的小伙伴有帮助,接下来荔枝可能会继续学习和梳理并总结产出,在暑假结束后荔枝也会对项目进行总结和复盘,希望那时的荔枝会离大佬更进一步哈哈哈哈哈哈。
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~