Spring 面向切面编程
AOP 是 Spring 框架除了 IOC 之外的另一个核心概念。
AOP:Aspect Oriented Programming,意为面向切面编程。这是一个新的概念,但是我们知道 Java 是面向对象编程(OOP:Object Oriented Programming)的,指将所有的一切都看作对象,通过对象与对象之间相互作用来解决问题。AOP 是对 OOP 的一个补充,是在另外一个维度抽象出来的对象,具体是指程序在运行时,动态地将非业务代码切入业务代码中,从而实现代码的解耦合,将非业务代码抽象成一个对象,对该对象进行编程,这就是面向切面编程。
AOP 的优点:
- 降低模块之间的耦合度;
- 提高了代码的可维护性;
- 提高了代码的复用性;
- 集中分开管理非业务代码和业务代码,逻辑更加清晰;
- 业务代码更加简洁纯粹,没有其他代码的影响;
1. 面向切面编程思想
介绍概念过于抽象和空泛,不易于理解,下面我们通过一个实例来说明,慢慢引出 AOP 的动态代理机制以及实际开发中如何使用面向切面编程。
1、创建一个计算器接口 Cal,定义四个方法:加、减、乘、除;
public interface Cal {
public int add(int num1,int num2);
public int sub(int num1,int num2);
public int mul(int num1,int num2);
public int div(int num1,int num2);
}
2、创建接口实现类 CalImpl,并且实现上面定义的四个方法;
public class CalImpl implements Cal{
public int add(int num1, int num2) {
int result = num1 + num2;
return result;
}
public int sub(int num1, int num2) {
int result = num1 - num2;
return result;
}
public int mul(int num1, int num2) {
int result = num1 * num2;
return result;
}
public int div(int num1, int num2) {
int result = num1 / num2;
return result;
}
}
3、在测试方法中创建 CalImpl 对象,调用方法;
public class Test3 {
public static void main(String[] args) {
Cal cal = new CalImpl();
cal.add(12,4); //16
cal.sub(12,4); //8
cal.mul(12,4); //48
cal.div(12,4); //3
}
}
以上这段代码很简单,现在想要添加功能:在每一个方法执行的同时,打印日志信息,即该方法的参数列表和该方法的计算结果。
这个需求很简单,只需要在每一个方法体中,运算执行之前打印参数列表,运行之后打印计算结果即可,因此,对代码做出如下修改:
public class CalImpl implements Cal{
public int add(int num1, int num2) {
System.out.println("add方法的参数是["+num1+","+num2+"]" );
int result = num1 + num2;
System.out.println("add方法的结果是"+result);
return result;
}
public int sub(int num1, int num2) {
System.out.println("sub方法的参数是["+num1+","+num2+"]" );
int result = num1 - num2;
System.out.println("sub方法的结果是"+result);
return result;
}
public int mul(int num1, int num2) {
System.out.println("mul方法的参数是["+num1+","+num2+"]" );
int result = num1 * num2;
System.out.println("mul方法的结果是"+result);
return result;
}
public int div(int num1, int num2) {
System.out.println("div方法的参数是["+num1+","+num2+"]" );
int result = num1 / num2;
System.out.println("div方法的结果是"+result);
return result;
}
}
再次运行代码,成功打印日志信息:
功能已经实现了,但是我们会发现这种方式业务代码和打印日志代码的耦合性非常高,不利于代码后期的维护。例如,如果需求发生改变,需要对打印的日志内容作出修改,那么我们就必须修改4个方法中的所有相关代码,如果是100个方法呢?每次就需要手动去修改100个方法中的代码,对项目的维护成本就相当高。
从这个例子中,我们会发现4个打印日志信息的代码基本相同,那么有没有可能将这部分的代码抽取出来进行封装,统一进行维护呢?同时也可以将日志代码和业务代码完全分离,解耦合。
按照这个思路继续抽象,我们希望做的事情就是把这4个方法的相同位置(业务方法执行前、业务方法执行后)提取出来,形成一个横切面,并且将这个横切面封装成一个对象,将所有的打印日志代码写到这个对象中,以实现与业务代码的分离,这就是面向切面编程的思想。
2. 动态代理实现AOP
根据上述的需求,我们希望在 CalImpl 中只进行业务运算,不进行打印日志的工作,那么就需要有一个对象来代替 CalImpl 进行打印日志的工作,这就是代理对象。所以一个很直观的实现思路就是使用动态代理的方式。
1、删除 CalImpl 方法中所有的打印日志代码,只保留业务代码;
public class CalImpl implements Cal{
public int add(int num1, int num2) {
int result = num1 + num2;
return result;
}
public int sub(int num1, int num2) {
int result = num1 - num2;
return result;
}
public int mul(int num1, int num2) {
int result = num1 * num2;
return result;
}
public int div(int num1, int num2) {
int result = num1 / num2;
return result;
}
}
2、创建 MyInvocationHandler 类,并实现 InvocationHandler 接口,成为一个动态代理类;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class MyInvocationHandler implements InvocationHandler {
//委托对象,因为不知道委托对象的类型,所以定义成Object(多态)
private Object object = null;
//返回代理对象
public Object bind(Object object){
this.object = object;
return Proxy.newProxyInstance(object.getClass().getClassLoader(),
object.getClass().getInterfaces(),
this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+"的参数是:"+ Arrays.toString(args));
Object result = method.invoke(object,args);
System.out.println(method.getName()+"的结果是:"+result);
return result;
}
}
bind 方法是 MyInvocationHandler 类提供给外部调用的方法,传入需要被代理的对象,bind 方法会返回一个代理对象。bind 方法完成了两项工作:
-
将外部传进来的委托对象保存到成员变量中,因为业务方法调用时需要用到委托对象。
-
通过
Proxy.newProxyInstance
方法创建一个代理对象:解释一下
Proxy.newProxyInstance
方法的参数:- 我们知道对象是 JVM 根据运行时类来创建的,此时需要动态创建一个代理对象,可以使用委托对象的运行时类来创建代理对象:
object.getClass().getClassLoader()
获取委托对象的运行时类; - 同时代理对象需要具备委托对象的所有方法,即需要用于委托对象的所有接口,所以传入
object.getClass().getInterfaces()
; this
指的是当前 MyInvocationHandler 类的对象;
- 我们知道对象是 JVM 根据运行时类来创建的,此时需要动态创建一个代理对象,可以使用委托对象的运行时类来创建代理对象:
invoke 方法:method 是描述委托对象的所有方法的对象,args 是描述委托对象方法的参数列表的对象。
method.invoke(object,args)
是通过反射机制来调用被代理对象的方法,即业务方法。
所以在 method.invoke(object, args)
前后添加打印日志的信息,就等同于在委托对象的业务方法前后添加打印日志信息,并且已经做到了分离,业务方法在委托对象中,打印日志信息在代理对象中。
3、测试方法中执行代码
public class Test3 {
public static void main(String[] args) {
//委托对象
Cal cal = new CalImpl();
MyInvocationHandler handler = new MyInvocationHandler();
//代理对象
Cal cals = (Cal)handler.bind(cal);
cals.add(12,4);
cals.sub(12,4);
cals.mul(12,4);
cals.div(12,4);
}
}
可以看到和上面的执行结果一样,但是我们现在已经做到了代码分离,CalImpl 类中只有业务方法,打印日志的代码写在了 MyInvocationHandler 类中。
以上就是通过动态代理实现 AOP 的过程,我们在使用 Spring 框架的 AOP 时,并不需要那么复杂,Spring 已经对这个过程进行了封装,让开发者可以更加便捷的使用 AOP 进行开发。
3. Spring 框架的AOP操作
在 Spring 框架中,我们不需要创建动态代理类,只需要创建一个切面类,Spring 底层自动会根据切面类以及目标类生成一个代理对象。
第一步:在 pom.xml 配置文件中添加 aspect 注解相关的依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
第二步:创建切面类 LoggerAspect
@Component
@Aspect
public class LoggerAspect {
@Before("execution(public int com.trainingl.aop.CalImpl.*(..))")
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 com.trainingl.aop.CalImpl.*(..))")
public void after(JoinPoint joinPoint){
//获取方法名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法结束");
}
@AfterReturning(value = "execution(public int com.trainingl.aop.CalImpl.*(..))",returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result){
//获取方法名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法的结果是"+result);
}
@AfterThrowing(value = "execution(public int com.trainingl.aop.CalImpl.*(..))",throwing = "exception")
public void afterThrowing(JoinPoint joinPoint,Exception exception){
//获取方法名
String name = joinPoint.getSignature().getName();
}
}
LoggerAspect 类名处添加了两个注解:
@Aspect
:表示该类是切面类;@Component
:将该类注入到 IOC 容器;
分别来说明类中的 4 个方法的注解的含义:
@Before("execution(public int com.trainingl.aop.CalImpl.*(..))")
public void before(JoinPoint joinPoint){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取参数列表
String args = Arrays.toString(joinPoint.getArgs());
System.out.println(name + "的参数是:" + args);
}
@Before
:表示 before 方法执行的时机;execution(public int com.trainingl.aop.CalImpl.*(..))
:表示切入点是com.trainingl.aop
包下CalImpl
类中的所有方法;
@Before
表示 CalImpl 所有方法在执行之前会首先执行 LoggerAspect 类中的 before 方法。
@after
注解同理,表示 CalImpl 所有方法在执行之后会首先执行 LoggerAspect 类中的 after 方法;
@afterReturning
注解表示 CalImpl 所有方法在 return 之后会执行 LoggerAspect 类中的方法;
@afterThrowing
注解表示 CalImpl 所有方法在抛出异常时会执行 LoggerAspect 类中的 afterThrowing 方法。
所以,开发者可以根据具体的切入需求,选择在 before、after、afterReturn、afterThrowing 方法中添加相应的代码。
第三步:目标类也需要添加 @Component
注解
@Component
public class CalImpl implements Cal{
public int add(int num1, int num2) {
int result = num1 + num2;
return result;
}
public int sub(int num1, int num2) {
int result = num1 - num2;
return result;
}
public int mul(int num1, int num2) {
int result = num1 * num2;
return result;
}
public int div(int num1, int num2) {
int result = num1 / num2;
return result;
}
}
第四步:spring.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:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--将指定包下的类扫描到IOC容器中-->
<context:component-scan base-package="com.trainingl.aop"></context:component-scan>
<!--使Aspect注解生效,为目标类自动生成代理对象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
-
将
com.trainingl.ioc
包中的类扫描到 IOC 容器中; -
添加
aop:aspectj-autoproxy
注解,Spring 容器会结合切面类和目标类自动生成动态代理对象,Spring 框架的 AOP 底层就是通过动态代理的方式完成 AOP;
第五步:测试方法执行如下代码
从 IOC 容器中获取代理对象,执行方法
public class Test4 {
public static void main(String[] args) {
//加载spring.xml
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
//获取代理对象
Cal cal = (Cal) applicationContext.getBean(Cal.class);
cal.add(12,4);
cal.sub(12,4);
cal.mul(12,4);
cal.div(12,4);
}
}
执行结果如下:
结合代码和图示,重新理解几个概念:
- 切面:横切关注点被模块化的特殊对象,即本例中 CalImpl 所有方法中需要加入日志的部分,抽象成一个切面对象 LoggerAspect;
- 切点:AOP 通过切点定位到连接点;
- 连接点:程序要执行的某个特定位置,切面方法要插入业务代码的具体位置。