概念
AOP : Aspect Oriented Programming 即: 面向切面编程。
简单的说,就是把程序中重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的
基础上,对已有方法进行增强。
一些术语
先了解一些术语:
- Joinpoint(连接点):
指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。 - Pointcut(切入点):
是指要对哪些 Joinpoint 进行拦截的定义。 - Advice(通知/增强):
通知指拦截到 Joinpoint 之后,所要做的事情。
通知的类型: 前置通知,后置通知,异常通知,最终通知,环绕通知。 - Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方
法或 Field。 - Target(目标对象):
代理的目标对象。 - Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。 - Proxy(代理) :
一个类被 AOP 织入增强后,就产生一个结果代理类。 - Aspect(切面):
是切入点和通知(引介)的结合 。
AOP入门
首先得学会怎么用——这是开发阶段需要关注的。
其次得明白其背后的机制——这部分之后将深入了解。
-
开发阶段
编写核心业务代码(开发主线)。
把公用代码抽取出来,制作成通知。
在配置文件中,声明切入点与通知间的关系,即切面。 -
运行阶段
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
Spring基于XML的AOP
项目准备
结构:
导坐标:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
必要代码(模拟):
业务层接口与实现:
/**
* 账户的业务层接口
*/
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
/**
* 模拟更新账户
* @param i
*/
void updateAccount(int i);
/**
* 删除账户
* @return
*/
int deleteAccount();
}
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService{
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新:"+i);
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
公共代码,这里关注日志记录:
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的pringLog()开始记录日志了。。。");
}
}
基于XML的AOP配置步骤
首先业务bean当然要配:
<bean id="accountService" class="com.zhu.service.impl.AccountServiceImpl"></bean>
接下来配置AOP:
- 1.把通知Bean也交给spring来管理
<!-- 配置Logger类 -->
<bean id="logger" class="com.zhu.utils.Logger"></bean>
-
2.使用 aop:config 标签表明开始AOP的配置
-
3.使用 aop:aspect 标签表明配置切面
id属性:给切面提供一个唯一标识
ref属性:指定通知类bean的Id
<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
- 4.在aop:aspect标签的内部使用对应标签来配置通知的类型
当前示例是让printLog方法在切入点方法执行之前,所以属于 前置通知 类型
aop:before :表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定 切入点表达式 ,该表达式的含义指的是对业务层中哪些方法增强
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="切入点表达式"></aop:before>
切入点表达式的写法:
execution(表达式)
表达式:
访问修饰符 返回值 包名.类名.方法名(参数列表)
标准的表达式写法:
public void com.zhu.service.impl.AccountServiceImpl.saveAccount()
完整配置如下:
<?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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置spring的Ioc,把service对象配置进来-->
<bean id="accountService" class="com.zhu.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="com.zhu.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(public void com.zhu.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
配置完毕,测试一下:
package com.zhu.test;
import com.zhu.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:bean.xml")
public class AOPTest {
@Autowired
private IAccountService accountService;
@Test
public void testService() {
accountService.saveAccount();
}
}
可以看到在执行保存方法前打印了日志。
切入点表达式的其他写法
上面提到切入点表达式的标准写法:
访问修饰符 返回值 包名.类名.方法名(参数列表)
public void com.zhu.service.impl.AccountServiceImpl.saveAccount()
若切入点非常多,岂不是很繁琐?
首先,访问修饰符是可以省略的:
返回值 包名.类名.方法名(参数列表)
void com.zhu.service.impl.AccountServiceImpl.saveAccount()
其次,返回值可以使用通配符,表示任意返回值:
* 包名.类名.方法名(参数列表)
* com.zhu.service.impl.AccountServiceImpl.saveAccount()
更进一步,包名可以使用通配符,表示任意包,但是有几级包,就需要写几个*。
本例中得写4个:
* *.类名.方法名(参数列表)
* *.*.*.*.AccountServiceImpl.saveAccount()
其实,包名可以使用…表示当前包及其子包:
* *..类名.方法名(参数列表)
* *..AccountServiceImpl.saveAccount()
// 这么写表示任意包下的AccountServiceImpl类的这个方法都会成为切入点
更进一步,类名和方法名都可以使用*来实现通配:
* *..*.*(参数列表)
* *..*.*()
如果写到这里,测试imp中的三个方法,将有2个方法被增强,因为还有一个方法是有参的:
参数列表可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
当然,目标范围还能进一步扩大——
可以使用通配符表示任意类型,但是必须有参数
可以使用…表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
写成这样,表示任意方法都会被作为切入点(当然实际中不能这么做,一般写到具体类即可):
四种常用通知类型
上面的例子中用到了前置通知,上文提到通知共有4中类型,下面继续实例演示其他通知类型的配置方法。
先修改Logger这个类,提供四种通知方法:
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知:Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("后置通知:Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("异常通知:Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("最终通知:Logger类中的afterPrintLog方法开始记录日志了。。。");
}
}
上文使用aop:before标签配置了前置通知,其他类型的当然也有各自的标签:
aop:after-returning 后置通知
aop:after-throwing 异常通知
aop:after 最终通知
此外,为了解决每个通知都要写切入点表达式的问题,可以使用aop:pointcut标签进行切入点表达式的配置:
id属性用于指定表达式的唯一标识,expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用。
<aop:pointcut id="pt1" expression="execution(* com.zhu.service.impl.*.*(..))"></aop:pointcut>
这样一来,就能在配置具体通知时使用pointcut-ref属性来引入表达式,解决了重复写的问题。
完整的配置如下:
<!-- 配置Logger类 -->
<bean id="logger" class="com.zhu.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!-- 配置切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.zhu.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut-ref="pt1" ></aop:before>
<!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
测试业务中的其中一个方法:
可见3个通知被执行了,因为后置通知和异常通知只能执行一个。
现在可以模拟一个异常:
重新测试:
可以看到后置通知被替换为异常通知。
环绕通知
环绕通知,是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
Spring框架为我们提供了一个接口: ProceedingJoinPoint。该接口有一个方法proceed(),此方法可以明确调用切入点方法。
先在Logger类中添加环绕通知方法:
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
beforePrintLog(); // 前置
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
afterReturningPrintLog(); //后置
return rtValue;
}catch (Throwable t){
afterThrowingPrintLog(); // 异常
throw new RuntimeException(t);
}finally {
afterPrintLog(); //最终
}
}
并且配置AOP,使用 aop:around 标签配置环绕通知:
(为了避免混淆把之前配的通知先去掉,业务中的异常也注释掉)
<!--配置AOP-->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.zhu.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置环绕通知 -->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
这样就配置好了,测试一下:
Spring 的环绕通知和前置通知,后置通知有着很大的区别,主要有两个重要的区别:
1) 目标方法的调用由环绕通知决定,即你可以决定是否调用目标方法,而前置和后置通知是不能决定的,他们只是在方法的调用前后执行通知而已,即目标方法肯定是要执行的。
2)环绕通知可以控制返回对象,即你可以返回一个与目标对象完全不同的返回值,虽然这很危险,但是你却可以办到。而后置方法是无法办到的,因为他是在目标方法返回值后调用。
Spring基于注解的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: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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.zhu"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
可以看出,xml中只保留两个配置即可,那么省略掉的将使用注解来完成。
接下来在通知类上使用 @Aspect 注解声明为切面:
@Aspect
public class Logger {
使用@Pointcut指定切入点表达式:
@Pointcut("execution(* com.zhu.service.impl.*.*(..))")
private void pt1(){}
接着逐一在增强的方法上使用注解配置通知。
/**
* 前置通知
*/
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知:Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知:Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知:Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知:Logger类中的afterPrintLog方法开始记录日志了。。。");
}
同理,环绕通知使用@Around注解即可。
如何使用纯注解呢?
用@EnableAspectJAutoProxy来代替xml中的配置就好。
@Configuration
@ComponentScan(basePackages="com.zhu")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}