静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现,JDK本身就支持动态代理,这是反射技术的一部分。
public class TestStaticProxy { @Test public void testProxy(){ //1. 创建一个CalculatorPureImpl对象:被代理者对象 Calculator calculator = new CalculatorPureImpl(); //2. 创建代理者对象:使用动态代理技术创建 //动态代理技术分为两种: //1. JDK内置的动态代理技术:要求被代理者必须实现接口,它的底层其实就是使用反射创建接口的实现类对象 //2. CGLIB的动态代理技术(需要引入依赖):被代理者可以不实现接口,它的底层其实是创建被代理类的子类对象 //我们现在使用JDK的动态代理技术 Class<? extends Calculator> clazz = calculator.getClass(); //参数一:用于定义代理对象的类加载器:和被代理者的类加载器是同一个类加载器 ClassLoader classLoader = clazz.getClassLoader(); //参数二:表示要被代理的接口的集合 //获取参数二的方式一: 获取被代理者实现的所有接口 Class<?>[] interfaces = clazz.getInterfaces(); //获取参数二的方式二: 自己编写一个数组,将你想要代理的接口放里面new Class[]{Calculator.class} //参数三:InvocationHandler接口的实现类对象或者是匿名内部类对象,在它里面真正编写代理逻辑 Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //这个方法中就编写具体的代理逻辑 //invoke()方法什么时候会执行:当代理对象调用任何方法的时候都会执行invoke() //proxy参数:就是调用方法的代理对象 //method参数:代理对象调用的那个方法 //args参数:代理对象调用的那个方法的参数 //目标:在被代理者的add、sub、mul、div方法前后添加日志打印 //1. 判断方法名是否是add、sub、mul、div String methodName = method.getName(); if (methodName.equals("add") || methodName.equals("sub") || methodName.equals("mul") || methodName.equals("div")) { //先执行前置打印日志 System.out.println("[日志] "+methodName+" 方法开始了,参数是:" + args[0] + "," + args[1]); //执行被代理者的核心逻辑 Object result = method.invoke(calculator, args); //执行后置打印日志 System.out.println("[日志] "+methodName+" 方法结束了,结果是:" + result); return result; } //返回值是返回给代理对象调用的那个方法 //对于不需要进行代理的方法,就执行被代理者原本的方法 return method.invoke(calculator,args); } }); int result = calculatorProxy.sub(2, 3); System.out.println("在外面打印代理对象执行运算的结果:" + result); } }
使用代理模式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KroIFJ3I-1640004690352)(./img/003.jpg)]
①代理:又称之为代理者,用于将非核心luoji剥离出来,封装这些非核心逻辑类、方法、对象
②目标:用于核心逻辑,并把这些代理者非核心逻辑用在类、对象方法上
简化代码:把固定重复的代码抽取出来,让其方法更专注于自己的核心功能,提高内聚性
代码增强:抽取出来的方法,放在切面类里面,看哪里需要就往哪里套,被套用的切面楼价就被切面类增强了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRh3PdmJ-1640004690360)(./img/004.jpg)]
从每个方法中抽取同一类非核心业务
每个横向切点都需写一个方法来实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDBspPJD-1640004690363)(.\img\005.png)]
封装通知的方法类
被代理目标对象
向目标对象应用通知之后创建的代理对象
各个方法中可以被增强或修改的点
方法中真正要去配置增强或者配置修改的地方
<dependencies> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- spring-aspects会帮我们传递过来aspectjweaver --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency> <!--spring整合Junit--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.1</version> </dependency> </dependencies>
public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); }
import org.springframework.stereotype.Component; @Component public class CalculatorPureImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; //int num = 10 / 0; 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; } }
/** * Aspect注解:指定一个切面类 * Component注解: 对这个切面类进行IOC * * 注解AOP的关键点: * 1. 一定要在配置文件中加上<aop:aspectj-autoproxy />表示允许自动代理 * 2. 切面类一定要加上Aspect注解,并且切面类一定要进行IOC * 3. 其它的类该进行IOC和依赖注入的就一定要进行IOC和依赖注入 * 4. 通知上一定要指定切入点(怎么使用切入点表达式描述切入点又是一个难点) */ @Aspect @Component public class LogAspect { @Before("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))") public void printLogBeforeCore(){ System.out.println("[前置通知]在方法执行之前打印日志..."); } @AfterReturning("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))") public void printLogAfterReturning(){ System.out.println("[返回通知]在方法执行成功之后打印日志..."); } @AfterThrowing("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))") public void printLogAfterThrowing(){ System.out.println("[AOP异常通知]在方法抛出异常之后打印日志..."); } @After("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))") public void printLogFinallyEnd(){ System.out.println("[AOP后置通知]在方法最终结束之后打印日志..."); } }
<!--包扫描--> <context:component-scan base-package="com.wwb"/> <!--允许注解AOP--> <aop:aspectj-autoproxy />
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:spring-application.xml") //@ContextConfiguration这个注解通常与@RunWith(SpringJUnit4ClassRunner.class)联合使用用来测试 public class TestAop { @Autowired private Calculator calculator; @Test public void testAdd(){ //调用CalculatorPureImpl对象的add()方法 System.out.println("返回值是:"+calculator.add(1, 2)); } }
[AOP前置通知] 方法开始了 方法内部 result = 12 [AOP返回通知] 方法成功返回了 [AOP后置通知] 方法最终结束了 方法外部 add = 12
要点1:JoinPoint接口通过getSignature()方法获取目标方法的签名
要点2:通过目标方法签名对象获取方法名
要点3:通过JoinPoint对象获取外界调用目标方法时传入的实参列表组成的数组
// 1.通过JoinPoint对象获取目标方法签名对象 // 方法的签名:一个方法的全部声明信息 Signature signature = joinPoint.getSignature(); // 2.通过方法的签名对象获取目标方法的详细信息 String methodName = signature.getName(); System.out.println("methodName = " + methodName); // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表 Object[] args = joinPoint.getArgs(); // 4.由于数组直接打印看不到具体数据,所以转换为List集合 List<Object> argList = Arrays.asList(args);
只有在AfterReturning返回通知中才能够获取目标方法的返回值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M4wzQwaw-1640004690366)(./img/006.jpg)]
易于维护,一处修改,处处生效。
@Pointcut("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))") public void calculatorPointCut(){ }
通过方法名引入
@Before("calculatorPointCut()") public void printLogBeforeCore(JoinPoint joinPoint){
@Before("com.wwb.pointcut.wwbPointCut.calculatorPointCut()") public void printLogBeforeCore(JoinPoint joinPoint){}
public class wwbPointCut { @Pointcut("execution(int com.wwb.component.CalculatorPureImpl.*(int,int))") public void calculatorPointCut(){ } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqEevW1E-1640004690368)(.\img\007.jpg)]
环绕通知对应整个try…catch…finally结构,可以在目标方法的各个部位进行套用代理逻辑,它能够真正介入并改变目标方法的执行
<!--包扫描--> <context:component-scan base-package="com.wwb"/> <!-- 使用xml方式配置AOP: 1. 切面: 封装非核心逻辑的那个类,非核心逻辑就是封装在切面的方法中 2. 通知: 将非核心逻辑套在核心逻辑上进行执行 3. 切入点: 核心逻辑 --> <aop:config> <!-- 1. 切面: ref属性就是指定作为切面的那个对象的id,order属性表示切面的优先级 --> <aop:aspect id="myAspect" ref="logAspect"> <!--2. 通知--> <!--配置前置通知--> <aop:before method="printLogBeforeCore" pointcut-ref="calculatorPoint"/> <!--配置返回通知--> <aop:after-returning method="printLogAfterReturning" pointcut-ref="calculatorPoint" returning="result"/> <!--配置异常通知--> <aop:after-throwing method="printLogAfterThrowing" pointcut-ref="calculatorPoint" throwing="throwable"/> <!--配置后置通知--> <aop:after method="printLogFinallyEnd" pointcut-ref="calculatorPoint"/> <!--配置环绕通知--> <aop:around method="printLogAround" pointcut-ref="calculatorPoint"/> <!--3. 切入点--> <aop:pointcut id="calculatorPoint" expression="execution(* com.wwb.component.CalculatorPureImpl.*(..))"/> </aop:aspect> </aop:config>
测试
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:spring-application.xml") public class TestAop { @Autowired private Calculator calculator; @Test public void testAdd(){ //调用CalculatorPureImpl对象的add()方法 System.out.println("调用完目标方法之后获取返回值是:"+calculator.sub(5, 3)); } }