Spring AOP
AOP并不是Spring框架所特有的,Spring只是支持AOP编程的框架之一。前面已经谈到过,Spring只支持方法拦截的AOP,在Spring中有4种方式去实现AOP的拦截器:
- 使用ProxyFactoryBean和对应的接口实现AOP
- 使用xml配置AOP
- 使用@AspectJ注解驱动切面
- 使用AspectJ注入切面
在四种方式中,真正常用的是用@AspectJ注解的方式去实现切面,有时xml配置也会用在一些遗留项目中或者起辅助作用,而其它两种方式基本不会使用,所以我们就对使用@AspectJ注解驱动切面和使用xml配置AOP做出讨论。
使用@AspectJ注解开发Spring AOP
使用@AspectJ注解方式已经成为主流,所以先以@AspectJ注解方式详细讨论Spring AOP的开发,有了对@AspectJ注解实现的理解,其它的方式都是大同小异的。先看一下关键步骤:
<?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/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 自动扫描+注解驱动:通过注解进行自动装配 <context:component-scan base-package="edu.uestc.avatar"/> base-package:spring自动扫描该包及子包的bean --> <context:component-scan base-package="edu.uestc.avatar"/> <!-- 开启自动代理 --> <aop:aspectj-autoproxy/> </beans>
选择连接点
spring是方法级别的AOP框架,我们主要以某个类的某些方法作为连接点,用动态代理的理论来说,就是要拦截哪些方法织入对应AOP通知。为了更好地测试,先建立一个业务接口
package edu.uestc.avatar.service; import edu.uestc.avatar.domain.Book; import java.util.List; public interface BookService { Integer save(Book book); void removeById(Integer id); void update(Book book); Book findById(Integer id); List<Book> list(); }
接下来提供一个实现类
package edu.uestc.avatar.service.impl; import edu.uestc.avatar.dao.BookDao; import edu.uestc.avatar.domain.Book; import edu.uestc.avatar.service.BookService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.atomic.AtomicInteger;
@Service public class BookServiceImpl implements BookService { private AtomicInteger idGenerator = new AtomicInteger(); @Autowired private BookDao dao; @Override public Integer save(Book book) { System.out.println("save()执行"); var id = idGenerator.incrementAndGet(); book.setId(id); dao.save(book); return id; } @Override public void removeById(Integer id) { System.out.println("removeById()执行"); if(id == 0) //测试异常通知 throw new IllegalArgumentException("id不能为0"); dao.removeById(id); } @Override public void update(Book book) { System.out.println("update()执行"); dao.update(book); } @Override public Book findById(Integer id) { System.out.println("findById()执行"); return dao.findById(id); } @Override public List<Book> list() { System.out.println("list()执行"); return dao.list(); } }
现在将业务实现类里的所有方法作为AOP的连接点,用动态代理的语言就是要为类BookServiceImpl生成代理对象,然后拦截这些业务方法,于是可以产生各种AOP通知方法。
创建切面(切点+增强引介)
选择好了连接点就可以创建切面了,对于动态代理语言而言,它就如同一个拦截器,在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面了。
package edu.uestc.avatar.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; /** * 定义切面:切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义 * 在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面类 */ @Component @Aspect public class ServiceAspect { /** * 定义切点@Pointcut:哪些方法要被拦截 * @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))") * execution:代表执行方法的时候会触发 * *:任意的返回类型 * edu.uestc.avatar.service.*.*:位于edu.uestc.avatar.service下所有接口及实现了该接口的子包下的所有方法 * (..):任意的参数 */ @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))") public void pointcut(){ } /** * 定义前置通知:在被代理对象调用前调用 */ @Before("pointcut()") public void before(){ System.out.println("前置通知:开启事务"); } /** * 后置通知:在被代理对象调用后调用 */ @AfterReturning(pointcut = "pointcut()") public void afterReturning(){ System.out.println("后置通知:提交事务"); } /** * 例外通知(异常通知) * 如何得到异常对象 */ @AfterThrowing(pointcut = "pointcut()",throwing = "th") public void afterThrowing(Throwable th){ System.out.println("例外通知:记录异常信息==>" + th); } @After("pointcut()") public void after(){ System.out.println("最终通知:释放资源"); } /** * 环绕通知: * 区别:有权决定对目标对象调用与否(权限拦截) * 改变最终的返回值(最好不要这样干) * @param pjd:连接点(被拦截到的方法) */ @Around("pointcut()") public Object around(ProceedingJoinPoint pjd) throws Throwable { Object ret = null; System.out.println("环绕通知:" + pjd); ret = pjd.proceed();//调用连接点方法 System.out.println("方法返回值:" + ret); return ret;//可以改变方法的最终返回值 } }
定义切点
确定连接点。在上面的代码清单@Pointcut注解中定义了execution的正则表达式,spring正是通过这个正则表达式判断是否需要拦截业务方法的。这个表达式如下:
execution(* edu.uestc.avatar.service.*.*(..))具体含义见代码清单注释。
AspectJ指示器 | 描述 |
arg() | 限制连接点匹配参数为指定类型的方法 |
@args() | 限制连接点匹配参数为执行注解标注的执行方法 |
execution() | 匹配连接点的执行方法,最常用的匹配,可以使用类似于代码清单中的正则表达式进行匹配 |
this() | 限制连接点匹配AOP代理的Bean,引用为指定类型的类 |
target() | 限制连接点匹配被代理对象为指定的类型 |
@target() | 限制连接点匹配特定的执行对象,这些对象要符合指定的注解类型 |
within() | 限制连接点匹配指定的包 |
@within() | 限制连接点匹配指定的类型 |
@annotation() | 限制匹配带有指定注解的连接点 |
测试AOP
package edu.uestc.avatar.service; import edu.uestc.avatar.domain.Book; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; class BookServiceTest { private static ApplicationContext context; private static BookService service; @BeforeAll public static void before(){ context = new ClassPathXmlApplicationContext("spring-beans.xml"); service = context.getBean("bookService",BookService.class); } @Test void save() { var book = new Book().setTitle("射雕英雄传-jdbc") .setAuthor("金庸") .setMarketPrice(300f).setSellPrice(180f).setBuyPrice(100f) .setPublisher("三联出版社"); //通过spring IoC容器获取bean System.out.println(service.save(book)); } @Test void removeById() { service.removeById(22); } @Test void update() { var book = service.findById(21); book.setTitle("spring data jdbc"); service.update(book); } @Test void findById() { System.out.println(service.findById(21)); } @Test void list() { service.list().forEach(System.out::println); } }
注意观察运行结果
织入
织入是生成代理对象并将切面内容放入约定流程的过程。在上述代码中,连接点所在的类都是拥有接口的类,而事实上即使没有接口,Spring也能提供AOP的功能,所以是否拥有接口不是使用Spring AOP的一个强制要求。目标类如果实现了接口,Spring使用JDK的动态代理技术生成代理对象;如果目标类没有实现接口,Spring使用CGLib来生成代理对象。从而织入各个通知。
动态代理对象是由Spring IoC容器根据描述生成的,一般不需要修改它,对于使用者而言,只要知道AOP属于中的约定就可以使用AOP了,只是在Spring中建议使用接口编程,这样的好处是使定和实现分离,有利于实现变化和替换,更为灵活。
给通知传递参数
package edu.uestc.avatar.aop; import edu.uestc.avatar.annotation.Privilege; import edu.uestc.avatar.domain.Book; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; /** * 定义切面:切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义 * 在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面类 */ @Component @Aspect public class ServiceAspect { /** * 定义切点@Pointcut:哪些方法要被拦截 * @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))") * execution:代表执行方法的时候会触发 * *:任意的返回类型 * edu.uestc.avatar.service.*.*:位于edu.uestc.avatar.service下所有的子包下的所有方法 * (..):任意的参数 */ @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))") public void pointcut(){ } /** * 定义前置通知:在被代理对象调用前调用 * 向前置通知传递参数 */ @Before("pointcut() && args(book)") public void before(Book book){ System.out.println("前置通知:开启事务" + book); } /** * 后置通知:在连接点调用后调用 * */ @AfterReturning(pointcut = "pointcut()",returning = "retValue") public void afterReturning(Integer retValue){ System.out.println("后置通知:提交事务,返回结果:" + retValue); } /** * 例外通知(异常通知) * 如何得到异常对象 */ @AfterThrowing(pointcut = "pointcut()",throwing = "th") public void afterThrowing(Throwable th){ System.out.println("例外通知:记录异常信息==>" + th); } @After("pointcut()") public void after(){ System.out.println("最终通知:释放资源"); } /** * 环绕通知: * 区别:有权决定对目标对象调用与否(权限拦截) * 改变最终的返回值(最好不要这样干) * @param pjd:连接点(被拦截到的方法) * * 拦截标注了@Privilege方法,意味着这些方法需要对应的权限 */ @Around("pointcut() && @annotation(privilege)") public Object around(ProceedingJoinPoint pjd, Privilege privilege) throws Throwable { Object ret = null; System.out.println(pjd.getSignature() + "需要出示权限:" + privilege.permission()); System.out.println("环绕通知:" + pjd); //if(当前登录用户拥有privilege.permission()) { ret = pjd.proceed();//调用连接点方法 System.out.println("方法返回值:" + ret); //} return ret;//可以改变方法的最终返回值 } }
引入(为已有的类添加新的接口)
Spring AOP只是通过动态代理技术,把各类通知织入到它所约定的流程中,而事实上,有时候我们希望通过引入其他类的方法来获得更好的实现,这时候就可以引入其它的方法了。比如业务类中的save方法要求,如果当图书为空时则不再进行保存,那么可以引入一个新的检测器对其进行检测。先定义一个检测接口:
public interface BookVerifier{ //检测book对象是否不为空 default boolean verify(Book book){ return book != null; } }
可以在切面类中加入一个新的属性,用于检测Book是否为空
使用XML配置AOP
原理和使用注解是相同的,所以介绍一些用法即可。需要在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/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="edu.uestc.avatar"/> <!-- 开启aop的@Aspectj的支持 --> <!--<aop:aspectj-autoproxy/>--> <!-- 通过xml配置AOP --> <aop:config> <!-- 定义切面(切点 + 通知) --> <aop:aspect ref="serviceAspect2"> <!-- 配置切点 --> <aop:pointcut id="pointcut" expression="execution(* edu.uestc.avatar.service.*.*(..))"/> <!-- 前置通知--> <aop:before method="before" pointcut="execution(* edu.uestc.avatar.service.*.*(..)) and args(book)"/> <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="retValue"/> <aop:after method="after" pointcut-ref="pointcut"/> <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="th"/> <aop:around method="around" pointcut="execution(* edu.uestc.avatar.service.*.*(..)) and @annotation(privilege)"/> </aop:aspect> </aop:config> </beans>