1 概述

本文主要讲述了AOP的基本概念以及在SpringAOP的几种实现方式。

2 AOP

AOP,即Aspect-Oriented Programming,面向切面编程,与OOP相辅相成。类似的,在OOP中,以类为程序的基本单元,在AOP中的基本单元是Aspect(切面)。AOP采用横向抽取的机制,将分散在各个方法中重复的代码提取出来,然后在程序编译或运行阶段将这些抽取出来的代码应用到需要执行的地方,这种横向抽取机制是OOP无法办到的。

AOP最典型的一个应用就是抽离出业务模块中与业务不相关的代码,比如,日志记录,性能统计,安全控制,事务处理等。假设业务代码如下:

public void save()
{
saveUser();
}

但是随着软件开发越来越复杂,业务代码变成了下面的样子:

public void save()
{
//安全控制
//性能统计
//事务处理
//日志记录
saveUser();
}

这样业务类就回混杂很多业务无关的代码,不仅会显得类臃肿不堪,同时也难于开发人员进行维护。因此,引入AOP后,可以将安全控制等的代码抽取出来,交给AOP机制在编译时或运行时再将这些代码进行动态“织入”,这样就可以让业务层专注于业务代码,而不用混杂其他逻辑的代码。

3 AOP实现方式

AOP的实现方式主要有以下几种:

  • 动态代理:JDKCGLIBJavassistASM
  • SpringAOP实现:基于代理类/AspectJ实现

下面先来看一个简单的例子。

4 动态代理实现

Java中有多种动态代理技术,比如:

  • JDK
  • CGLIB
  • Javassist
  • ASM

等等,在Spring AOP中常用的有JDKCGLIB,先看一下最经典的JDK动态代理的实现。

4.1 JDK实现

结构:

  • JDKInterface:测试接口,含三个方法
  • JDKImpl:测试接口实现类
  • JDKAspect:切面类
  • JDKProxy:代理类
  • JDKTest:测试类

首先对接口类进行编写,包含三个方法:

public interface JDKInterface {
void save();
void modify();
void delete();
}

编写实现类:

public class JDKImpl implements JDKInterface {
@Override
public void save() {
System.out.println("保存");
} @Override
public void modify() {
System.out.println("修改");
} @Override
public void delete() {
System.out.println("删除");
}
}

接下来是定义切面类,模拟上面提到的异常处理等功能:

public class JDKAspect {
public void check()
{
System.out.println("模拟权限控制");
} public void except()
{
System.out.println("模拟异常处理");
} public void log()
{
System.out.println("模拟日志记录");
} public void monitor()
{
System.out.println("模拟性能监测");
}
}

再接着是很关键的代理类,该类的任务是创建代理对象,并且负责如何对被代理对象进行增强处理:

public class JDKProxy implements InvocationHandler {
public JDKInterface testInterface;
public Object createProxy(JDKInterface testInterface)
{
this.testInterface = testInterface;
//获取代理类的类加载器
ClassLoader loader = JDKProxy.class.getClassLoader();
//获取被代理对象(目标对象)的接口
Class<?>[] classes = testInterface.getClass().getInterfaces();
//创建代理后的对象
return Proxy.newProxyInstance(loader,classes,this);
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
//创建一个切面
JDKAspect aspect = new JDKAspect();
//前增强
aspect.check();
aspect.except();
//调用被代理对象的方法,这里指的是testInterface的方法,objects是参数,obj是返回值
Object obj = method.invoke(testInterface,objects);
//后增强
aspect.log();
aspect.monitor();
return obj;
}
}

创建代理对象时,需要指定代理类的类加载器以及被代理对象的接口,接着通过Proxy.newProxyInstance创建代理对象。

测试:

public class JDKTest {
public static void test()
{
JDKProxy proxy = new JDKProxy();
JDKInterface testInterface = (JDKInterface) proxy.createProxy(new JDKImpl());
testInterface.save();
testInterface.modify();
testInterface.delete();
}
}

输出:

可以看到:

  • 在执行真正的业务操作之前,被动态织入了权限控制,异常处理功能
  • 在执行业务操作后被织入了日志记录和性能监测功能

这就是AOP的一种最典型的应用。

4.2 CGLIB实现

首先添加依赖(由于这是新建的工程需要手动添加CGLIB,在Spring Core中已经包含了CGLIB无需手动添加):

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

结构与上面的类似:

  • CGLibInterface:一个简单的接口
  • CGLibImpl:接口实现类
  • CGLibProxy:代理对象创建类
  • CGLibAspect:切面类
  • CGLibTest:测试类

首先是接口类和接口实现类,与上面的一样:

public interface CGLibTestInterface {
void save();
void modify();
void delete();
} public class CGLibImpl implements CGLibTestInterface{
@Override
public void save() {
System.out.println("保存");
} @Override
public void modify() {
System.out.println("修改");
} @Override
public void delete() {
System.out.println("删除");
}
}

切面类也一样:

public class CGLibAspect {
public void check()
{
System.out.println("模拟权限控制");
} public void except()
{
System.out.println("模拟异常处理");
} public void log()
{
System.out.println("模拟日志记录");
} public void monitor()
{
System.out.println("模拟性能监测");
}
}

唯一不同的是代理类,需要实现MethodInterceptor接口:

public class CGLibProxy implements MethodInterceptor {
public Object createProxy(Object target)
{
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
} @Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
CGLibAspect aspect = new CGLibAspect();
aspect.check();
aspect.except();
Object obj = methodProxy.invokeSuper(o,objects);
aspect.log();
aspect.monitor();
return obj;
}
}

其中代理对象创建过程如下:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();

首先使用Enhancer生成一个代理类,该代理类的父类通过setSuperclass设置,回调函数通过setCallback设置,其中setCallback的参数为MethodInterceptor,也就是实现了该接口的类,最后通过create创建代理对象。

接着在intercept对方法进行织入,过程与JDK的实现类似。

测试:

public class CGLibTest {
public static void test() {
CGLibProxy proxy = new CGLibProxy();
CGLibTestInterface cgLibTestInterface = (CGLibTestInterface) proxy.createProxy(new CGLibImpl());
cgLibTestInterface.delete();
cgLibTestInterface.modify();
cgLibTestInterface.save();
}
}

输出也是一致:

这里因为笔者用的是OpenJDK11,使用CGLIB包时会有非法反射访问警告:

加上JVM参数就好了:

--illegal-access=deny --add-opens java.base/java.lang=ALL-UNNAMED

5 相关术语

看了两个Demo后应该可以大概知道AOP的作用了,下面正式介绍相关术语:

  • Join Pont连接点,可能需要注入切面的地方,包括方法调用的前后等等
  • Pointcut切点,需要特殊处理的连接点,通过切点确定哪些连接点需要处理,比如上面的两个接口JDKInterfaceCGLibInterface中的方法就是切点,当调用这些接口中的方法(也就是切点)时需要进行特殊的处理
  • Advice通知,定义在什么时候做什么事情,上面的例子没有提到,下面Spring AOP的例子会有体现
  • Aspect切面通知+切点的集合,类似于Java中的类声明,比如上面的JDKAspect以及CGLibAspect就相当于切面
  • Target目标对象,需要被通知的对象,比如上面的new CGLibImpl()以及new JDKImpl(),也就是实现了接口的对象
  • Proxy代理,通知应用到目标对象后被创建的对象,比如上面的通过createProxy创建的对象
  • Weaving织入,把切面代码插入到目标对象后生成代理对象的过程,比如上面的把checkexcept等代码插入到目标对象new CGLibImpl()以及new JDKImpl())后,生成代理对象(通过Proxy.newInsatnce()enhancer.create())的过程。织入有三种方式,一种是编译器织入,需要有特殊的编译器,一种是类装载期织入,需要有特殊的类装载器,一种是动态代理织入,在运行时期为目标类添加通知生成子类的方式,Spring AOP默认使用动态代理织入AspectJ采用编译器织入类装载期织入

如果还是不明白就自己动手多实现几次,AOP确实是有点难理解,笔者也只能帮大家到这了。

6 Spring AOP实现

SpringAOP实现主要有两种方式:

  • 使用JDK自带的动态代理,配合ProxyFactoryBean实现
  • 使用AspectJ实现,可以通过XML或注解配置实现

先来看一下代理类实现的方式。

6.1 代理类实现

使用代理类实现需要ProxyFactoryBean类,同时也需要了解一下AOP中的通知类型。

6.1.1 Spring AOP通知类型

6种:

  • 环绕通知:目标方法执行前和执行后实施的增强
  • 前置通知:目标方法执行前实施增强
  • 后置返回通知:目标方法执行成功后实施增强,发生异常不执行
  • 后置最终通知:目标方法执行后实施增强,不管是否发生异常都要执行
  • 异常通知:抛出异常后实施增强
  • 引入通知:在目标类添加一些新的方法和属性,可用于修改目标类

流程如下:

6.1.2 ProxyFactoryBean

ProxyFactoryBeanFactoryBean的一个实现类,负责为其他Bean实例创建代理实例,XML配置中常用属性如下:

  • target:代理的目标对象
  • proxyInterfaces:代理需要实现的接口列表
  • interceptorNames:需要织入目标的Advice
  • proxyTargetClass:是否对类代理而不是接口,默认为false,使用JDK动态代理,为true时使用CGLIB
  • singleton:代理类是否为单例,默认true
  • optimize:设置为true时强制使用CGLIB

6.1.3 实现

依赖:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>

结构类似:

  • ProxyClassInterface:一个简单的接口
  • ProxyClassImpl:接口实现类
  • ProxyClassAspect:切面类
  • ProxyTest:测试类

其中接口类与接口实现类与上面一致:

public interface ProxyInterface {
void save();
void modify();
void delete();
} public class ProxyClassImpl implements ProxyInterface{
@Override
public void save() {
System.out.println("保存");
} @Override
public void modify() {
System.out.println("修改");
} @Override
public void delete() {
System.out.println("删除");
}
}

切面类实现了MethodInterceptororg.aopalliance.intercept.MethodInterceptor),同时指定了增强方法:

public class ProxyClassAspect implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
check();
expect();
Object obj = methodInvocation.proceed();
log();
monitor();
return obj;
} private void check()
{
System.out.println("模拟权限控制");
} private void expect()
{
System.out.println("模拟异常处理");
} private void log()
{
System.out.println("模拟日志记录");
} private void monitor()
{
System.out.println("性能监测");
}
}

这里调用目标方法相比起CGLIB以及JDK简单了很多,无需参数,返回Object,接着是测试类:

public class ProxyClassTest {
public static void test()
{
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
ProxyInterface proxyInterface = (ProxyInterface)context.getBean("factory");
proxyInterface.delete();
proxyInterface.save();
proxyInterface.modify();
}
}

配置文件:

<?xml version="1.0" encoding="utf-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="impl" class="com.proxyclass.ProxyClassImpl"/>
<bean id="aspect" class="com.proxyclass.ProxyClassAspect" />
<bean id="factory" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.proxyclass.ProxyInterface"/>
<property name="target" ref="impl" />
<property name="interceptorNames" value="aspect"/>
<property name="proxyTargetClass" value="true"/>
</bean>
</beans>

配置文件说明:

  • impl:接口实现类Bean
  • aspect:切面类Bean
  • factory:代理BeanproxyInterfaces指定代理实现的接口,target指定目标对象,interceptorNames指定切面,proxyTargetClass设置为true,指定使用CGLIB

输出:

6.2 XML配置的AspectJ实现

6.2.1 AspectJ

AspectJ是一个基于JavaAOP框架,使用AspectJ实现Spring AOP的方法有两种,一种是基于XML配置,一种是基于注解配置,先来看基于XML配置的实现。

6.2.2 XML元素

基于XML配置的AspectJ需要在其中定义切面,切点以及通知,需要定义在<aop:config>内。

<aop:config>AspectJ顶层配置元素,子元素如下:

  • <aop:aspect>:定义切面
  • <aop:pointcut>:定义切点
  • <aop:before>:定义前置通知
  • <aop:after-returning>:定义后置返回通知
  • <aop:around>:定义环绕通知
  • <aop:after-throwing>:定义异常通知
  • <aop:after>:定义后置最终通知

6.2.3 实现

依赖:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>

接口与实现类:

public interface XMLInterface {
void save();
void modify();
void delete();
} public class XMLImpl implements XMLInterface {
@Override
public void save() {
System.out.println("保存");
} @Override
public void modify() {
System.out.println("修改");
} @Override
public void delete() {
System.out.println("删除");
}
}

切面类:

public class XMLAspect {
public void before()
{
System.out.println("前置通知");
} public void afterReturning()
{
System.out.println("后置返回通知");
} public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
{
System.out.println("环绕通知开始");
Object object = proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
return object;
} public void expect(Throwable e)
{
System.out.println("异常通知");
} public void after()
{
System.out.println("后置最终通知");
}
}

提供了更加精细的方法,比如前置通知以及后置通知。在环绕通知中,简化了目标方法的调用,只需要通过proceed调用即可获取返回值,测试类如下:

public class XMLTest {
public static void test()
{
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
XMLInterface xmlInterface = (XMLInterface)context.getBean("xmlImpl");
xmlInterface.delete();
xmlInterface.modify();
xmlInterface.save();
}
}

配置文件:

<bean id="xmlImpl" class="com.aspectj_xml.XMLImpl"/>
<bean id="xmlAspect" class="com.aspectj_xml.XMLAspect" />
<aop:config>
<aop:pointcut id="pointCut" expression="execution(* com.aspectj_xml.XMLInterface.*(..))"/>
<aop:aspect ref="xmlAspect">
<aop:before method="before" pointcut-ref="pointCut" />
<aop:after-returning method="afterReturning" pointcut-ref="pointCut" />
<aop:around method="around" pointcut-ref="pointCut" />
<aop:after-throwing method="expect" pointcut-ref="pointCut" throwing="e"/>
<aop:after method="after" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>

说明如下:

  • pointCut定义了一个切点,expression是切点表达式
  • expression中第一个*表示返回类型,使用*表示任意类型
  • 注意第一个*与后面的包名有空格
  • 第二个*表示该接口/类中的所有方法
  • (..)表示方法的参数,..表示任意参数

关于expression更详细的例子请查看文档,戳这里

  • <aop:aspect>定义了一个切面,里面包含了前置通知/后置通知/异常通知等等,method表示的是调用的方法,pointcut-ref是切点的引用,表示在哪一个切点上进行增强处理

6.2.4 输出

可以看到执行顺序为:

前置通知->环绕通知开始->目标方法->后置最终通知->环绕通知结束->后置返回通知

可以看到顺序为:

前置通知->环绕通知开始->目标方法-后置最终通知->异常通知

与上面的执行流程图一致。

6.3 注解配置的AspectJ实现(推荐)

6.3.1 常用注解

基于注解开发是目前最常用的方式,比XML要便捷很多,常见的注解如下:

  • @Aspect:定义一个切面,注解在切面类上
  • @Pointcut:定义切点表达式,需要一个返回值与方法体都为空的方法,并将@Pointcut注解在该空方法上
  • @Before:定义前置通知,值可以是切点或切点表达式
  • @AfterReturning:定义后置返回通知
  • @Around:定义环绕通知
  • @AfterThrowing:定义异常通知
  • @After:定义后置最终通知

6.3.2. 实现

接口与实现类:

public interface AnnotationInterface {
void save();
void modify();
void delete();
} @Component
public class AnnotationImpl implements AnnotationInterface {
@Override
public void save() {
System.out.println("保存");
} @Override
public void modify() {
System.out.println("修改");
} @Override
public void delete() {
System.out.println("删除");
// 注释下面这行语句开启异常通知
// int a = 1/0;
}
}

切面类:

@Aspect
@Component
public class AnnotationAspect { @Pointcut("execution(* com.aspectj_annotation.AnnotationInterface.*(..))")
public void pointcut(){} @Before("pointcut()")
public void before()
{
System.out.println("前置通知");
} @AfterReturning(value = "pointcut()")
public void afterReturning()
{
System.out.println("后置返回通知");
} @Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
{
System.out.println("环绕通知开始");
Object object = proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
return object;
} @AfterThrowing(value = "pointcut()",throwing = "e")
public void except(Throwable e)
{
System.out.println("异常通知");
} @After("pointcut()")
public void after()
{
System.out.println("后置最终通知");
} }

测试类:

public class AnnotationTest {
public static void test()
{
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
AnnotationInterface annotationInterface = (AnnotationInterface)context.getBean("annotationImpl");
annotationInterface.delete();
annotationInterface.modify();
annotationInterface.save();
}
}

配置文件:

<context:component-scan base-package="com.aspectj_annotation"/>
<aop:aspectj-autoproxy/>

输出:

值得注意的是输出的顺序变动了,首先是环绕通知而不是前置通知。至于为什么这样笔者也不是很清楚,欢迎大神留言补充。

7 参考源码

Java版:

Kotlin版:

Spring 学习笔记(四):Spring AOP的更多相关文章

  1. spring学习笔记四:AOP

    AOP(Aspect Orient Programming),面向切面编程,是对面向对象编程OOP的一种补充 面向对象编程使用静态角度考虑程序的结构,而面向切面编程是从动态角度考虑程序运行过程 AOP ...

  2. spring学习笔记(一) Spring概述

    博主Spring学习笔记整理大部分内容来自Spring实战(第四版)这本书.  强烈建议新手购入或者需要电子书的留言. 在学习Spring之前,我们要了解这么几个问题:什么是Spring?Spring ...

  3. Java架构师之路 Spring学习笔记(一) Spring介绍

    前言 这是一篇原创的Spring学习笔记.主要记录我学习Spring4.0的过程.本人有四年的Java Web开发经验,最近在面试中遇到面试官总会问一些简单但我不会的Java问题,让我觉得有必要重新审 ...

  4. Spring学习笔记IOC与AOP实例

    Spring框架核心由两部分组成: 第一部分是反向控制(IOC),也叫依赖注入(DI); 控制反转(依赖注入)的主要内容是指:只描述程序中对象的被创建方式但不显示的创建对象.在以XML语言描述的配置文 ...

  5. Spring学习笔记四 整合SSH

    三大框架架构(整合原理) 步骤1:导包 Hibernate包 1.Hibernate包,hibernate/lib/required 2.hibernate/lib/jpa | java persis ...

  6. Spring 学习(四)--- AOP

    问题 : AOP 解决的问题是什么 Spring AOP 的底层实现是什么 Spring AOP 和 AspectJ 的区别是什么 概述 在软件业,AOP为Aspect Oriented Progra ...

  7. Spring学习十四----------Spring AOP实例

    © 版权声明:本文为博主原创文章,转载请注明出处 实例 1.项目结构 2.pom.xml <project xmlns="http://maven.apache.org/POM/4.0 ...

  8. Spring 学习笔记(2) Spring Bean

    一.IoC 容器 IoC 容器是 Spring 的核心,Spring 通过 IoC 容器来管理对象的实例化和初始化(这些对象就是 Spring Bean),以及对象从创建到销毁的整个生命周期.也就是管 ...

  9. Spring学习笔记四:SpringAOP的使用

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6776247.html  一:AOP基础概念 (1)通知(增强)Advice 通知,其实就是我们从众多类中提取出 ...

  10. spring学习 十四 注解AOP 通知传递参数

    我们在对切点进行增强时,不建议对切点进行任何修改,因此不加以使用@PointCut注解打在切点上,尽量只在Advice上打注解(Before,After等),如果要在通知中接受切点的参数,可以使用Jo ...

随机推荐

  1. mysql导入备份.sql文件时报错总结(还有待完善)

    错误1:ERROR Unknown character set: 'utf8mb4' utf8mb4编码集支持了表情符号,相信处理过社交网络数据的人都有了解.这个mysql5.5以后支持了utf8mb ...

  2. DRF的orm多表关系补充及serializer子序列化

    目录 一.控制多表关系的字段属性 1.如何建立基表 2.断开连表关系 3.四种级联关系 二.子序列化 一.控制多表关系的字段属性 1.如何建立基表 要在基表中配置Meta,设置abstract=Tru ...

  3. 后端程序员之路 59、go uiprogress

    gosuri/uiprogress: A go library to render progress bars in terminal applicationshttps://github.com/g ...

  4. 记录core中GRPC长连接导致负载均衡不均衡问题 二,解决长连接问题

    题外话: 1.这几天收到蔚来的面试邀请,但是自己没做准备,并且远程面试,还在上班时间,再加上老东家对我还不错.没想着换工作,导致在自己工位上做算法题不想被人看见,然后非常紧张.估计over了.不过没事 ...

  5. HDOJ-2087(KMP算法)

    剪花布条 HDOJ-2087 本题和hdoj-1686相似,唯一不同的是这里的子串一定要是单独的.所以在确定有多少个子串时不能用前面的方法.而是在循环时,只要找到一个子串,i就不是++,而是+=子串的 ...

  6. 模式识别Pattern Recognition

    双目摄像头,单目摄像头缺少深度 Train->test->train->test->predicive

  7. JAVA多线程与锁机制

    JAVA多线程与锁机制 1 关于Synchronized和lock synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码 ...

  8. MyBatis中模糊查询

    接口 // 模糊查询 List<User> getUserLike(String value); Mapper.xml文件 <!-- 模糊查询 --> <select i ...

  9. 前端性能监控之performance

    如果我们想要对一个网页进行性能监控,那么使用window.performance是一个比较好的选择. 我们通过window.performance可以获取到用户访问一个页面的每个阶段的精确时间,从而对 ...

  10. .Net5 下Dictionary 为什么可以在foreach中Remove

    在一个讨论群里,看见有人说Dictionary可以在foreach中直接调用Remove了,带着疑问,写了简单代码进行尝试 class Program { static void Main(strin ...