目录
二、SpringAop
1 场景模拟
要求: 1. 声明计算器接口Calculator,包含加减乘除的抽象方法
2.编写对应的实现类
3.在执行运算之前输出参数,计算之后输出对应的结果
代码如下:
//接口:
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);
}
//实现类
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("add 方法开始了,参数是:" + i + "," + j);
int result = i + j;
System.out.println("add 方法结束了,结果是:" + result);
return result;
}
//测试类
public class Test {
public static void main(String[] args) {
CalculatorImpl calculator = new CalculatorImpl();
calculator.add(1,2);
}
}
思考: 代码问题
1、代码中的日志属于非核心业务,可能会导致核心业务的开发
2、日志分散在各个业务功能方法中,不利于统一维护
为了使代码更加简洁,业务逻辑更加清晰就需要进行解耦
解决方法:
因为公共代码都在方法内部,以前的提取公共代码的方法不再实用,所以需要引入AOP
在引入AOP之前,先介绍一下几种代理模式
2 代理模式
二十三种设计模式中的一种,属于结构型模式。
它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。
调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
2.1 静态代理
接口:
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);
}
实现类:
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;
}
}
静态代理
/**静态代理
* 涉及三个角色:抽象角色:接口
* 目标类:实现类
* 代理类
* 静态代理注意事项
* ①将被代理的目标类声明为成员变量
* ②静态代理和目标类实现相同的接口
* ③非核心业务功能由代理类中的代理方法来实现
* ④通过目标类来实现核心业务逻辑
* 静态代理缺点
* 静态代理实现了解耦,但是由于代码都写死,不具备灵活性。
* 会产生大量重复的代码,功能分散无法进行统一管理
*/
public class StaticProxy implements Calculator{
//将被代理的目标类声明为成员变量
private CalculatorImpl calculator;
public StaticProxy(CalculatorImpl calculator) {
this.calculator = calculator;
}
@Override
public int add(int i, int j) {
// 输出一下传过来的参数
System.out.println("[日志]参数是:" + i+","+j);
// 核心逻辑
int result = calculator.add(i,j);
// 输出运算结果
System.out.println("[日志]计算的结果result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
// 输出一下传过来的参数
System.out.println("[日志]参数是:" + i+","+j);
// 核心逻辑
int result = calculator.sub(i,j);
// 输出运算结果
System.out.println("[日志]计算的结果result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
// 输出一下传过来的参数
System.out.println("[日志]参数是:" + i+","+j);
// 核心逻辑
int result = calculator.mul(i,j);
// 输出运算结果
System.out.println("[日志]计算的结果result = " + result);
return result;
}
@Override
public int div(int i, int j) {
// 输出一下传过来的参数
System.out.println("[日志]参数是:" + i+","+j);
// 核心逻辑
int result = calculator.div(i,j);
// 输出运算结果
System.out.println("[日志]计算的结果result = " + result);
return result;
}
}
测试类:
public class Test {
public static void main(String[] args) {
CalculatorImpl calculator = new CalculatorImpl();
StaticProxy staticProxy = new StaticProxy(calculator);
staticProxy.add(1,2);
staticProxy.sub(1,2);
staticProxy.div(1,2);
}
}
静态代理缺点: 静态代理实现了解耦,但是由于代码都写死,不具备灵活性。 会产生大量重复的代码,功能分散无法进行统一管理
2.2 动态代理
接口:
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);
}
实现类:
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;
}
}
动态代理(非工厂方式):
/**
* jdk动态代理类
* 实现解耦
* 功能可以统一的进行管理
* 缺点:
* 目标类必须实现接口
*/
public class DynamicProxy implements InvocationHandler {
// 代理对象
private Object targetProxy;
public DynamicProxy(Object targetProxy) {
this.targetProxy = targetProxy;
}
/**
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("参数的值是"+ Arrays.toString(args));
// 这里用到了反射
Object invoke = method.invoke(targetProxy, args);
System.out.println("运行的结果是"+invoke);
return invoke;
}
}
测试类:
/**
* 获取动态代理类对象需要三个参数,缺一不可
* ClassLoader loader, 目标类的classLoader对象
* Class<?>[] interfaces, 目标类实现的所有接口的class对象所组成的数组(一个类可以实现多个接口)
* InvocationHandler h 动态代理类(动态代理类的名字)
*/
public class Test {
public static void main(String[] args) {
// 代理的目标类
CalculatorImpl calculator = new CalculatorImpl();
// 1、获取目标类的classLoader对象
Class<? extends CalculatorImpl> aClass = calculator.getClass();
ClassLoader classLoader = aClass.getClassLoader();
// 2、获取目标类实现的所有接口的class对象所组成的数组(一个类可以实现多个接口)
Class<?>[] interfaces = aClass.getInterfaces();
// 3、获取动态代理类(这里需要填入要代理的目标类)
DynamicProxy dynamicProxy = new DynamicProxy(calculator);
// 填入动态代理类所需要的参数,
// 在这里对要代理目标类实现的接口进行选择(一个目标类可以实现多个接口)
Calculator o = (Calculator) Proxy.newProxyInstance(classLoader, interfaces, dynamicProxy);
o.add(1,2);
o.div(4,2);
}
}
动态代理也可以采用工厂模式的代理类:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
/**
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接
口中的抽象方法
*/
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
* 这里是提前使用了
*/
Object result = null;
try {
System.out.println("[动态代理] "+method.getName()+",参数:"+ Arrays.toString(args));
result = method.invoke(target, args);
System.out.println("[动态代理] "+method.getName()+",结果:"+ result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("[动态代理] "+method.getName()+",异常:"+e.getMessage());
} finally {
System.out.println("[动态代理] "+method.getName()+",方法执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
测试类:
public class Test {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator) factory.getProxy();
proxy.div(4,2);
}
}
动态代理缺点:jdk动态代理的目标类必须实现接口,不实现接口的目标类不能被代理
2.3 CGLIB代理
假如有些目标类不实现接口,也想使用代理,那就需要CGLIB代理
CGLIB采用了非常底层的字节码技术,其原理是通过字节码技术为一个目标类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
因为采用的是继承,所以不能对final修饰的类进行代理。
目标类:
public class CalculatorImplCgLib{
public int add(int i, int j) {
// 核心逻辑
int result = i + j;
return result;
}
public int sub(int i, int j) {
// 核心逻辑
int result = i - j;
return result;
}
public int mul(int i, int j) {
// 核心逻辑
int result = i * j;
return result;
}
public int div(int i, int j) {
// 核心逻辑
int result = i / j;
return result;
}
}
代理类:
/**
* 实现MethodInterceptor接口要导的包为import org.springframework.cglib.proxy.MethodInterceptor;
* 注意是cdlib,不可以导错包
*/
public class CgLibProxy implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("参数的值是"+ Arrays.toString(objects));
/**
* 因为这里使用的是父类的方法,所以这里是invokeSuper方法
*/
Object invoke = methodProxy.invokeSuper(o, objects);
System.out.println("运行的结果是"+invoke);
return invoke;
}
}
测试类:
public class Test {
public static void main(String[] args) {
// 获取目标类对象
CalculatorImplCgLib calculatorImplCgLib = new CalculatorImplCgLib();
// 允许没有实现接口的目标类使用代理
Enhancer enhancer = new Enhancer();
// 设置目标类的字节码文件,并把目标类作为父类
enhancer.setSuperclass(calculatorImplCgLib.getClass());
// 设置回调函数(代理类)
enhancer.setCallback(new CgLibProxy());
// 创建真实对象
CalculatorImplCgLib calculator = (CalculatorImplCgLib) enhancer.create();
calculator.add(1,2);
calculator.mul(1,2);
}
}
2.4 三种代理的特点
代理 | |
---|---|
静态代理 | 目标类和代理类要实现相同的接口 目标类要作为一个成员变量存放到代理类里面 实现解耦,但是代理类太多会浪费内存 |
动态代理 | 目标类必须实现InvocationHandler接口 ClassLoader loader, 目标类的classLoader对象 Class<?>[] interfaces, 目标类实现的所有接口的class对象所组成的数组(一个类可以实现多个接口) InvocationHandler h 动态代理类(动态代理类的名字) |
CGLIB代理 | 目标类没有接口 目标类作为父类 CGLIB代理类要实现MethodInterceptor接口 |
3 AOP
3.1 AOP概念及相关术语
3.1.1 概念
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
3.1.2 相关术语
①横切关注点(可以简单粗暴的理解为与业务逻辑无关的代码:逻辑概念)
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
②通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
前置通知:在被代理的目标方法前执行
返回通知:在被代理的目标方法成功结束后执行
异常通知:在被代理的目标方法异常结束后执行
后置通知:在被代理的目标方法最终结束后执行
环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
③切面(类)
封装通知方法的类。切面由一个切入点(pointcut)和一个通知(Advice)组成
④目标(类)
被代理的目标对象。
⑤代理(对象)
向目标对象应用通知之后创建的代理对象。
⑥连接点(逻辑概念)
执行程序中的某个特定的位置,比如类的初始化前、初始化后、类的某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些有边界性质的特定点,称之为“连接点”。
Spring仅能支持方法的连接点
连接点由两个信息确定:1)用方法表示的程序执行点;2)用相对位置表示的方位。
⑦切入点(定位连接点的方式)
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
3.1.3 AOP作用
简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
代码增强:把特定的功能封装到切面类中,哪里需要就哪里引用,引用了切面逻辑的方法就被切面增强了。
3.2 AOP使用注意点
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(有接口的使用的是动态代理)。
AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。
Spring会自动在JDK动态代理和CGlib代理之间转换,
1、目标对象生成了接口,则默认用JDK动态代理<aop:aspectj-autoproxy />;
也可以强制使用CGlib代理(
①在Spring配置中更换后面代码<aop:aspectj-autoproxy proxyt-target-class=”true”/>,
②可以使用注解方式@EnableAspectJAutoProxy(proxyTargetClass = true) );
2、如果目标对象没有实现接口,必须采用CGlib代理;
3.3 基于注解使用AOP
1、加入jar包
<!-- AOP -->
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!--
单元测试使用,如果这里使用的是5.3.1版本的test,那么junit就要使用4.1.2版本及以上
否则报错:SpringJUnit4ClassRunner requires JUnit 4.12 or higher.
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 4.12版本以上-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
接口:(如果有接口,那就是jdk动态代理:没有接口就一定是CGLIB代理)
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);
}
实现类:
@Component
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;
}
}
切面
/**
* Aspect:标记该类为切面
* Order(n):n的值越小,该切面优先级就越高
*
* 前置通知:使用@Before注解标识,在被代理的目标方法前执行
* 返回通知:使用@AfterReturning注解标识
* 异常通知:使用@AfterThrowing注解标识
* 后置通知:使用@After注解标识
* 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,
* 方法执行过程中几种通知的顺序是
* Spring版本5.3以前:前置通知-->目标操作-->后置通知-->返回通知/异常通知
* Spring版本5.3以后;前置通知-->方法执行-->返回通知/异常通知-->后置通知
*
* 切面是由切入点和通知组成
* execution():方法体
* execution( <修饰符模式> ? <返回类型模式> <方法名模式>(<参数模式>) <异常模式> ? )
* 除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。
* eg:execution(int com.lwl.service.impl.CalculatorImpl.*(..))
* 第一个参数代表返回值,若是*就指代所有类型的返回值,如果是int就只能识别返回值为int类型的方法
* 第二个参数代表通知所作用到的包、类、或是更下一级的方法:com.lwl.service.impl.CalculatorImpl.*(..))
* 首先是包:
* com.lwl.service.impl 代表的是作用到的包就impl包,
* com.lwl.service.. 后面的两个句点表示service包和service包的所有子包
* 再下一级就是类:如果是类名,那就是对应的类,如果是*,那就是代表这个包下所有的类
* 再下一级是方法:星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
* 获取通知的相关信息
①获取连接点信息
获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
②获取目标方法的返回值
@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值
③获取目标方法的异常
@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常
*/
@Aspect
@Component
@Order(1)
public class LogAspect {
// 切点表达式复用的声明,声明一个切点表达式
// 在不同的切面中引用这个表达式时,就是包名加方法名
@Pointcut("execution(int com.lwl.service.impl.CalculatorImpl.*(..))")
public void pointCut(){
}
// 前置通知:通知中,需要有切入点表达式
@Before("execution(int com.lwl.service.impl.CalculatorImpl.*(..))")
public void beforeAdvice(JoinPoint joinPoint){
//获取方法名字及参数的值
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("[前置通知]:要执行的方法名字是"+name+",对应参数的值是"+args);
}
// 切点表达式复用
@After("pointCut()")
public void afterAdvice(JoinPoint joinPoint){
//获取方法名字及参数的值
String name = joinPoint.getSignature().getName();
System.out.println("[后置通知]:已经执行的方法名字是"+name);
}
// 切点表达式复用
// 返回通知中有一个返回值,这个值与形参中的名字要一致(都是result)
// 用来将通知方法的某个形参,接收目标方法的返回值
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturnAdvice(JoinPoint joinPoint,Object result){
//获取方法名字及参数的值
String name = joinPoint.getSignature().getName();
System.out.println("[返回通知]:方法"+name+"执行后得到的结果是"+result);
}
// 异常通知中有一个异常值,这个值与形参中的名字要一致(都是e)
// 用来将通知方法的某个形参,接收目标方法的异常
@AfterThrowing(value = "execution(int com.lwl.service.impl.CalculatorImpl.*(..))",throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint,Throwable e){
//获取方法名字及参数的值
String name = joinPoint.getSignature().getName();
System.out.println("[异常通知]:方法"+name+"执行时产生的异常"+e);
}
//环绕通知
// @Around(value = "execution(int com.lwl.service.impl.CalculatorImpl.*(..))")
// public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
获取方法名字及参数的值
// String name = proceedingJoinPoint.getSignature().getName();
// String args = Arrays.toString(proceedingJoinPoint.getArgs());
// Object proceed = null;
// try {
// System.out.println("[前置通知]:要执行的方法名字是"+name+",对应参数的值是"+args);
// proceed = proceedingJoinPoint.proceed();//目标方法的执行
// System.out.println("[返回通知]:方法"+name+"执行后得到的结果是"+proceed);
// } catch (Throwable e) {
// e.printStackTrace();
// System.out.println("[异常通知]:方法"+name+"执行时产生的异常"+e);
// }finally {
// System.out.println("[后置通知]:已经执行的方法名字是"+name);
// }
这里的返回值要和对应方法的结果值一致
// return proceed;
// }
}
Spring配置文件中配置自动代理:
<!-- 扫描包 -->
<context:component-scan base-package="com.lwl"/>
<!--
基于xml配置aop代理:
<aop:aspectj-autoproxy/>:默认使用jdk动态代理,如果扫描不到接口,就使用CGLIB代理
<aop:aspectj-autoproxy proxyt-target-class=”true”/>:强制使用CGLIB代理
如果要使用aop就要配置一下,开启aop的命名空间,开启自动代理(此时使用的就是jdk的动态代理)
xmlns:aop="http://www.springframework.org/schema/aop"
-->
<aop:aspectj-autoproxy/>
测试:
/**
* RunWith:运行的时候用的是什么工具
* ContextConfiguration:标明对应的配置文件
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestAop {
@Resource
private Calculator calculator;
@Test
public void testAdd(){
calculator.add(1,2);
}
@Test
public void testSub(){
calculator.sub(1,2);
}
@Test
public void testDiv(){
calculator.div(1,0);
}
}
3.4 基于XML的AOP配置
<aop:config>
<!--
切面
ref:默认是创建的切面类的类名首字母小写
order:设置切面的优先级,值越小,切面的优先级越高
-->
<aop:aspect ref="logAspectXml" order="1">
<!--切点 设置切点id,以便通知可以找到切点的id-->
<aop:pointcut id="pc" expression="execution(* com.lwl.service.impl.*.*(..))"/>
<aop:before method="beforeAdvice" pointcut-ref="pc"></aop:before>
<aop:after method="afterAdvice" pointcut-ref="pc"></aop:after>
<aop:after-returning method="afterReturnAdvice" returning="result" pointcut-ref="pc"></aop:after-returning>
<aop:after-throwing method="afterThrowingAdvice" throwing="e" pointcut-ref="pc"></aop:after-throwing>
<!--通知:这里因为切入点已经定义好了,所以这里可以直接引用-->
<aop:around method="aroundAdvice" pointcut-ref="pc"></aop:around>
</aop:aspect>
</aop:config>