AOP简介
什么是AOP
AOP的全称是 Aspect-Oriented Programming,即面向切面编程。在传统的业务处理代码中,通常都会进行事务处理、日志记录等。虽然使用OOP(面向对象编程)可以通过组合或继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),相同的代码仍然会分散到各个方法中。如果想要关闭某个功能,或者对其进行修改,就必须修改所有相关方法。
为了解决这一问题,AOP思想随之产生。AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时再将这些提取出来的代码应用到需要执行的地方。
在AOP思想中,通过Aspect(切面)可以分别在不同类的方法中加入事务、日志、权限和异常等功能。
AOP术语
- Aspect(切面): 在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、日志等)的类,该类要被Spring容器识别为切面,需要在配置文件中通过< bean >元素指定
- Joinpoint(连接点): 在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在Spring AOP中,连接点就是指方法的调用。
- Pointcut(切入点): 是指切面与程序流程的交叉点,即那些需要处理的连接点。通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点。
- Advice(通知增强处理): AOP框架在特定的切入点执行增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现。
- Target Object(目标对象): 是指所有被通知的对象,也称为被增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
- Proxy(代理): 将通知应用到目标对象之后,被动态创建的对象。
- Weaving(织入): 将切面代码插入目标对象上,从而生成代理对象的过程。
AspectJ开发
AspectJ是一个基于Java语言的AOP框架,它提供了强大的AOP功能。 Spring AOP引入了对AspectJ的支持,并允许直接使用AspectJ进行编程。
使用 AspectJ实现AOP有两种方式: 一种是基于XML的声明式AspectJ;另一种是基于注解的声明式AspectJ.
基于XML的声明式AspectJ
基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点及通知。所有的切面、切入点和通知都必须定义在< aop:config >元素内。Spring配置文件中的< beans >元素下可以包含多个< aop:config >元素,一个< aop:config >元素中又可以包含属性和子元素。其子元素包括< aop:pointcut >、< aop:advisor >和< aop:aspect >。在配置时,这3个子元素必须按照此顺序来定义。在< aop:aspect >元素下,同样包含属性和多个子元素,通过使用< aop:aspect >元素及其子元素就可以在XML文件中配置切面、切入点和通知。常用元素的配置代码如下所示。
1.配置切面
在Spring的配置文件中,配置切面使用的是< aop:aspect >元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的Spring Bean(如上述代码中定义的myAspect)。 定义完成后,通过< aop:aspect >元素的ref属性即可引用该Bean;
配置< aop:aspect >元素时,通常会指定id和ref两个属性,如下表所示:
2.配置切入点
在Spring的配置文件中,切入点是通过< aop:pointcut >元素来定义的。当< aop:pointcut >元素作为< aop:config > 元素的子元素定义时,表示该切入点是全局切入点,可以被多个切面所共享; 当< aop:pointcut >元素作为< aop:aspect >元素的子元素时,表示该切入点只对当前切面有效。在定义< aop:pointcut >元素时,通常会指定id和expression两个属性,如下表所示:
在上述配置代码中,execution( * com.ssm.jdk.* . *(…))就是定义的切入点表达式,该切入点表达式的意思是匹配com.ssm.jdk包中任意类的执行方法的执行。其中execution是表达式的主体,第1个 * 表示的是返回类型,使用 * 代表所有类型。 com.ssm.jdk表示的是需要拦截的包名,后面第2个 * 表示的是类名,使用 * 代码所有的类。 第3个 * 表示的是方法名, 使用 * 表示所有方法; 后面的() 表达方法的参数, 其中的"…"表示任意参数。需要注意的是:第1个 * 与包名之间有一个空格。
上面示例中定义的切入点表达式只是开发中常用的配置方法,而Spring AOP中 切入点表达式的基本格式如下:
- modifiers-pattern : 表示定义的目标方法访问修饰符,如public、private等
- ret-type-pattern:表示定义的目标方法的返回值类型,如void、String等
- declaring-type-pattern:表达定义的目标方法的类路径,如com.ssm.jdk.UserDaoImpl
- name-pattern :表示具体需要被代理的目标方法,如add()方法
- param-pattern:表达需要被代理的目标方法包含的参数。
- throws- pattern:表达需要被代理的目标方法抛出的异常类型
注意: 带有问号(?)的部分(如modifiers-pattern、declaring-type-pattern和throws-pattern)表示可选配置项,其他部分属于必须配置项。
3.配置通知
在配置代码中,分别使用< aop: aspect >的子元素配置了5种常用通知,有以下一些属性:
代码实现
需要导入AspectJ框架相关的JAR包,由于spring-aspects-5.0.2.RELEASE.jar 和aspectjwearver-1.8.10.jar,由于在第一节中,在配置Maven项目的时候,已给出所有相关的坐标,这里就不再给出了。
UserDao.java
在src.java目录下创建一个com.ssm.aspect包,在该包中创建接口UserDao,并在接口中编写添加和删除的方法
package com.ssm.aspectj;
public interface UserDao {
//添加用户方法
public void addUser();
//删除用户方法
public void deleteUser();
}
UserDaoImpl.java
在com.ssm.aspectj包中创建UserDao接口的实现类 UserDaoImpl
package com.ssm.aspectj;
public class UserDapImpl implements UserDao{
@Override
public void addUser() {
System.out.println("添加用户");
}
@Override
public void deleteUser() {
System.out.println("删除用户");
}
}
MyAspect.java
在com.ssm.aspectj目录下,创建切面类MyAspect,并在类中分别定义不同类型的通知.
package com.ssm.aspectj;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
//切面类,在此类中编写通知
public class MyAspect {
//前置通知
public void myBefore(JoinPoint joinPoint){
System.out.print("前置通知: 模拟执行权限检查....");
System.out.print("目标类是: "+joinPoint.getTarget());
System.out.println(",被植入增加处理的目标方法为:"+joinPoint.getSignature().getName());
}
//后置通知
public void myAfterReturning(JoinPoint joinPoint){
System.out.println("后置通知: 模拟记录日志");
System.out.println("被植入增加处理的目标方法为:"+joinPoint.getSignature().getName());
}
/**
* 环绕通知:
* proceedingJoinPoint是JoinPoint的子接口,表示可执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJointPoint
* 3.必须throws Throwable
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始
System.out.println("环绕开始: 执行目标方法之前,模拟开启事务....");
Object object = proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务....");
return object;
}
//异常通知
public void myAfterThrowing(JoinPoint joinPoint,Throwable throwable){
System.out.println("异常通知: 出错了"+throwable.getMessage());
}
//最终通知
public void myAfter(){
System.out.println("最终通知: 模拟方法结束后释放资源。。。。");
}
}
在MyAspect .java ,分别定义了5种不同类型的通知,在通知中使用JoinPoint接口及其子接口ProceedingJoinPoint作为参数来获得目标对象的类名、目标方法名和目标方法参数等
注意: 环绕通知必须接收一个类型为ProceedingJoinPoint的参数,返回值也必须是Object类型,且必须抛出异常。
我们可以在环绕通知中,对事务进行控制。 比如下面这几行代码
比如说有这么一个问题,我们如果一次性执行多条SQL语句,第一条执行成功呢?后面几条执行失败了,是不是我们就需要回滚事务?
上述代码中,我们执行方法前,先开启事务,如果方法没有报错,那么就提交事务,否则我们就回滚事务。AOP很好的解决了我们对事务管理。
applicationContext.xml
在使用AOP编程前,需要先导入AOP的约束
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="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
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--1.目标类-->
<bean id="userDao" class="com.ssm.aspectj.UserDapImpl"/>
<!--2.切面-->
<bean id="myAspect" class="com.ssm.aspectj.MyAspect"/>
<!--3.aop编程-->
<aop:config>
<!--3.1配置切面-->
<aop:aspect id="aspect" ref="myAspect">
<!--3.2配置切入点-->
<aop:pointcut id="myPointCut" expression="execution(* com.ssm.aspectj.*.*(..))"/>
<!--前置通知-->
<aop:before method="myBefore" pointcut-ref="myPointCut"/>
<!--后置通知-->
<aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut"/>
<!--环绕通知-->
<aop:around method="myAround" pointcut-ref="myPointCut"/>
<!--异常通知-->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="throwable"/>
<!--最终通知-->
<aop:after method="myAfter" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
注意:
在AOP的配置信息中,使用< aop:after-returning >配置的后置通知和使用< aop:after >配置的最终通知虽然都是在目标方法执行之后执行,但它们是有区别的。后置通知只有在目标方法执行成功后才会被执行,而最终通知不论目标方法如何执行(包括成功执行和异常中止),它都会被执行。另外,如果程序没有异常,异常通知将不会执行。
TestXmlAspectJ.java
在com.ssm.aspectj 下创建测试类TestXmlAspectJ
package com.ssm.aspectj;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestXmlAspectJ {
public static void main(String[] args) {
//1.初始化Spring容器,加载配置文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.从容器中获得userDao实例
UserDao userDao = (UserDao) applicationContext.getBean("userDao");
//3.执行添加用户方法
userDao.addUser();
}
}
执行代码,结果如下:
如果在UserDaoImpl类中的addUser()方法添加错误代码,如"int i = 10/0",后置通知和环绕结束将不会被执行,并且可以看到异常通知的执行。
基于注解的声明式AspectJ
AspectJ注解及其描述,如下表所示
MyAspect.java
在基于XML声明式AspectJ的基础上对MyAspect.java进行修改
package com.ssm.aspectj;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
//切面类,在此类中编写通知
@Aspect
@Component
public class MyAspect {
//定义切入点表达式
@Pointcut("execution(* com.ssm.aspectj.*.*(..))")
//使用一个返回值为void、方法体为空发方法来命名切入点
public void myPointCut(){};
//前置通知
@Before("myPointCut()")
public void myBefore(JoinPoint joinPoint){
System.out.print("前置通知: 模拟执行权限检查....");
System.out.print("目标类是: "+joinPoint.getTarget());
System.out.println(",被植入增加处理的目标方法为:"+joinPoint.getSignature().getName());
}
//后置通知
@AfterReturning("myPointCut()")
public void myAfterReturning(JoinPoint joinPoint){
System.out.println("后置通知: 模拟记录日志");
System.out.println("被植入增加处理的目标方法为:"+joinPoint.getSignature().getName());
}
/**
* 环绕通知:
* proceedingJoinPoint是JoinPoint的子接口,表示可执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJointPoint
* 3.必须throws Throwable
*/
@Around("myPointCut()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始
System.out.println("环绕开始: 执行目标方法之前,模拟开启事务....");
//执行目标对象的方法
Object object = proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务....");
return object;
}
//异常通知
@AfterThrowing(value = "myPointCut()",throwing = "throwable")
public void myAfterThrowing(JoinPoint joinPoint,Throwable throwable){
System.out.println("异常通知: 出错了"+throwable.getMessage());
}
//最终通知
@After("myPointCut()")
public void myAfter(){
System.out.println("最终通知: 模拟方法结束后释放资源。。。。");
}
}
首先使用@Aspect注解定义了切面类,由于该类在Spring中是作为组件使用的,因此还需要添加@Component注解才能生效。然后使用@Pointcut注解来配置切入表达式,并通过定义方法来表示切入点名称。然后再每个通知相应的方法上添加了相应的注解,并将切入点名称"myPointCut"作为参数传递给需要执行增加的通知方法。如果需要其他参数(如异常通知的异常参数),可以根据代码提示传递相应的属性值
UserDaoImpl.java
在目标类com.ssm.aspectj.UserDaoImpl中添加注解@Repository(“userDao”)
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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="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
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--指定需要扫描的包,使注解生效-->
<context:component-scan base-package="com.ssm.aspectj"/>
<!--启动基于注解的声明式AspectJ支持-->
<aop:aspectj-autoproxy/>
</beans>
首先引入了context约束信息,然后使用< context >元素设置了需要扫描的包,使注解生效。由于目标类位于com.ssm.aspectj包中,因此base-package的值为"com.ssm.aspectj"。 最后使用< aop.aspectj-autoproxy />来启动Spring对基于注解的声明式Aspect的支持。
其余方法均和基于XML的声明式AspectJ一致。
基于注解的方式与基于XML的方式执行结果相同,只是在目标方法前后通知的执行顺序发生了变化。相对来说,使用注解的方式更加简单、方便。
注意
如果在同一个连接点有多个通知需要执行,那么在同一切面中,目标方法之前的前置通知和环绕通知的执行顺序是未知的,目标方法之后的后置通知和环绕通知的执行顺序也是未知。