目录
3.1 Spring AOP简介
- Spring的AOP模块是Spring框架体系中很重要的模块,提供了面向切面编程的实现。
3.1.1 AOP(面向切面编程)
它也称面向方面编程,是面向对象编程(OOP)的一种补充,已成为一种比较成熟的编程方式。
- 在传统的业务处理代码中,通常都会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),相同的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须修改所有相关方法。这不但增加了开发人员的工作量,而且提高了代码的出错率。
- 为了解决这一问题,AOP思想随之产生。AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。
- 在AOP思想中,通过Aspect(切面)可以分别在不同类的方法中加入事务、日志、权限和异常等功能。
目前流行的AOP框架有两个
- Spring AOP,Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。
- AspectJ,AspectJ是一个基于Java语言的AOP框架,从Spring 2.0开始,Spring AOP引入了对AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的植入。
3.1.2 AOP术语
在学习使用AOP之前,首先要了解一下AOP的专业术语。这些术语包括Aspect、Joinpoint、Pointcut、Advice、Target Object、Proxy和Weaving,对于这些专业术语的解释,具体如下:
- Aspect(切面):在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、日志等)的类,该类要被Spring容器识别为切面,需要在配置文件中通过< bean>元素指定。,也就是说功能类
- Joinpoint(连接点):在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在Spring AOP中,连接点就是指方法的调用。
- Pointcut(切入点):是指切面与程序流程的交叉点,即那些需要处理的连接点。通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点。,切入点就是需要应用到切面的方法或者类。
- Advice(通知增强处理):AOP框架在特定的切入点执行增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现。
- Target Object(目标对象):是指所有被通知的对象,也称为被增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
- Proxy(代理):将通知应用到目标对象之后,被动态创建的对象。
- Weaving(织入):将切面代码插入目标对象上,从而生成代理对象的过程。
应该可以理解为将切面代码插入目标对象,然后生成一个代理对象执行切面程序。
3.2 AspectJ开发
它是一个基于java语言的AOP框架,从Spring 2.0开始,Spring AOP引入了对AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的植入,允许直接使用AspectJ进行编程,而Spring自身的AOP API也尽量与AspectJ保持一致
使用AspectJ实现AOP有两种方式
- 基于XML的声明式AspectJ
- 基于注解的声明式AspectJ
3.2.1 基于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. 配置切面
配置切面使用的是< aop:aspect>元素,该元素会将一个定义好的Spring Bean转换为切面Bean,所以前提是在applicationContext.xml配置文件中先定义一个普通的Spring Bean,定义完成后,通过元素的ref属性值就可以引用该Bean。
2. 配置切入点
在Spring的配置文件中,切入点是通过< aop:pointcut>元素来定义的。当< aop:pointcut>元素作为< aop:config>元素的子元素定义时,表示该切入点是全局切入点,可以被多个切面所共享;当< aop:pointcut>元素作为< aop:aspect>元素的子元素时,表示该切入点只对当前切面有效。在定义< aop:pointcut>元素时,通常会指定id和expression两个属性,如图
< aop:pointcut>元素的属性及其描述
在上述配置代码片段中,execution(* com.ssm.jdk.* .* (…))就是定义的切入点表达式,该切入点表达式的意思是匹配com.ssm.jdk包中任意类的任意方法的执行。其中execution是表达式的主体,第1个 * 表示的是返回类型,使用 * 代表所有类型;com.ssm.jdk表示的是需要拦截的包名,后面第2个 * 表示的是类名,使用 * 代表所有的类;第3个 * 表示的是方法名,使用 * 表示所有方法;后面的()表示方法的参数,其中的“…”表示任意参数。需要注意的是,第1个 * 与包名之间有一个空格。
上面示例中定义的切入点表达式只是开发中常用的配置方式。
而Spring AOP中切入点表达式的基本格式如下:
execution(modifiers-pattern?
ret-type-pattern declaring-type-pattern?
name-pattern(param-pattern) throws-pattern?
在上述格式中,各部分说明如下:
- 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种常用通知,这些子元素不支持再使用子元素,但在使用时可以指定一些属性,如图
实例1
- 创建项目,导入所需要的jar包,发布到类路径下
- 创建接口USerDao,添加方法
package com.ssm.aspectj;
public interface UserDao {
public void addUser();
public void deleteUser();
}
- 创建UserDao类的实现类,UserDaoImpl
package com.ssm.aspectj;
import org.springframework.stereotype.Repository;
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
// TODO Auto-generated method stub
System.out.println("增加用户");
}
@Override
public void deleteUser() {
// TODO Auto-generated method stub
System.out.println("删除用户");
}
}
我们这里把这个类作为目标类,对其中的方法进行增强处理‘
- 创建一个切面编程包,在其中创建切面类MyAspect,并定义不同的通知
package com.ssm.aspectj.xml;
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.print("后置通知:模拟记录日志..,");
System.out.println("被植入增强处理的目标方法为:" + joinPoint.getSignature().getName());
}
/**
* 环绕通知
* ProceedingJoinPoint是JoinPoint的子接口,表示可执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须throws Throwable
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
//开始
System.out.println("环绕开始:执行目标方法之前,模拟开启事务..,");
//执行当前目标方法
Object obj=proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..,");
return obj;
}
//异常通知
public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
System.out.println("异常通知:出错了"+e.getMessage());
}
//最终通知
public void myAfter(){
System.out.println("最终通知:模拟方法结束后释放资源..");
}
}
分别定义了5种不同类型的通知,在通知中使用了JoinPoint接口及其子接口ProceedingJoinPoint作为参数来获得目标对象的类名、目标方法名和目标方法参数等。
注意1
环绕通知必须接收一个类型为ProceedingJoinPoint的参数,返回值也必须是Object类型,且必须抛出异常。异常通知中可以传入Throwable类型的参数来输出异常信息。
- 创建项目配置文件
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 1 目标类 -->
<bean id="userDao" class="com.ssm.aspectj.UserDaoImpl" />
<!-- 2 切面 -->
<bean id="myAspect" class="com.ssm.aspectj.xml.MyAspect" />
<!-- 3 aop编程 -->
<aop:config>
<!-- 1.配置切面 -->
<aop:aspect id="aspect" ref="myAspect">
<!-- 2.配置切入点 -->
<aop:pointcut expression="execution(* com.ssm.aspectj.*.*(..))" id="myPointCut" />
<!-- 3.配置通知 -->
<!-- 前置通知 -->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!--后置通知-->
<aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal"/>
<!--环绕通知 -->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!--异常通知 -->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e" />
<!--最终通知 -->
<aop:after method="myAfter" pointcut-ref="myPointCut" />
</aop:aspect>
</aop:config>
</beans>
returning=“returnVal”
注意2
在AOP的配置信息中,使用< aop:after-returning>配置的后置通知和使用< aop:after>配置的最终通知虽然都是在目标方法执行之后执行,但它们是有区别的。后置通知只有在目标方法成功执行后才会被植入,而最终通知不论目标方法如何结束(包括成功执行和异常中止两种情况),它都会被植入。另外,如果程序没有异常,异常通知将不会执行。
- 创建测试类
package com.ssm.aspectj.xml;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ssm.aspectj.UserDao;
public class TestXmlAspectJ {
public static void main(String[] args) {
String xmlPath="com/ssm/aspectj/xml/applicationContext.xml";
ApplicationContext applicationContext=new ClassPathXmlApplicationContext(xmlPath);
//从容器中获得内容
UserDao userDao=(UserDao)applicationContext.getBean("userDao");
//执行方法
userDao.addUser();
}
}
运行结果
在执行方法userDao.addUser();时,切面的程序就会响应,在编译的时候就会运行起来。
3.2.2 基于注解的声明式AspectJ
基于XML的声明式AspectJ,有一个缺点就是需要在Spring文件中配置大量的代码信息,为解决这一问题,AspectJ框架为AOP的实现提供了一套注解,用以取代Spring配置文件中为实现AOP功能所配置的臃肿的代码。
AspectJ注解的图示
实例2
- 对MyAspect.java类进行修改
package com.ssm.aspectj.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
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(value="myPointCut()")
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("后置通知:模拟记录日志..,");
System.out.println("被植入增强处理的目标方法为:" + joinPoint.getSignature().getName());
}
/**
* 环绕通知
* ProceedingJoinPoint是JoinPoint的子接口,表示可执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须throws Throwable
*/
@Around("myPointCut()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
//开始
System.out.println("环绕开始:执行目标方法之前,模拟开启事务..,");
//执行当前目标方法
Object obj=proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..,");
return obj;
}
//异常通知
@AfterThrowing(value="myPointCut()",throwing="e")
public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
System.out.println("异常通知:出错了"+e.getMessage());
}
//最终通知
@After("myPointCut()")
public void myAfter(){
System.out.println("最终通知:模拟方法结束后释放资源..");
}
}
首先使用@Aspect注解定义了切面类,由于该类在Spring中是作为组件使用的,因此还需要添加@Component注解才能生效。然后使用@Pointcut注解来配置切入表达式,并通过定义方法来表示切入点名称。接下来在每个通知相应的方法上添加了相应的注解,并将切入点名称“myPointcut”作为参数传递给需要执行增强的通知方法。如果需要其他参数(如异常通知的异常参数),可以根据代码提示传递相应的属性值。
- 在目标类UserDaoImpl中添加注解
@Repository(“userDao”)
package com.ssm.aspectj;
import org.springframework.stereotype.Repository;
@Repository("userDao")
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
// TODO Auto-generated method stub
System.out.println("增加用户");
}
@Override
public void deleteUser() {
// TODO Auto-generated method stub
System.out.println("删除用户");
}
}
- 创建配置文件编辑
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 指定需要扫描的包,使注解生效 -->
<context:component-scan base-package="com.ssm" />
<!-- 启动基于注解的声明式AspectJ支持 -->
<aop:aspectj-autoproxy />
</beans>
首先引入了context约束信息,然后使用元素设置了需要扫描的包,使注解生效。由于此案例中的目标类位于com.ssm.aspectj包中,因此这里设置base-package的值为“com.ssm",最后,使用<aop.aspectj-autoproxy/>来启动Spring对基于注解的声明式Aspect的支持。
- 创建测试类
package com.ssm.aspectj.annotation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ssm.aspectj.UserDao;
public class TestAnnotation {
public static void main(String[] args) {
String xmlPath="com/ssm/aspectj/annotation/applicationContext.xml";
ApplicationContext applicationContext=new ClassPathXmlApplicationContext(xmlPath);
UserDao userDao=(UserDao)applicationContext.getBean("userDao");
userDao.addUser();
}
}
运行结果和上述一致
注意3
如果在同一个连接点有多个通知需要执行(即是同一个连接点配置了多个切面),那么在同一切面中,目标方法之前的前置通知和环绕通知的执行顺序是未知的,目标方法之后的后置通知和环绕通知的执行顺序也是未知的。