首页 > 编程笔记

Spring AOP(面向切面编程)

AOP(Aspect Oriented Programming)与 OOP(Object Oriented Programming,面向对象编程)相辅相成。AOP 提供了与 OOP 不同的抽象软件结构的视角。在 OOP 中,我们以类(Class)作为基本单元,而在 AOP 中则以切面(Aspect)作为基本单元。

AOP 是一种增强的编程方式,可以解耦一些非业务逻辑,如声明式事务管理、日志管理或异常处理等。从底层原理来讲,AOP 实际上是基于 Java 的代理模式实现的。

代理模式

代理模式是经典的设计模式之一,目的是为了扩展和增强类或接口。代理模式通常可以分为静态代理模式和动态代理模式。

1) 静态代理模式

静态代理模式的实现比较简单,主要的实现原理是:代理类与被代理类同时实现一个主题接口,代理类持有被代理类的引用。

(1)新建一个公共接口 UserInterface,代码如下:
//声明 UserInterface 接口
public interface UserInterface {
    //声明方法
        public abstract void getUser();
}
(2)定义真实执行类 RealUser 并实现公共接口 UserInterface,代码如下:
//声明 RealUser 类,实现 UserInterface 接口
public class RealUser implements UserInterface {
        @Override
        public void getUser() {
                //新建UserService对象
                System.out.println("真实用户角色执行!");
                UserService userService = new UserService();
                userService.setId(1);
                userService.setName("www.weixueyuan.net");
                userService.getUser();
        }
}
(3)定义代理类 UserProxy 实现公共接口 UserInterface,并持有被代理类的实例。在执行时,调用被代理类(RealUser)实例的 getUser()方法。代码如下:
//声明 UserProxy 代理类,并实现 UserInterface 接口
public class UserProxy implements UserInterface {
        private UserInterface userInterface;
        //构造方法传入 UserInterface 类型参数
        public UserProxy(UserInterface userInterface) {
                this.userInterface = userInterface;
        }
        //实现getUser()方法,在执行方法前后进行额外操作
        @Override
        public void getUser() {
                doBefore();
                userInterface.getUser();
                doAfter();
        }
        //真实方法执行前操作
        private void doBefore() {
                System.out.println("代理类开始执行");
        }
        //真实方法执行后操作
        private void doAfter() {
                System.out.println("代理类结束执行");
        }
}
(4)编写测试代码,具体如下:
public class SpringProxyTest {
        public static void main(String[] args) {
                UserInterface realUser = new RealUser();
                //传入真实对象RealUser
                UserProxy userProxy = new UserProxy(realUser);
                userProxy.getUser();
        }
}
运行结果如下:

代理类开始执行
真实用户角色执行!
id:1
name:www.weixueyuan.net
代理类结束执行

从输出结果可以看到,代理类实际上是调用了被代理类的方法。

2) 动态代理

顾名思义,动态代理是指在程序运行时动态地创建代理类。动态代理的使用方式主要分为两种:一种是基于接口的代理,另一种则是基于类的代理。基于接口的代理方式是指通过 JDK 自带的反射类来生成动态代理类;基于类的代理方式则是指通过字节码处理来实现类代理,如 CGLIB 和 Javassist 等。

首先我们来看一个基于 JDK 反射生成代理类的例子。

(1)定义一个公共接口 UserServiceInterface,代码如下:
public interface UserServiceInterface {
        public void getUser();
}
(2)定义真实用户角色类 UserServiceImpl 并实现公共接口 UserServiceInterface,代码如下:
public class UserServiceImpl implements UserServiceInterface {
        @Override
        public void getUser() {
                System.out.println("www.weixueyuan.net");  //实现getUser()方法
        }
}
(3)定义代理类 UserServiceProxy,实现 InvocationHandler 接口,并重写 invoke() 方法,代码如下:
//定义实现 InvocationHandler 接口的代理类 UserServiceProxy
public class UserServiceProxy implements InvocationHandler {
        private Object target;
        //构造方法
        public UserServiceProxy(Object target) {
                this.target = target;
        }
        //通过Proxy动态生成代理类对象
        public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    //动态执行方法
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
                System.out.println("JDK before");
                method.invoke(target, args);
                System.out.println("JDK after");
                return null;
        }
}
(4)编写测试代码:
public class SpringProxyTest {
        public static void main(String[] args) {
                //通过代理类生成UserServiceInterface接口类型对象
                UserServiceInterface userServiceInterface = new UserServiceProxy(new UserServiceImpl()).getProxy();
                userServiceInterface.getUser(); //调用getUser()方法
        }
}
运行结果如下:

JDK proxy before
www.weixueyuan.net
JDK proxy after

通过上面的代理类的执行结果可以看到,真实用户角色类被屏蔽了,只需要暴露接口即可执行成功。屏蔽内部实现的逻辑就是代理模式的特点。

上面主要讲的是基于 JDK 反射的例子。下面来看一下 CGLIB 实现动态代理的原理。它是通过继承父类的所有公共方法,然后重写这些方法,并在重写方法时对这些方法进行增强处理来实现的。根据里氏代换原则(LSP),父类出现的地方子类都可以出现,因此 CGLIB 实现的代理类也是可以被正常使用的。

CGLIB 的基本架构如图 1 所示,代理类继承自目标类,每次调用代理类的方法时都会被拦截器拦截,然后在拦截器中调用真实目标类的方法。
图1 CGLIB动态代理实现原理
图1 CGLIB动态代理实现原理

CGLIB 实现动态代理的方式比较简单,具体如下:

(1)直接实现 MethodInterceptor 拦截器接口,并重写 intercept() 方法。代码如下:
//继承MethodInterceptor类并实现intercept()方法
public class UserMethodInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
                System.out.println("Cglib before");
                proxy.invokeSuper(obj, args);
                System.out.println("Cglib after");
                return null;
        }
}
(2)新建Enhancer类,并设置父类和拦截器类。代码如下:
public class SpringProxyTest {
        public static void main(String[] args) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(UserServiceImpl.class); //设置父类
                //设置拦截器
                enhancer.setCallback(new UserMethodInterceptor());
                UserServiceImpl userServiceImpl = (UserServiceImpl) enhancer.create();  //创建对象
                userServiceImpl.getUser(); //调用getUser方法
        }
}
运行结果如下:

Cglib before
www.weixueyuan.net
Cglib after

JDK实现动态代理是基于接口,其中,目标类与代理类都继承自同一个接口;而CGLIB实现动态代理是继承目标类并重写目标类的方法。在项目开发过程中,可根据实际情况进行选择。

AOP中的术语

Spring AOP 就是负责实现切面编程的框架,它能将切面所定义的横切逻辑织入切面所指定的连接点中。AOP 是一种面向切面的编程,有很多独有的概念,如切面、连接点和通知等,它们组合起来才能完成一个完整的切面逻辑。因此,AOP 的工作重心在于如何将增强织入目标对象的连接点上。

1) 切面

切面(Aspect)通常由 Pointcut(切点)和 Advice(通知)组合而成。通常是定义一个类,并在类中定义 Pointcut 和 Advice。定义的 Pointcut 用来匹配Join point(连接点),也就是对那些需要被拦截的方法进行定义。定义的 Advice 用来对被拦截的方法进行增强处理。在 Spring AOP 中,切面定义可以基于 XML 配置定义,也可以用 @Aspect 注解定义。我们可以简单地认为,使用 @Aspect 注解的类就是一个切面类。

2) 连接点

连接点是程序执行过程中的一个明确的点,如方法的执行或者异常处理。在 Spring AOP 中,一个连接点一般代表一个方法的执行。

3) 通知

通知是切面在特定的连接点上执行的特殊逻辑。通知可以分为方法执行前(Before)通知、方法执行后(After)通知和环绕(Around)通知等。包括Spring AOP 在内的许多 AOP 框架通常会使用拦截器来增强逻辑处理能力,围绕着连接点维护一个拦截器链。

Spring AOP 的 Advice 类型如表 2 所示。
表2 Advice类型
类  型 说  明
Before advice 前置增强,在连接点之前执行
After returning advice 后置增强,在方法正常退出时执行
After throwing advice 后置增强,在抛出异常时执行
After (finally) advice 后置增强,不管是抛出异常还是正常退出都会执行
Around advice 在连接点之前和之后均执行

4) 切点

切点是一种连接点的声明。通知是由切点表达式连接并匹配上切点后再执行的处理逻辑。切点用来匹配特定连接点的表达式,增强处理将会与切点表达式产生关联,并运行在匹配到的连接点上。通过切点表达式匹配连接点是 AOP 的核心思想。Spring 默认使用 AspectJ 的切点表达式。

5) 引入

Spring AOP 可以引入一些新的接口来增强类的处理能力。例如,可以使用引入(Introduction)让一个 Bean 实现 IsModified 接口,从而实现一个简单的缓存功能。

6) 目标类

目标类(Target Class)是指被切面增强的类。被一个或多个切面增强的对象也叫作增强对象。Spring AOP 采用运行时代理(Runtime Proxies),目标对象就是代理对象。

7) AOP代理

Spring 框架中的 AOP 代理(AOP Proxy)指的是 JDK 动态代理或者 CGLIB 动态代理。为了实现切面功能,目标对象会被 AOP 框架创建出来。在 Spring框架中,AOP 代理的创建方式包括两种:如果有接口,则使用基于接口的 JDK 动态代理,否则使用基于类的 CGLIB 动态代理。也可以在 XML 中通过设置proxy-target-class 属性来完全使用 CGLIB 动态代理。

8) 织入

在编译期、加载期和运行期都可以将增强织入(Weaving)目标对象中,但 Spring AOP一般是在运行期将其织入目标对象中。织入可以将一个或多个切面与类或对象连接在一起,然后创建一个被增强的对象。

优秀文章