Spring核心机制之 AOP
文章目录
1. Spring AOP简介
众所周知,Spring的两大核心机制为:
- IoC
- AOP (Aspect Oriented Programming,面向切面编程)
本篇博客就来细细品一下AOP的实现以及它的原理;
AOP意味面向切面编程,我们之前一直使用的是OOP(面向对象编程):
- OOP:将程序中所有参与模块都抽象化为对象,然后通过对象之间的相互调用来完成业务需求;
- AOP:是对OOP的一个补充,是在另外一个维度上抽象出对象,具体是指程序运行时动态的将非业务代码切入到业务代码中,从而实现代码的解耦和;
AOP的优点
- 降低模块耦合度
- 使系统容易扩展
- 延迟设计决定:使用AOP,设计师可以推迟为将来的需求作决定,因为需求作为独立的方面很容易实现
- 更好的代码复用性
2. AOP例子实践
1. 案例起源
创建一个计算机接口Com,定义以下四个方法:
public interface Com {
int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);
}
定义其实现类:ComImpl:
public class ComImpl implements Com {
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;
}
}
测试类:
public class Test {
public static void main(String[] args) {
Com com = new ComImpl();
System.out.println(com.add(1,4));
}
}
输出:
5
15
这个例子很简单,这里不做描述了,接下来看新的需求 :
- 要求在每个方法执行的同时,完成打印日志信息;
这个需求也很简单,我们可以通过在ComImpl类中的四个方法中每个方法都添加如下:
public int add(int x, int y) {
System.out.println("add方法的参数是:"+x+","+y);
int result = x+y;
System.out.println("add方法的结果为:"+result);
return result;
}
其他三个方法也类似,我就不再写其代码,这种方法能完成业务需求,但是其弊端显著:
- 重复代码过多,代码冗余;
- 业务代码和打印日志代码耦合性非常高,不利于后期的维护;
- 例如: 假如我要对打印日志的格式稍作修改,我就得去改变四个方法中得打印日志部分,假如有100个方法呢?每次都要手动去修改100个方法中的日志打印这块?
那么如何解决这个问题呢?
我们可以发现日志打印的代码基本都是一个格式的,我们能不能将这些相同部分的代码提取出来形成一个横切面呢?并且将这个横切面抽象成一个对象,将所有的打印日志代码写到这个对象中,以实现业务和代码的分离;
上面就是AOP的思想 ☝☝☝☝
2. 静态代理实现AOP
静态代理的要求:
- 代理对象和被代理对象实现同一个接口,接口中包含着真实业务;
- 代理对象注入被代理对象,同时可以添加辅助业务;
缺点:实质上还是比较繁杂,因为你还是需要在代理类的每个真实业务中添加自己的辅助业务,这样还是有许多重复的代码,不便于扩展;
3. 动态代理实现AOP
上面的思想我们可以用动态代理来实现;
对于ComImpl,我们只保留其业务代码(即最初的版本,不在其中添加日志代码);
1. 动态代理类的实现
创建MyInvocationHandler类,这个类实现InvocationHandler接口,成为一个动态代理类:
public class MyInvocationHandler implements InvocationHandler {
Object targetObj;
//返回代理对象
public Object bind(Object targetObj) {
this.targetObj = targetObj;
return Proxy.newProxyInstance(this.targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(),
this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
//日志业务
System.out.println(method.getName()+"的参数是:"+Arrays.toString(args));
//主业务
result = method.invoke(this.targetObj, args);
//日志业务
System.out.println(method.getName()+"的结果是:"+result);
return result;
}
}
bind 方法是 MyInvocationHandler 类提供给外部调用的方法,传入委托对象,bind 方法会返回一个代理对象,bind 方法完成了两项工作:
- (1)将外部传进来的委托对象保存到成员变量中,因为业务方法调用时需要用到委托对象。
- (2)通过 Proxy.newProxyInstance 方法创建一个代理对象,解释一下 Proxy.newProxyInstance 方法的参数:
- 我们知道对象是 JVM 根据运行时类来创建的,此时需要动态创建一个代理对象的运行时类,同时需要将这个动态创建的运行时类加载到 JVM 中,这一步需要获取到类加载器才能实现,我们可以通过委托对象的运行时类来反向获取类加载器,obj.getClass().getClassLoader() 就是通过委托对象的运行时类来获取类加载器的具体实现;
- 同时代理对象需要具备委托对象的所有功能,即需要拥有委托对象的所有接口,因此传入obj.getClass().getInterfaces();
- this 指当前 MyInvocationHandler 对象。
以上全部是反射的知识点,invoke 方法:method 是描述委托对象所有方法的对象,agrs 是描述委托对象方法参数列表的对象。 method.invoke(this.obj,args) 是通过反射机制来调用委托对象的方法,即业务方法。 因此在 method.invoke(this.obj, args) 前后添加打印日志信息,就等同于在委托对象的业务方法前后添加打印日志信息,并且已经做到了分类,业务方法在委托对象中,打印日志信息在代理对象中;
2. 测试
给出测试类:
public class Test {
public static void main(String[] args) {
//真实业务类
Com com = new ComImpl();
//代理类的对象(这个不叫代理对象)
MyInvocationHandler myInvocationHandler = new MyInvocationHandler();
//根据代理类的对象获取代理对象
Com com1 = (Com) myInvocationHandler.bind(com);
com1.add(1,5);
com1.sub(6, 3);
}
}
输出:
add的参数是:[1, 5]
add的结果是:6
sub的参数是:[6, 3]
sub的结果是:3
3. 结果分析
从测试中我们可以看到已经达到了我们的要求,业务和日志都能正确实现,而且:
- 业务和日志代码分离,ComImpl类中只有业务代码,而日志代码在MyInvocationHandler类中;
3. Spring中的AOP
在上面的案例中,我们用动态代理实现了AOP,但是在Spring中,我们不需要创建MyInvocationHandler类,Spring已经对其完成了封装,我们只需要创建一个切面类,Spring底层会自动根据切面类以及目标类生成一个代理对象;
1. 第一步:创建一个切面类:LoggerAspect
@Aspect
@Component
public class LoggerAspect {
//int代表返回值类型,必须写,后面是具体类名,*代表这个类的所有方法,..代表所有参数
@Before("execution(public int www.springAOP.ComImpl.*(..))")
public void before(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("方法名为:"+name+" 参数为:"+args);
}
@After("execution(public int www.springAOP.ComImpl.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("方法结束");
}
@AfterReturning(value = "execution(public int www.springAOP.ComImpl.*(..))", returning = "result")
//注意上面的result必须与下面的形参名一模一样,且必须有这个形参
public void afterReturn(JoinPoint joinPoint, Object result) {
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法结果为:"+result);
}
@AfterThrowing(value = "execution(public int www.springAOP.ComImpl.*(..))", throwing = "ex")
public void afterThrow(JoinPoint joinPoint, Exception ex) {
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法抛出异常:"+ex);
}
}
下面来分别解释以下各个注解:
- @Aspect : 声明该类为切面类
@Component :将该类注入到IoC容器
(注意:被切入的那个类,即ComImpl必须加上@Component注解⭐) - @Before :表示before方法执行的时机
- execution表达式: public可以省略,int代表返回值,不可以省略,int可以用*来代替,即所有返回值;
www.springAOP.ComImpl代表需要代理的真实类的全路径,紧跟后面的.代表这个类的所有方法,如果你要指定方法,可以将改成你所指定的方法名;后面的()里面代表参数,…代表所有参数,同样的,你要指定参数,可以将…换成指定的参数类型; - execution后面的表达式就是一个范围,代表在这个范围内进行切入;上面的就代表ComImpl所有方法在执行前都会执行LoggerAspect中的before方法;
- execution表达式: public可以省略,int代表返回值,不可以省略,int可以用*来代替,即所有返回值;
- @after :同理,表示 ComImpl 所有方法执行之后会执行 LoggerAspect 类中的 after 方法;
- @afterReturn : 表示 ComImpl 所有方法在 return 之后会执行 LoggerAspect 类中的 afterReturn 方法;
- @afterThrowing :表示 ComImpl 所有方法在抛出异常时会执行 LoggerAspect 类中的 afterThrowing 方法;
2. XML中的配置
在applicationContext_AOP.xml中进行如下配置:
<context:component-scan base-package="www.springAOP"/>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
第一行就不用说了,自动扫描包,使用注解的时候都必须用到这一行;
第二行则是:
-
使Aspect注解生效,为目标类自动生成代理对象;
-
Spring容器会结合切面类和目标类自动生成代理对象,Spring框架的底层就是通过动态代理的方式完成AOP;
-
目标类其实就是在那些execution表达式里;
-
3. 测试
public class AOPTest {
ApplicationContext applicationContext;
@Before
public void testInitial() {
applicationContext = new ClassPathXmlApplicationContext
("applicationContext_AOP.xml");
}
@Test
public void testMethod() {
//注意这里返回的是代理类,这个代理类继承了Com这个接口的
//这里返回的不是ComImpl,所以前面的类型只能是Com
Com com = (Com) applicationContext.getBean("comImpl");
com.div(2,1);
System.out.println("---------------------------------");
com.add(2,5);
System.out.println("---------------------------------");
com.div(2,0);
}
}
方法名为:div 参数为:[2, 1]
方法结束
div方法结果为:2
---------------------------------
方法名为:add 参数为:[2, 5]
方法结束
add方法结果为:7
---------------------------------
方法名为:div 参数为:[2, 0]
方法结束
div方法抛出异常:java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
。。。。。。(报错信息一大堆)
注意:
- 可能会疑问comImpl我们都没有在xml中配置它,怎么能getBean获取?
因为ComImpl使用了@Component注解,自动注入了IoC容器,而且前面我们知道,它的默认id就是类名的第一个字母小写后的名字; - 因为切面类中需要切入的是ComImpl类,所以这里getBean获取这个类的实例其实是获取它的代理类的实例; (⭐⭐)
4. AOP术语解释
在Spring AOP中,有如下几个术语:
1. Aspect(切面类)
- 切面是一个模板,它定义了所有需要完成的工作,比如切入的范围和时间,都是在切面中来完成;
- 在Spring中,通过实现@Aspect注解来构造一个切面;
- 上面的例子中,LoggerAspect就是一个切面类;
2. Advice(通知)
- 定义了切面是什么,何时使用,描述了切面要完成的工作,还解决何时执行这个工作的问题。
- 其实通知简单地说就是切面类的代码,即非业务代码,上面例子中就是LoggerAspect中的代码;
- 在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
3. Target(目标)
- 被横切的对象,对应上面例子中的ComImpl类的实例化对象,将通知放入其中;
4. Proxy(代理)
- 切面对象、通知、目标混合之后的内容,即我们用JDK动态代理机制创建的对象;
5. Join point(连接点)
- 在Java程序执行中,我们可以把每个方法看成一个点,所有方法的执行就是这些点串联的结果;而连接点就是目标类需要被切入的的位置,即通知要插入业务代码的具体位置;