Spring—面向切面编程(AOP)详解

作者:我要进阿里!
原文:https://www.cnblogs.com/liuhongchen/p/11580319.html?utm_source=tuicool&utm_medium=referral

声明:本问仅仅是一个初学者的学习记录、心得总结,其中肯定有许多错误,不具有参考价值,欢迎大佬指正,谢谢!

Spring—面向切面编程(AOP)详解

一、问题引入

​ 在日常写项目的时候,肯定少不了要打印日志。例如,要向数据库中insert一个用户,我想在插入前输出一下相关信息,怎么实现呢?最基本的做法是:在insert方法中写日志输出语句。这样写完全能实现功能,但是会不会显得很冗余?耦合度是不是很高?编程的准则是“高内聚,低耦合”,低耦合的意思就是类与类之间的依赖关系尽量少、关联程度尽量小。

​ 而如果在上述情景中使用面向切面编程(AOP),就可以不在insert方法中写日志输出语句却能实现日志输出功能。当然,AOP不止如此。

二、概念引入

1.AOP

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方 式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个 热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑 的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高 了开发的效率。

2.几个基本概念

- 切入点:所有要操作的方法定义,要求业务层方法风格统一
- 分离点:将不可分割的组件单独提取出去定义为单独的操作功能
- 横切关注点:将所有与开发无关的程序组成类单独提取后组织运行
- 织入:将所有切入点、关注点的代码组成在一张完整的程序结构中

3.通知(Advice)

​ AOP是通过通知来实现功能的,有如下五种:

  • 前置通知(BeforeAdvice)
  • 后置通知(AfterAdvice)
  • 后置返回通知(AfterReturningAdvice)
  • 后置异常通知(AfterThrowingAdvice)
  • 环绕通知(AroundAdvice)

三、Pointcut与Execution表达式

​ pointcut使用execution表达式表示要被切入的方法(即定义切入点)。

​ execution表达式,功能类似于正则表达式,都是用来匹配筛选,只不过正则表达式用来筛选字符串,而execution表达式用来筛选要被切入的方法。

​ execution表达式的格式为:

​execution(<注解>? <修饰符>? <返回值类型> <方法名模式>(<参数模式>) <异常>?)) <and args()>?)

​ 例:execution(@Deprecated public Void aop.MyAspect.hello(int,String) throws Exception))')

package aop;
public class AspectDemo {
 @Deprecated
 public void hello(int i,String s) throws Exception{
 }
}

​ 其实不难发现,这个表达式和我们声明的方法的各个部分一一对应

  • 注解:(可省略)例如上面代码中的@Deprecated ,就是筛选带有该注解的方法
  • 修饰符(可省略)
  • public
  • protected
  • private
  • 当然一般用通配符 *
  • 返回值类型
  • 写各种返回值,一般用通配符 *
  • 方法名模式
  • 包名部分:在上例中,AspectDemo是位于aop包中的,所以可以通过包名.包名.类名的格式来定位到某个类,例如aop.AspectDemo 中aop. 就是包名部分;
  • 当然也可以用通配符
  • *:匹配任何数量字符,例如service.*.UserService 表示的是service的直接子包
  • ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包,例如service..代表着匹配service及其包含的所有包;而在方法参数模式中匹配任何数量参数。
  • +: 匹配指定类型的子类型;仅能作为后缀放在类型模式后边,例如java.lang.Number+ 表示的是lang包下Numer的子类
  • 类名部分:在上例中aop.AspectDemo中aop.是包名部分,AspectDemo就是类名部分,可以用通配符来表示,*用的比较多
  • 参数模式
  • 写法1:直接按照方法的参数列表写具体类型,上例的方法中参数列表(int i,String s),就可以直接在表达式中写(int,String)
  • 写法2:使用通配符:
  • “()”表示方法没有任何参数;
  • “(..)”表示匹配接受任意个参数的方法
  • “(..,java.lang.String)”表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法
  • “(java.lang.String,..)” 表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法
  • “(*,java.lang.String)” 表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法;
  • 异常模式(可省略)
  • throws Exception1,Exception2.。。。
  • 传入参数(可省略)
  • ​ and args(arg-name),一般用于AfterAdvice和Around通知

四、前期准备

  1. 创建项目,导入相关jar包,参考Spring——IOC,此外还需导入aop和aspectj的jar包
  2. 创建applicationContext.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:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="
 http://www.springframework.org/schema/aop
 http://www.springframework.org/schema/aop/spring-aop.xsd
 http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/context
 http://www.springframework.org/schema/context/spring-context.xsd">
 <aop:aspectj-autoproxy/>
</beans>

​ 注意:新增了

​xmlns:aop="http://www.springframework.org/schema/aop"

​http://www.springframework.org/schema/aop

​http://www.springframework.org/schema/beans/spring-aop.xsd

​<aop:aspectj-autoproxy/> 不加这个可能会报错,可坑了

  1. 创建UserService这个类,内部有insert方法用来注册用户
public class UserService { 
 public void insert(){
 System.out.println("UserService正在注册用户……");
 }
}
  1. 创建MyAspect类
public class MyAspect {}

五、基于XML配置的AOP

1.BeforeAdvice

​ (1)在MyAspect类中创建方法beforeAdvice

public void beforeAdvice(){
 System.out.println("【AOP】Before Advice正在执行……");
 }

​ (2)在applicationContext.xml中配置

关于pointcut和execution表达式见下文

<!--首先要引入myAspect这个bean,备用-->
 <bean id="myAspect" class="aop.MyAspect"/>
 <bean id="userService" class="aop.UserService"/>
 <aop:config>
 <!--配置切面,一个aop:aspect标签对应一个Aspect类-->
 <aop:aspect id="beforeAdvice" ref="myAspect">
 <!--配置通知 method对应MyAspect类中定义的方法,pointcut是切入点表达式用于筛选需要被 切入的方法-->
 <aop:before method="beforeAdvice" pointcut="execution(* aop..*.*(..)))"/>
 </aop:aspect>
 </aop:config>

​ (3)编写测试类

public class UserServiceTest {
 public static void main(String[] args) {
 ApplicationContext context=
 new ClassPathXmlApplicationContext("applicationContext.xml");
 UserService userService = context.getBean("userService", UserService.class);
 userService.insert();
 }
}

​ (4)输出结果

【AOP】Before Advice正在执行……
 UserService正在注册用户……

​ 可以发现,BeforeAdvice就已经实现了

2.AfterAdvice(相当于异常里面的finally语句)

​ (1)UserService类同上

​ (2)在MyAspect中创建方法afterAdvice

public void afterAdvice(){
 System.out.println("【AOP】after Advice…… 不管怎样我都会执行");
}

​ (3)修改applicationContext.xml

<aop:config>
 <aop:aspect id="beforeAdvice" ref="myAspect">
 <aop:after method="afterAdvice"
 pointcut="execution(* aop.*.insert(..)))" />
 </aop:aspect>
</aop:config>

​ (4)编写测试类(同上)

​ (5)输出结果

UserService正在注册用户……
【AOP】after Advice…… 不管怎样我都会执行

3.AfterReturningAdvice

​ (1)修改UserService的insert方法,使其有返回值

public class UserService {
 public int insert(){
 System.out.println("UserService正在注册用户……");
 return 1;
 }
}

​ (2)在MyAspect中新增afterReturningAdvice方法

public void afterReturningAdvice(int result) {
 System.out.println("【AOP】after advice……返回值为"+result);
 }

​ (3)在applicationContext.xml中配置

<aop:config>
 <aop:aspect id="beforeAdvice" ref="myAspect">
 <aop:after-returning method="afterReturningAdvice"
 pointcut="execution(* aop.*.insert(..)))" returning="result"/>
 </aop:aspect>
</aop:config>

​ 注意:这里这个returning="result"与MyAspect类中对应方法的参数名必须保持一致,本例中都为result

​ (4)编写测试类(代码同1.)

​ (5)输出结果

UserService正在注册用户……
 【AOP】after advice……返回值为1

4.AfterThrowingAdvice

​ (1)修改UserService使其抛异常

public int insert() throws Exception {
 try {
 System.out.println("UserService开始注册用户……");
 int i=1/0;
 }catch (Exception e){
 throw new Exception("insert方法遇到异常……");
 }
 return 1;
}

​ (2)在MyAspect中新增方法 afterThrowingAdvice

//这里传入的这个Exception就是捕获到的异常对象
public void afterThrowingAdvice(Exception e){
 System.out.println("【AOP】得到异常信息:"+e.getMessage());
}

​ (3)修改applicationContext.xml

<aop:config>
 <aop:aspect id="beforeAdvice" ref="myAspect">
 <aop:after-throwing method="afterThrowingAdvice"
 pointcut="execution(* aop.*.insert(..)))" throwing="e"/>
 </aop:aspect>
</aop:config>

​ 注意:这里的throwing="e”就是跑出的异常对象的名字,要与MyAspect中afterThrowingAdvice方法中传入的参数Exception e的名字保持一致。

​ (4)编写测试类

public static void main(String[] args) throws Exception {
 ApplicationContext context=
 new ClassPathXmlApplicationContext("applicationContext.xml");
 UserService userService = context.getBean("userService", UserService.class);
 userService.insert();
}

​ (5)输出结果

UserService开始注册用户……
【AOP】得到异常信息insert方法遇到异常……
Exception in thread "main" java.lang.Exception: insert方法遇到异常……
 at aop.UserService.insert(UserService.java:12)
 at aop.UserService$$FastClassBySpringCGLIB$$7e3b8e5e.invoke(<generated>)
 ...

5.AroundAdvice

​ (1)修改UserService中的insert方法

public int insert(int arg) throws Exception {
 try {
 int i = 1 / 0;
 } catch (Exception e) {
 throw new Exception("insert方法遇到异常……");
 }
 return 1;
}

​ (2)在MyAspect中添加方法AroundAdvice

//这里这个ProceedingJointPoint可以理解为切入点对象,可以通过它获取切入点(被切入的方法)的参数、返回值、抛出的异常,并且可以通过pjp.proceed(args);为该切入点设置参数
public int aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
 Object[] args = pjp.getArgs();
 System.out.println("【AOP】before Advice,获取到insert方法传入的参数为:"+args[0]);
 Object result;
 try {
 result=pjp.proceed(args);//这里是我们手动执行切入点,并传入参数
 System.out.println("【AOP】after Returning Advice,返回值为:"+result);
 }catch (Exception e){
 //这里捕获的就是切入点运行时抛出的异常
 System.out.println("【AOP】after Throwing Advice,错误信息为:"+e.getMessage());
 }
 System.out.println("【AOP】after advice……不管异常不异常我都执行");
 //这个就跟着这样写吧。。如果不写返回值的话会报 null return value does not match...
 return 1;
}

​ (3)修改applicationContext.xml文件

<aop:config>
 <aop:aspect id="beforeAdvice" ref="myAspect">
 <aop:around method="aroundAdvice"
 pointcut="execution(* aop.*.insert(..)))" />
 </aop:aspect>
</aop:config>

​ (4)编写测试类

public static void main(String[] args) throws Exception {
 ApplicationContext context=
 new ClassPathXmlApplicationContext("applicationContext.xml");
 UserService userService = context.getBean("userService", UserService.class);
 userService.insert(2);
}

​ (5)输出结果

【AOP】before Advice,获取到insert方法传入的参数为:2
【AOP】after Throwing Advice,错误信息为:insert方法遇到异常……
【AOP】after advice……不管异常不异常我都执行

六、基于注解配置的AOP

​ 先把项目状态恢复到 “四、前期准备”的状态,然后在applicationContext.xml中添加下面的语句开启注解和包扫描。

<context:annotation-config/>
<context:component-scan base-package="aop"/>

​ 注意,这个base-package可以配置多个包,以半角(英文)逗号隔开,例如“aop,mvc,dao,service”,当然,为了省事,可以直接配一个顶级包,他会自动遍历扫描所有的子包及子包的子包等等。

​ 然后为MyAspect类和UserService类加上注解

@Component
@Aspect
public class MyAspect {}
@Service
public class UserService {}

1.BeforeAdvice

​ (1)在MyAspect类中创建beforeAdvice方法,并写好注解

@Before(value = "execution(* aop..*.*(..)))")
public void beforeAdvice(){
 System.out.println("【AOP】Before Advice正在执行……");
}

​ 不需要配任何bean,是不是很爽

​ (2)编写测试类

public static void main(String[] args) {
 ApplicationContext context=
 new ClassPathXmlApplicationContext("applicationContext.xml");
 UserService userService = context.getBean("userService", UserService.class);
 userService.insert();
}

​ (3)输出结果

【AOP】Before Advice正在执行……
 UserService正在注册用户……

2.AfterService(我就不写测试了)

​ (1)在MyAspect类中创建afterAdvice方法,并写好注解

@After(value = "execution(* aop..*.*(..)))")
public void afterAdvice(){
 System.out.println("【AOP】after Advice正在执行……");
}

3.AfterReturningAdvice

​ (1)修改UserService中的insert方法

public int insert(){
 System.out.println("UserService正在注册用户……");
 return 1;
}

​ (2)在MyAspect类中创建afterReturningAdvice方法,并写好注解

@AfterReturning(value = "execution(* aop..*.*(..))&& args(result))")
public void afterReturningAdvice(int result){
 System.out.println("【AOP】after Returning Advice正在执行……返回值为:"+result);
}

​ (3)不写测试了

4.AfterThrowingAdvice

​ (1)修改UserService中的insert方法

public int insert() throws Exception {
 try {
 int i=1/0;
 }catch (Exception e){
 throw new Exception("【UserService】的insert遇到了错误……");
 }
 return 1;
}

​ (2)在MyAspect类中创建afterThrowingAdvice方法,并写好注解

@AfterThrowing(value = "execution(* aop..*.*(..)))",throwing = "e")
public void afterThrowingAdvice(Exception e){
 System.out.println("【AOP】after Throwing Advice正在执行……错误信息为:"+e.getMessage());
}

​ (3)不测试了。。

5.AroundAdvice

​ (1)把MyAspect中之前写的方法注释掉,不然会影响观察结果

​ (2)修改insert方法

public int insert(int arg) throws Exception {
 try {
 int i=1/0;
 }catch (Exception e){
 throw new Exception("【UserService】的insert遇到了错误……");
 }
 return 1;
}

​ (3)在MyAspect类中创建aroundAdvice方法,并写好注解

@Around(value = "execution(* aop..*.*(..)))")
public int aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
 Object[] args = pjp.getArgs();
 System.out.println("【AOP】before Advice,获取到insert方法传入的参数为:"+args[0]);
 Object result;
 try {
 result=pjp.proceed(args);
 System.out.println("【AOP】after Returning Advice,返回值为:"+result);
 }catch (Exception e){
 System.out.println("【AOP】after Throwing Advice,错误信息为:"+e.getMessage());
 }
 System.out.println("【AOP】after advice……不管异常不异常我都执行");
 return 1;
}

关注作者:JAVA高级程序员

专注分享:(高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等技术...)

欢迎转发,评论~~~每天Java一下,成为架构师!

相关推荐