现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
解决思路
解决核心:解耦。把附加功能从业务功能代码中抽取出来。
困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引
入新的技术。
代理模式
概念
介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时
候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中
剥离出来一一解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够
集中在一起也有利于统一维护。
相关术语
,代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
·目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
场景模拟
-
声明计算器接口Calculator,包含加减乘除的抽象方法
package org.example; public interface Calculator { public int add(int i, int j); public int sub(int i, int j); public int mul(int i, int j); public int div(int i, int j); }
-
写一个实现Calculator业务的实现类
package org.example; public class CalculatorImpl implements Calculator {@Overridepublic int add(int i, int j) {int result = i + j;System.out.println("result=" + result);return result;}@Overridepublic int sub(int i, int j) {int result = i - j;System.out.println("result=" + result);return result;}@Overridepublic int mul(int i, int j) {int result = i * j;System.out.println("result=" + result);return result;}@Overridepublic int div(int i, int j) {int result = i / j;System.out.println("result=" + result);return result;} }
-
写一个实现Calculator业务的带有日志功能的实现类
package org.example; public class CalculatorLogImpl implements Calculator {@Overridepublic int add(int i, int j) {System.out.println("计算开始,i=" + i + "j=" + j);int result = i + j;System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;}@Overridepublic int sub(int i, int j) {System.out.println("计算开始,i=" + i + "j=" + j);int result = i - j;System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;}@Overridepublic int mul(int i, int j) {System.out.println("计算开始,i=" + i + "j=" + j);int result = i * j;System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;}@Overridepublic int div(int i, int j) {System.out.println("计算开始,i=" + i + "j=" + j);int result = i / j;System.out.println("计算结束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;} }
静态代理
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其
他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散
的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。
这就需要使用动态代理技术了。
动态代理
使用java.lang.reflect.Proxy类实现动态代理
官方示例代码
InvocationHandler handler = new MyInvocationHandler(...);
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new class<?>[]{Foo.class}, handler);
创建一个代理工厂类
package org.example; import lombok.val; import javax.print.attribute.standard.JobKOctets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy; public class ProxyFactory { Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxy() {
/* 有三个参数 第一个参数:CLassLoader:加载动态生成代理类的来加载器 第二个参数:CLass[]interfaces:目录对象实现的所有接口cLass类型数组 第三个参数:InvocationHandler:设置代理对象实现目标对象方法的过程*/ ClassLoader cLassLoader = target.getClass().getClassLoader(); Class[] classes = target.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //调用方法前日志 System.out.println("[动态代理][调用前日志]" + method.getName() + "参数:" + args); //调用目标方法 Object result = method.invoke(target, args); //调用方法后日志 System.out.println("[动态代理][调用后日志]" + method.getName() + "参数:" + args); return result; } }; return Proxy.newProxyInstance(cLassLoader, classes, invocationHandler); }
}
编写测试类
@Test
public void calculatorTest(){ ProxyFactory proxyFactory=new ProxyFactory(new CalculatorImpl()); Calculator proxy=(Calculator) proxyFactory.getProxy(); proxy.add(1,1);
}
输出结果
[动态代理][调用前日志]add参数:[Ljava.lang.Object;@7d0587f1
result=2
[动态代理][调用后日志]add参数:[Ljava.lang.Object;@7d0587f1
基于注解的AOP
动态代理分类:JDK动态代理和cglib动态代理
JDK动态代理生成接口实现类代理对象
cglib动态代理继承被代理的目标类,生成子类代理对象,不需要目标类实现接口
- 有接口可以使用JDK动态代理和cblib动态代理
- 没有接口只能使用cblib动态代理
Aspect:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入"被代理的目标类编译得到的字节码
文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了Aspect)中的注解。
使用AOP步骤
-
引入aop相关依赖
<!--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>
-
创建目标资源
-
接口
package com.example.annoAOP; public interface Calculator { public int add(int i, int j); public int sub(int i, int j); public int mul(int i, int j); public int div(int i, int j); }
-
实现类
package com.example.annoAOP; import org.springframework.stereotype.Component; @Component public class CalculatorImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println("result=" + result); return result; } @Override public int sub(int i, int j) { int result = i - j; System.out.println("result=" + result); return result; } @Override public int mul(int i, int j) { int result = i * j; System.out.println("result=" + result); return result; } @Override public int div(int i, int j) { int result = i / j; System.out.println("result=" + result); return result; } }
-
第三步创建切面类
-
创建
bean.xml
,使用AOP约束,开启AOP功能和扫描功能<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 开启组件扫描 --> <context:component-scan base-package="com.example"/> <!--开启aspectj自动代理,为目标对象生成代理--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
-
创建
LogAscept
类,增加一个方法的前置切入点package com.example.annoAOP; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect//表明这是一个AOP文件 @Component//让IoC进行管理 public class LogAspect { //设置切入点和通知类型 //通知类型: // 前置 @Before(value="切入点表达式") // 返回 @AfterReturning // 异常 @AfterThrowing // 后置 @After() // 环绕 @Around() //切入点表达式写法:execution(权限修饰 方法返回值 方法所在全类名.方法名 (参数列表)) //execution:固定语法 //权限修饰:这里写*表示权限修饰符和返回值任意 //方法所在全类名:写*表示任意包名;写*...表示包名任意同时包层次深度任意 //类名用*号代替表示类名任意,部分用*代替,如*Service,表示匹配以Service结尾的列或接口 //方法名:用*号代替表示方法名任意;部分用*代替,如get*,表示匹配以get开头的方法 //参数列表可以使用(...)形式表示参数列表任意 @Before(value = "execution(public int com.example.annoAOP.CalculatorImpl.add (int,int))") public void beforeAdd() { System.out.println("[前置通知][add()]计算开始"); } }
方法表达式写法:
-
创建测试方法
@Test public void testAOPAdd(){ApplicationContext applicationContext=new ClassPathXmlApplicationContext("bean.xml");Calculator calculator=applicationContext.getBean(Calculator.class);calculator.add(1,1); }
-
输出结果
[前置通知][add()]计算开始 result=2
通知类型
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用try.catch.finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
修改LogAspect
类,添加五种通知方法
package com.example.annoAOP; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component; @Aspect//表明这是一个AOP文件
@Component//让IoC进行管理
public class LogAspect { //前置通知 @Before(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))") public void beforeMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[前置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); } //后置通知 @After(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))") public void afterMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); } //返回通知 @AfterReturning(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))", returning = "result") public void afterReturnMethod(JoinPoint joinPoint, Object result) { String MethodName = joinPoint.getSignature().getName(); System.out.println("[返回通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("[返回通知]result=" + result); } //异常通知 @AfterThrowing(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))", throwing = "exp") public void afterThrowing(JoinPoint joinPoint, Throwable exp) { String MethodName = joinPoint.getSignature().getName(); System.out.println("[异常通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println(exp); } //环绕通知 @Around("execution(* com.example.annoAOP.CalculatorImpl.* (..))") //ProceedingJoinPoint继承JoinPoint,比JoinPoint功能更强大,可以更好的调用目标方法 public Object around(ProceedingJoinPoint joinPoint) { 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; }
}
输出结果
环绕通知-目标方法执行前
[前置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@62727399
result=2
[返回通知][CalculatorImpl.MethodName=add()
[返回通知]result=2
[后置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@62727399
环绕通知-目标方法执行后
环绕通知-目标方法执行完成
编写测试方法,使测试方法引发异常
@Test
public void testAOPexp(){ ApplicationContext applicationContext=new ClassPathXmlApplicationContext("bean.xml"); Calculator calculator=applicationContext.getBean(Calculator.class); calculator.div(1,0);
}
运行结果
环绕通知-目标方法执行前
[前置通知][CalculatorImpl.MethodName=div()
Args[]=[Ljava.lang.Object;@4d9ac0b4
[异常通知][CalculatorImpl.MethodName=div()
java.lang.ArithmeticException: / by zero
[后置通知][CalculatorImpl.MethodName=div()
Args[]=[Ljava.lang.Object;@4d9ac0b4
环绕通知-目标方法执行异常
环绕通知-目标方法执行完成[之后是异常报错信息]
重用切入点
-
定义一个切入点
package com.example.annoAOP; @Pointcut(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))") public void pointCut() {}
-
使用切入点
-
内部使用切入点
@After(value = "pointCut") public void afterMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); }
-
外部使用切入点
@After(value = "com.example.annoAOP.pointCut") public void afterMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); }
-
切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order注解可以控制切面的优先级: - @Order(较小的数):优先级高
- @Order(较大的数):优先级低
XML形式配置AOP
-
创建新包
xmlaop
,复制上文接口、实现类、AOP配置类 -
删除
LogAspect
类的@Aspect注解和AOP注解 -
新建
XmlAop.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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 开启组件扫描 --> <context:component-scan base-package="com.example.xmlAOP"/> <!--配置AOP--> <aop:config> <!-- 配置切面类 --> <aop:aspect ref="logAspect"> <!-- 配置切入点 --> <aop:pointcut id="cutpoint" expression="execution(* com.example.xmlAOP.CalculatorImpl.* (..))"/> <!-- 配置方法执行前通知 --> <aop:before method="beforeMethod" pointcut-ref="cutpoint"/> <!-- 配置方法执行后通知 --> <aop:after method="afterMethod" pointcut-ref="cutpoint"/> <!-- 配置方法返回后通知 --> <aop:after-returning method="afterReturnMethod" pointcut-ref="cutpoint" returning="result"/> <!-- 配置环绕通知 --> <aop:around method="around" pointcut-ref="cutpoint"/> <!-- 配置异常通知 --> <aop:after-throwing method="afterThrowing" pointcut-ref="cutpoint" throwing="exp"/> </aop:aspect> </aop:config> </beans>
-
编写测试方法
@Test public void testXML_AOP(){ApplicationContext applicationContext=new ClassPathXmlApplicationContext("XmlAop.xml");//本项目存在两个Calculator,需要注意使用的是哪个Calculator类com.example.xmlAOP.Calculator calculator=applicationContext.getBean(com.example.xmlAOP.Calculator.class);calculator.add(1,1); }
-
输出结果
[前置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@eda25e5
环绕通知-目标方法执行前
result=2
环绕通知-目标方法执行后
环绕通知-目标方法执行完成
[返回通知][CalculatorImpl.MethodName=add()
[返回通知]result=2
[后置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@eda25e5