package com.sf.jin;
import com.sf.annotation.Service.Calculator;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author JinJian
* @version 1.0
* @date 2022/3/12 16:20
*/
public class SpringAopTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring01.xml");
//从IOC容器里面拿到目标对象,如果用类型一定要使用其接口类型,不要用他的本类
Calculator bean = context.getBean(Calculator.class);
bean.add(2,1);
System.out.println(bean);
System.out.println(bean.getClass());
}
}
运行结果:
方法开始运行,参数列表是
方法最终完成,参数列表是
方法正常执行完成,参数列表是
com.sf.annotation.ServiceImpl.CalculatorImpl@7b98f307
class com.sun.proxy.$Proxy16
从测试结果可以看出,我们从IOC容器里面拿到bean的类是class com.sun.proxy.$Proxy16
因为AOP的底层是动态代理,容器里面保存的是它的代理对象,而代理对象和目标对象唯一的联系点就是接口,所以在获取bean的时候使用的类型是接口类型而不是它的本类。
这是在使用动态代理的时候,IOC容器里面保存的是目标类的代理对象,如果没有面向切面编程就按原来的方法去写。
细节二:
如果目标对象没有实现接口,Spring也能帮我们实现动态代理。我们将该类改为不实现接口
package com.sf.annotation.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* @author JinJian
* @version 1.0
* @date 2022/3/10 11:13
*/
@Service
public class CalculatorImpl /*implements Calculator*/ {
// @Override
public int add(int i, int j) {
int result = i + j;
return result;
}
// @Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
// @Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
// @Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
测试方法如下:
package com.sf.jin;
import com.sf.annotation.Service.Calculator;
import com.sf.annotation.ServiceImpl.CalculatorImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author JinJian
* @version 1.0
* @date 2022/3/12 16:20
*/
public class SpringAopTest {
@Test
public void test01(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring01.xml");
CalculatorImpl bean = context.getBean(CalculatorImpl.class);
bean.add(2,1);
System.out.println(bean);
System.out.println(bean.getClass());
}
}
打印结果:
方法开始运行,参数列表是
方法最终完成,参数列表是
方法正常执行完成,参数列表是
com.sf.annotation.ServiceImpl.CalculatorImpl@fd07cbb
class com.sf.annotation.ServiceImpl.CalculatorImpl$$EnhancerByCGLIB$$45cf160e
bean的class是class com.sf.annotation.ServiceImpl.CalculatorImpl$$EnhancerByCGLIB$$45cf160e
这实际上是 CGLIB为我们创建的一个代理对象,实际上还是一个代理对象,这样Spring在我们没有实现接口的时候也可以使用动态代理来实现面向切面编程。
AOP的细节二:通配符
* 和 .. ,这两个通配符的作用在下面的代码块里面都写了
package com.sf.annotation.bean;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Service;
/**
* @author JinJian
* @version 1.0
* @date 2022/3/10 11:16
* 如何让切面类中的通知方法动态地在目标方法的各个位置切入
*
*
*/
@Aspect
@Service
public class LogUtils {
/*
* 还得告诉spring这些方法什么时候开始执行
* @Before 在目标方法运行之前执行
* @AfterReturning 在目标方法正常返回之后执行
* @AfterThrowing 在目标方法抛出异常之后执行
* @After 在目标方法结束之后运行
* @Around 环绕
*
* 切入点表达式的写法:
* 固定格式 execution(访问权限 返回值类型 方法全类名(参数列表))
*
* 通配符
* *:
* 1)匹配一个或多个字符: execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))
* 2)匹配任意一个参数:execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, *))
* 第一个参数是int类型,第二个参数是任意类型
* 3)只能匹配一层路径: execution(public int com.sf.annotation.ServiceImpl.*.*(int, *))
* 4)权限位置不能写*
* ..:
* 1)匹配任意多个和任意类型的参数 execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(..))
* 2)匹配任意多层路径:execution(public int com.sf.annotation..CalculatorImpl.*(..))
*
* 记住两种表达式:
* 1)最精确 execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))
* 2)最模糊 execution(* *.*(..))
*
* 切入点表达式还能使用逻辑修饰符去连接
* && || !
* execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int)) && execution(* *.*(..))
* 两个表达式都需要满足
* */
//目标方法执行之前执行
@Before("execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))")
public static void logStart(){
System.out.println("方法开始运行,参数列表是");
}
//目标方法正常执行完成之后执行
@AfterReturning("execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))")
public static void logReturn(){
System.out.println("方法正常执行完成,参数列表是");
}
//目标方法出现异常的时候执行
@AfterThrowing("execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))")
public static void logException(){
System.out.println("方法异常,参数列表是");
}
//目标方法结束的时候执行
@After(("execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))"))
public static void logEnd(){
System.out.println("方法最终完成,参数列表是");
}
}
AOP的细节三:通知方法的执行顺序
/* * 通知方法的执行顺序 * try{ * @Before * method.invoke(obj,args) * @AfterRetrning }catch (){ @AfterThrowing }finally { @After } 方法正常执行: @Before====method.invoke(obj,args)====@After====@AfterRetrning 方法异常执行:@Before====method.invoke(obj,args)====@After===@AfterThrowing * */
本来应该时结束方法放在最后的,看起来有些别扭, 但是就是这个规定的。
AOP细节四:在通知方法运行时拿到目标方法的详细信息
细节四;在通知方法运行时拿到目标方法的详细信息 *
1、在通知方法的参数列表上面写一个参数JoinPoint joinPoint,该参数封装了目标方法的详细信息 * 2、用哪个参数来接受返回值,以及异常情况,只需要在切入点表达式里面加入一些参数如returning = "res" throwing="exception"等 * @AfterReturning(value = "execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))",returning = "res" ) * @AfterThrowing(value = "execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))", throwing="exception")
package com.sf.annotation.bean;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Service;
import java.util.Arrays;
/**
* @author JinJian
* @version 1.0
* @date 2022/3/10 11:16
* 如何让切面类中的通知方法动态地在目标方法的各个位置切入
*
*
*/
@Aspect
@Service
public class LogUtils {
/*
* 通知方法的执行顺序
* try{
* @Before
* method.invoke(obj,args)
* @AfterRetrning
}catch (){
@AfterThrowing
}finally {
@After
}
方法正常执行:@Before====method.invoke(obj,args)====@After====@AfterRetrning
方法异常执行:@Before====method.invoke(obj,args)====@After===@AfterThrowing
* */
/*
* 还得告诉spring这些方法什么时候开始执行
* @Before 在目标方法运行之前执行
* @AfterReturning 在目标方法正常返回之后执行
* @AfterThrowing 在目标方法抛出异常之后执行
* @After 在目标方法结束之后运行
* @Around 环绕
*
* 切入点表达式的写法:
* 固定格式 execution(访问权限 返回值类型 方法全类名(参数列表))
*
* 通配符
* *:
* 1)匹配一个或多个字符: execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))
* 2)匹配任意一个参数:execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, *))
* 第一个参数是int类型,第二个参数是任意类型
* 3)只能匹配一层路径: execution(public int com.sf.annotation.ServiceImpl.*.*(int, *))
* 4)权限位置不能写*
* ..:
* 1)匹配任意多个和任意类型的参数 execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(..))
* 2)匹配任意多层路径:execution(public int com.sf.annotation..CalculatorImpl.*(..))
*
* 记住两种表达式:
* 1)最精确 execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))
* 2)最模糊 execution(* *.*(..))
*
* 切入点表达式还能使用逻辑修饰符去连接
* && || !
* execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int)) && execution(* *.*(..))
* 两个表达式都需要满足
* */
/*
* 细节四;在通知方法运行时拿到目标方法的详细信息
* 1、在通知方法的参数列表上面写一个参数JoinPoint joinPoint,该参数封装了目标方法的详细信息
* 2、用哪个参数来接受返回值,以及异常情况,只需要在切入点表达式里面加入一些参数如returning = "res" throwing="exception"等
* @AfterReturning(value = "execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))",returning = "res" )
* @AfterThrowing(value = "execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))", throwing="exception")
*
* */
//目标方法执行之前执行
@Before("execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))")
public static void logStart(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法开始运行,参数列表是"+ Arrays.asList(args));
}
//目标方法正常执行完成之后执行
@AfterReturning(value = "execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))",returning = "res" )
public static void logReturn(JoinPoint joinPoint,Object res){
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法正常执行完成,结果为"+res);
}
//目标方法出现异常的时候执行
@AfterThrowing(value = "execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))", throwing="exception")
public static void logException(JoinPoint joinPoint,Exception exception){
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法异常,异常信息是" + exception);
}
//目标方法结束的时候执行
@After(("execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))"))
public static void logEnd(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法最终完成");
}
}
细节5:Spring对通知方法不严格,唯一需要注意的是通知方法的参数列表不能乱写
细节6:通知方法上面的切入点表达式,也可以写在一个方法上面,这样可以解耦,需要修改切入点表达式时只需要修改该方法上面的切入点表达式。
抽取可重用的切入点表达式:
1、随便声明一个没有实现的返回void的空方法
2、给方法标注上@Pointcut注解,切入点表达式写在注解后面的括号里面
3、 直接在@Before等注解后面写上该方法名即可,需要修改切入点表达式的时候只需要修改该方法上面的@Pointcut注解里面的切入点表达式即可
细节7环绕通知:
/*
* 环绕通知:是Spring里面最强大的通知
* 环绕通知其实就是一个动态代理:
*
* try {
* //前置通知
method.invoke(obj,args);
//返回通知
} catch (Exception e) {
e.printStackTrace();
//异常通知
} finally {
//后置通知
}
环绕通知就是上面四种通知四合一的通知
环绕通知和普通通知放在一起的执行顺序:
环绕前置====普通前置====目标方法执行====环绕返回/异常====环绕后置===普通后置===普通返回/异常
* */
@Around(("execution(public int com.sf.annotation.ServiceImpl.CalculatorImpl.*(int, int))"))
public static Object MyAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
String name = pjp.getSignature().getName();
Object proceed = null;
try {
//@Before
System.out.println("[环绕前置通知]"+name+"方法开始了");
//就是通过反射调用目标方法,就是method.invoke(obj,args);这个方法一定要执行
proceed = pjp.proceed(args);
//@AfterReturning
System.out.println("[环绕返回通知]"+name+"方法正常返回,返回值是"+proceed);
} catch (Exception e) {
//@AfterThrowing
System.out.println("[环绕异常通知]"+name+"方法返回异常,异常是"+e);
//这里的异常要抛出去,这样普通通知才能拿到,不然普通通知拿不到异常
throw new RuntimeException(e);
} finally {
//@After
System.out.println("[环绕后置通知]"+name+"方法结束");
}
//反射调用后的返回值也一定要返回出去
return proceed;
}