一、动态代理
AOP 即 Aspect Oriented Programming面向切面编程,它是基于面向对象编程之上的新的编程思想,是指将某段代码动态的切入到指定方法的指定位置并运行。
新建一个maven项目spring-bean-aop,导入依赖
代码语言:javascript复制<properties>
<spring-version>5.3.13</spring-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
在util包定义一个计算器接口类,定义四个方法加减乘除,增加一个实现类,实现加减乘除四个方法
代码语言:javascript复制public interface Calculator {
int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);
}
在util.impl包中增加实现类
代码语言:javascript复制@Component
public class AppleCalculator implements Calculator {
@Override
public int add(int x, int y) {
int result = x y;
return result;
}
@Override
public int sub(int x, int y) {
int result = x - y;
return result;
}
@Override
public int mul(int x, int y) {
int result = x * y;
return result;
}
@Override
public int div(int x, int y) {
int result = x / y;
return result;
}
}
增加application.xml,将组件注入容器中
代码语言:javascript复制<?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:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.3.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.citi">
</context:component-scan>
</beans>
生成Spring Test测试类AppleCalculatorTest
代码语言:javascript复制@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:application.xml")
public class AppleCalculatorTest {
@Autowired
private AppleCalculator appleCalculator;
@Test
public void add() {
}
@Test
public void sub() {
}
@Test
public void mul() {
}
@Test
public void div() {
}
}
给每个方法运行前后增加日志输出,如何在不改变代码的前提下完成?
动态代理方式解决
首先创建一个代理CalculatorProxy,包含一个方法,可以返回代理对象,给代理对象加一些方法,通过代理对象执行目标方法
创建一个代理,静态方法getProxy用户获取Calculator的代理对象,
代码语言:javascript复制public class CalculatorProxy {
public static Calculator getProxy(Calculator calculator){
// 方法执行器
InvocationHandler invocationHandler = (Object proxy, Method method, Object[] args) -> {
System.out.println("动态代理执行目标方法:" method.getName());
// 反射执行目标方法
Object result = method.invoke(calculator, args);
return result;
};
// 类接口
Class<?>[] interfaces = calculator.getClass().getInterfaces();
// 类加载器
ClassLoader classLoader = calculator.getClass().getClassLoader();
Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
return (Calculator) proxyInstance;
}
}
该方法需要传入被代理的对象,通过newProxyInstance()创建一个代理,再强转为目标类返回,其中参数invocationHandler为目标方法执行器,通过invoke方法执行目标方法并使用Object接收保存后返回目标方法执行后的返回值, intefaces为被代理类的类接口列表,classLoader为被代理类的类加载器
在测试类中通过getProxy方法获取Calculator的代理对象,使用代理对象执行方法
代码语言:javascript复制@Test
public void add() {
Calculator calculator = CalculatorProxy.getProxy(appleCalculator);
calculator.add(2,1);
}
根据控制台打印可以看出,代理类成功执行了add方法
那么要实现在方法执行前后增加日志记录,就可以修改代理类CalculatorProxy的getProxy方法,在调用invoke方法前后增加日志输出,包括对方法执行异常时的处理
代码语言:javascript复制public class CalculatorProxy {
public static Calculator getProxy(Calculator calculator){
// 方法执行器
InvocationHandler invocationHandler = (Object proxy, Method method, Object[] args) -> {
Object result = null;
try {
// 反射执行目标方法
// System.out.println("动态代理执行目标方法:" method.getName());
System.out.println(method.getName() "方法开始执行,参数为:" Arrays.toString(args));
result = method.invoke(calculator, args);
System.out.println(method.getName() "方法执行结束,结果为:" result);
} catch (Exception e){
System.out.println(method.getName() "方法执行异常,异常信息:" e.getCause());
} finally {
System.out.println(method.getName() "方法最终执行结束");
return result;
}
};
// 类接口
Class<?>[] interfaces = calculator.getClass().getInterfaces();
// 类加载器
ClassLoader classLoader = calculator.getClass().getClassLoader();
Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
return (Calculator) proxyInstance;
}
}
再次执行测试类
定义异常情况
代码语言:javascript复制@Test
public void div() {
Calculator calculator = CalculatorProxy.getProxy(appleCalculator);
calculator.div(2,0);
}
执行异常情况的测试方法div,输出了异常的原因
如果目标对象没有实现任何接口,是无法创建代理对象的,而Spring AOP可以解决这个问题,Spring AOP 底层就是动态代理
二、AOP
1. 切面(Aspect) 被抽取出来的公共模块,可以用来会横切多个对象多个方法。Aspect切面可以看成Pointcut切点和Advice通知的结合,一个切面可以由多个切点和通知组成。
2. 连接点(Join point) 指程序运行过程中所执行的方法。在Spring AOP中,一个连接点总代表一个方法的执行。
3. 切点(Pointcut) 切点用于定义要对哪些连接点或者方法的执行前后(Join point)进行拦截。
4. 通知(Advice) 指要在连接点(Join Point) 上执行的动作,即增强的逻辑。比如权限校验和、日志记录,事务控制等。通知有各种类型,包括Around、Before、After、After returning、After throwing。
如何使用AOP
首先增加Spring AOP的相关依赖
代码语言:javascript复制<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring-version}</version>
</dependency>
新增aspect包,增加一个LogAspect切面类,@Component注解表示要将切面类注入到Spring容器中,@Aspect表示这是一个切面类,针对div方法定义了异常通知,对add方法定义了前置通知,后置通知返回通知和环绕通知
代码语言:javascript复制@Component
@Aspect
public class LogAspect {
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")
public void addPointCut(){
}
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")
public void divPointCut(){
}
@Before("addPointCut()")
public void logStart(JoinPoint joinPoint ){
System.out.println("方法调用前的输出");
System.out.println("方法名:" joinPoint.getSignature());
System.out.println("方法参数:" Arrays.toString(joinPoint.getArgs()));
}
@After("addPointCut()")
public void logEnd(){
System.out.println("方法调用后的输出");
}
@AfterReturning("addPointCut()")
public void logReturn(){
System.out.println("方法输出返回后输出");
}
@AfterThrowing("divPointCut()")
public void logException(){
System.out.println("方法抛出异常后输出");
}
@Around("addPointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知:方法执行前");
Object o = joinPoint.proceed();
System.out.println("环绕通知:方法执行后输出" o.toString());
return o;
}
}
编写测试类,注意注入的是Calculator接口类,不是AppleCalculator实现类
代码语言:javascript复制@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:application.xml")
public class AppleCalculatorTest {
@Autowired
private Calculator calculator;
@Test
public void add() {
calculator.add(2,1);
}
@Test
public void div() {
calculator.div(2,0);
}
}
增加xml配置,开启AOP
代码语言:javascript复制<!--开启AOP-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
执行测试,div的异常通知可以正常输出,add方法的前置后置返回通知和环绕通知也都可以正常输出
1.实现接口的情况下使用jdk动态代理创建动态代理对象
执行测试方法
代码语言:javascript复制@Test
public void testGetBean(){
System.out.println(calculator);
System.out.println(calculator.getClass());
}
从Spring容器中获取目标对象一定要自动导入接口类,不要用本类,AOP底层是动态代理,容器中保存的组件是目标对象的代理对象
如果AppleCalculator不实现接口的情况下,使用cglib创建动态代理,修改AppleCalculator
代码语言:javascript复制@Component
public class AppleCalculator {
public int add(int x, int y) {
int result = x y;
return result;
}
public int sub(int x, int y) {
int result = x - y;
return result;
}
public int mul(int x, int y) {
int result = x * y;
return result;
}
public int div(int x, int y) {
int result = x / y;
return result;
}
}
测试类中注入
代码语言:javascript复制@Autowired
private AppleCalculator calculator;
执行testGetBean()测试方法,可以看出是CGLIB创建的代理对象
2.切入点表达式
固定格式: execution(访问权限符 返回值类型 方法全类名(参数表))
通配符: *: 1)匹配一个或者多个字符:execution(public int com.citi.util.impl.AppleCar.(int, int)) 2)匹配任意一个参数:第一个是int类型,第二个参数任意类型;(匹配两个参数) execution(public int com.citi.util.impl.AppleCa.(int, *)) 3)*只能匹配一层路径 4)权限位置不能使用*,权限位置不写就行或者public(可选)
**..*: 1)匹配任意多个参数,任意类型参数 2)匹配任意多层路径:execution(public int com.citi..AppleCa.*(..));
记住两种: 最精确的:execution(public int com.citi.util.impl.AppleCalculator.add(int,int)) 最模糊的:execution(* .(..)),不要使用这种最模糊的切点表达式
"&&”、“||”、“!" &&:我们要切入的位置同时满足这两个表达式 execution(public int com.citi.util.impl.AppleCalculator.add(..))&&execution(* .(int,int))
||:满足任意一个表达式即可 execution(public int com.citi..AppleC*.(..))&&execution( .(int,int))
!:只要不是这个位置都切入!execution(public int com.citi..AppleC*.*(..))
3.通知方法的执行顺序
没有环绕通知的情况下,使用try/catch包裹:
代码语言:javascript复制try{
@Before
// 方法执行
method.invoke(obj,args);
// 正常执行后返回
@AfterReturning
}catch(){
// 异常执行
@AfterThrowing
}finally{
@After
}
正常执行:@Before前置通知->@AfterReturning正常返回->@After后置通知
异常执行:@Before前置通知->@After后置通知->@AfterThrowing异常通知
4.获取目标方法的详细信息
需要参数JoinPoint类,封装了目标方法的详细信息 获取方法参数,joinPoint.getSignature())可以获取方法名,joinPoint.getArgs()可以获取参数列表
代码语言:javascript复制@Before("addPointCut()")
public void logStart(JoinPoint joinPoint ){
System.out.println(this.getClass().getName() "切面类运行-目标方法调用前的输出-@Before前置通知");
System.out.println("目标方法名:" joinPoint.getSignature());
System.out.println("目标方法参数:" Arrays.toString(joinPoint.getArgs()));
}
执行测试方法中的add()
获取返回值,需要定义一个变量来接收,方法在入参中,为了能接收各种类型的参数因此最好定义为Object类型
代码语言:javascript复制@AfterReturning(value = "addPointCut()", returning = "result")
public void logReturn(Object result){
System.out.println(this.getClass().getName() "切面类运行-目标方法返回:" result ",@AfterReturning正常返回");
}
异常同样
代码语言:javascript复制@AfterThrowing(value = "divPointCut()", throwing = "e")
public void logException(Exception e){
System.out.println(this.getClass().getName() "切面类运行-目标方法抛出异常:" e);
}
异常也可以指定传入哪种异常,方法执行爆出的异常符合传入的异常才会打印出异常信息,否则不会,所以传入的异常类型要尽量是Exception,如果可以确切的知道方法爆出的异常,可以指定具体异常来接收, Result同样也可以指定具体类型来接收返回值
执行add方法和div方法的测试
5.切入点表达式
代码语言:javascript复制@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")
public void addPointCut(){
}
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")
public void divPointCut(){
}
@Pointcut注解定一个切点表达式,方法内部不用实现任何功能
6.环绕通知
其实就是一个动态代理
代码语言:javascript复制@Around("addPointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
System.out.println("环绕通知-方法执行前");
Object o = null;
try {
System.out.println("环绕通知-前置通知," methodName "方法开始执行");
// 反射执行目标方法
o = joinPoint.proceed();
System.out.println("环绕通知-返回通知," methodName "返回值:" o.toString());
} catch (Exception e){
System.out.println("环绕通知-异常通知," methodName "异常信息为:" e);
} finally {
System.out.println("环绕通知-后置通知," methodName "方法执行结束");
}
return o;
}
将普通通知的注解注释,执行add方法
将切入点表达式改为divPointcut,执行div方法的测试
普通通知和环绕通知同时存在时的执行顺序,将环绕通知的切点表达式改为addPointcut,将普通通知方法的注解注释取消,执行add方法的测试
7.有多个切面类的情况下的运行顺序
新增切面类
代码语言:javascript复制@Component
@Aspect
public class VerifyAspect {
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")
public void addPointCut(){
}
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")
public void divPointCut(){
}
@Before("addPointCut()")
public void verifyStart(JoinPoint joinPoint ){
System.out.println(this.getClass().getName() "切面类执行-目标方法调用前的输出-@Before");
//System.out.println("目标方法名:" joinPoint.getSignature());
//System.out.println("目标方法参数:" Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(value = "addPointCut()",returning = "result")
public void verifyReturn(Object result){
System.out.println(this.getClass().getName() "切面类执行-方法执行后输出返回值:" result ",@AfterReturning正常返回");
}
@After(value = "addPointCut()")
public void verifyEnd(JoinPoint joinPoint){
System.out.println(this.getClass().getName() "切面类执行-目标方法调用结束,@After后置通知");
}
@AfterThrowing(value = "divPointCut()",throwing = "e")
public void verifyException(Exception e){
System.out.println(this.getClass().getName() "切面类执行-方法抛出异常:" e);
}
}
LogAspect暂时注销环绕通知
代码语言:javascript复制@Component
@Aspect
public class LogAspect {
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")
public void addPointCut(){
}
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")
public void divPointCut(){
}
@Before("addPointCut()")
public void logStart(JoinPoint joinPoint ){
System.out.println(this.getClass().getName() "切面类运行-目标方法调用前的输出-@Before前置通知");
//System.out.println("目标方法名:" joinPoint.getSignature());
//System.out.println("目标方法参数:" Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(value = "addPointCut()", returning = "result")
public void logReturn(Object result){
System.out.println(this.getClass().getName() "切面类运行-目标方法返回:" result ",@AfterReturning正常返回");
}
@After("addPointCut()")
public void logEnd(JoinPoint joinPoint){
System.out.println(this.getClass().getName() "方法调用结束,@After后置通知");
}
@AfterThrowing(value = "addPointCut()", throwing = "e")
public void logException(Exception e){
System.out.println(this.getClass().getName() "切面类运行-目标方法抛出异常:" e);
}
//@Around("addPointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
System.out.println("环绕通知-方法执行前");
Object o = null;
try {
System.out.println("环绕通知-前置通知," methodName "方法开始执行");
// 反射执行目标方法
o = joinPoint.proceed();
System.out.println("环绕通知-返回通知," methodName "返回值:" o.toString());
} catch (Exception e){
System.out.println("环绕通知-异常通知," methodName "异常信息为:" e);
} finally {
System.out.println("环绕通知-后置通知," methodName "方法执行结束");
}
return o;
}
}
运行add方法的测试方法
执行顺序,先进后出
切面类先后执行是根据切面类名字首字母排序,讲VerifyAspect改为AerifyAspect,再次执行add的测试方法
要想改变按照切面类字母顺序执行切面类,可以在类上增加@Order注解,数值越小优先级越高,给LogAspect加上@Order(1),默认是2147483647,再次执行add方法的测试方法
有环绕通知的情况下,将LogAspect的环绕通知注解注释取消,并在输出中加上类名标识
环绕通知加在哪个切面,那个切面执行环绕,环绕通知优先执行,环绕通知中执行方法
AOP使用场景:
- AOP加日志保存到数据库
- AOP作权限验证
- AOP作安全检查
- AOP作事务控制三、XML配置实现AOP 将LogAspect和AerifiyAspect类上及方法上的注解全部注释。 基于注解的AOP实现步骤:
- 将目标类和切面类上添加@Component,交个Spring容器管理
- 切面类添加@Aspect,表明这是一个切面类
- 切面类方法中添加通知方法的注解
- xml配置中开启AOP功能,并使用component-scan扫描所有包
<?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:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.3.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.citi">
</context:component-scan>
<!--xml AOP配置-->
<!--配置目标类和切面类, @Component-->
<bean id="logAspect" class="com.citi.aspect.LogAspect"></bean>
<bean id="aerifyAspect" class="com.citi.aspect.AerifyAspect"></bean>
<bean id="appleCalculator" class="com.citi.util.impl.AppleCalculator"></bean>
<!--AOP配置,通知方法配置-->
<aop:config>
<!--指定切面,@Aspect, ref属性引用容器中已存在的Bean,order属性决定运行先后次序顺序-->
<aop:aspect ref="logAspect" order="1">
<!--配置切点表达式@Pointcut-->
<aop:pointcut id="addPointcut" expression="execution(public * com.citi.util.impl.AppleCalculator.add(int, int))"></aop:pointcut>
<aop:pointcut id="divPointcut" expression="execution(public * com.citi.util.impl.AppleCalculator.div(int, int))"></aop:pointcut>
<!--配置通知方法@Before,@After...-->
<!--logStart方法中有参数jointPoint,它是Spring知道的变量,不用配置在xml属性中-->
<aop:before method="logStart" pointcut-ref="addPointcut"></aop:before>
<!--logReturn方法中有参数 Object result,Spring不知道这个变量,xml中使用returning属性来接收-->
<aop:after-returning method="logReturn" returning="result" pointcut-ref="addPointcut"></aop:after-returning>
<aop:after-throwing method="logStart" pointcut-ref="addPointcut"></aop:after-throwing>
<aop:after method="logStart" pointcut-ref="addPointcut"></aop:after>
</aop:aspect>
<aop:aspect ref="aerifyAspect" order="3">
<!--配置切点表达式@Pointcut-->
<aop:pointcut id="addPointcut" expression="execution(public * com.citi.util.impl.AppleCalculator.add(int, int))"></aop:pointcut>
<!--配置通知方法@Before,@After...-->
<!--logStart方法中有参数jointPoint,它是Spring知道的变量,不用配置在xml属性中-->
<aop:before method="verifyStart" pointcut-ref="addPointcut"></aop:before>
<!--logReturn方法中有参数 Object result,Spring不知道这个变量,xml中使用returning属性来接收-->
<aop:after-returning method="verifyReturn" returning="result" pointcut-ref="addPointcut"></aop:after-returning>
<aop:after-throwing method="verifyException" throwing="e" pointcut-ref="addPointcut"></aop:after-throwing>
<aop:after method="verifyEnd" pointcut-ref="addPointcut"></aop:after>
</aop:aspect>
</aop:config>
<!--开启AOP-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
复制代码
环绕前置通知的顺序和普通普通前置通知的顺序于xml配置先后顺序有关,如果先配置<aop:around>就先打印环绕通知,如果<aop:before>在前就先打印普通前置通知
执行测试
至此,Spring AOP 告一段落