基本使用步骤
假设一个场景,有一个业务层,用于对账户的保存和更新删除,但是我要在对保存、更新等操作的时候打印日志该怎么办?
如果我们直接在业务层的代码上添加,就相当于加了无关紧要的代码。所以这里可以用 Spring 的 AOP 方式,在原有的业务层代码进行增强。业务层代码 AccountService.java 和 切面类 Logger.java代码如下
IAccountService.java 接口
/**
* 账户的业务层
*/
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
/**
* 更新账户
* @param i
*/
void updateAccount(int i);
/**
* 删除账号
*/
int deleteAccount();
}
AccountService.java
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新");
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
Logger.java
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("beforePrintLog打印日志中。。。。。。。。。");
}
/**
* 后置通知
*/
public void afterPrintLog(){
System.out.println("afterPrintLog打印日志中。。。。。。。。。");
}
/**
* 异常通知
*/
public void throwPrintLog(){
System.out.println("throwPrintLog打印日志中。。。。。。。。。");
}
/**
* 返回通知
*/
public void returnPrintLog(){
System.out.println("returnPrintLog打印日志中。。。。。。。。。");
}
/**
* 环绕通知
*/
public Object aroundPrintLog(){
.....
}
}
接下来在 Spring 的配置文件中书写
-
把通知的 bean(AccountService) 交给 Spring 管理。
<bean id="accountService" class="com.main.service.impl.AccountServiceImpl"></bean>
-
配置切面类(Logger)的 bean 对象,交给 Spring 管理,因为切面类也是需要生成为对象的。
<bean id="logger" class="com.main.utils.Logger"></bean>
-
使用
<aop:config></aop:config>
开始配置AOP<aop:config></aop:config>
-
使用
<aop:aspect></aop:aspect>
标签配置哪个类作为切面类<aop:config> <aop:aspect id="logAdvice" ref="logger"> ...... </aop:aspect> </aop:config>
-
接下来,我们就可以
<aop:aspect>
标签中,写我们需要配置的通知,通知分为五种
先介绍一下,关于 切入点表达式的写法
切入点表达式
-
语法
execution(表达式)
-
表达式写法
- 例子:
public void com.main.service.impl.AccountServiceImpl.saveAccount(int);
- 对应:
访问修饰符 返回值 包名.包名.包名....类名.方法名(参数列表)
- 例子:
-
表达式的通配符写法
- 全通配符写法:
* *..*.*(..)
,表示任意的类的方法 - 注意:
- 访问修饰符可以省略
- 返回值、包名、类名、方法名都可以用 * 表示任意。(这里的包名几级包,就几个 *)
- 包名和类名之间一个点,表示当前包下的类。两个点就是当前包及其子包下的类。
- 参数列表
- 使用 … 表示有无参数均可
- 对于基本类型:直接写名称
- 引用类型:写包名.类名的方式 java.long.String
- 全通配符写法:
通知分类
如何配置通用的切入点的切面表达式呢?可以在 aop:config
标签中,使用 aop:pointcut
标签,如果实在 aop:aspect
标签外面, 必须得写在所有的 aop:aspect
标签的前面,这里是由顺序的,里面则无需顺序,但是只能在这个 aop:aspect
标签内使用。如下
<aop:config>
<aop:pointcut id="pt1" expression="execution(public void com.main.service.IAccountService.*(..))"/>
<aop:aspect id="logAdvice" ref="logger">
.....
</aop:aspect>
</aop:config>
前置通知
前置通知,在目标方法执行之前执行。通过 <aop:before></aop:before>
标签来配置。
需要在 <aop:aspect></aop:aspect>
标签里面写配置前置通知
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!--前置通知,其中 pointcut 指的是要对哪个方法配前置通知-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
</aop:aspect>
</aop:config>
- aop:aspect 标签
- id 属性:的名字可以随意取
- ref 属性:这里是引用上面的 Logger 的 bean 对象
- aop:before 标签
- method 属性:指的是配置类 Logger 的哪个方法作为前置配置的方法。
- pointcut-ref 属性:里面是切入点表达式,用来指向哪个类的方法需要配置前置方法(这里是 IAccountService 的所有方法)。但是这里是引用了上面的
aop:pointcut
的标签公共部分。
上面的配置,运行结果如下:
beforePrintLog打印日志中。。。。。。。。。
执行了保存
通过在没有对源代码修改的基础上,我们进行了打印日志的操作。
后置通知
是在切入点正常执行之后执行。它和异常通知只有一个被执行,因为后置能执行代表整个方法都是正常执行了,没有出现异常。通过 <aop:after></aop:after>
标签来配置。
需要在 <aop:aspect></aop:aspect>
标签里面写配置后置通知
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:after method="afterPrintLog" pointcut="execution(public void com.main.service.IAccountService.*(..))"></aop:after>
</aop:aspect>
</aop:config>
标签中的属性和前置通知是一样的意思。
上面的配置,运行结果如下:
执行了保存
afterPrintLog打印日志中。。。。。。。。。
通过在没有对源代码修改的基础上,我们进行了打印日志的操作。
异常通知
是在切入点发生异常之后执行。它和后置通知只有一个被执行,通过 <aop:after-throwing></aop:after-throwing>
标签来配置。
需要在 <aop:aspect></aop:aspect>
标签里面写配置后置通知
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:after-throwing method="throwPrintLog" pointcut="execution(public void com.main.service.IAccountService.*(..))"></aop:after-throwing>
</aop:aspect>
</aop:config>
标签中的属性和前置通知是一样的意思。
如果我们修改上面的 saveAccount() 代码,添加一行
int i = 1 / 0;
就会执行通知代码。打印结果想一想就知道了。
返回通知
返回通知,在目标方法返回结果之后执行。通过 <aop:after-returning></aop:after-returning>
标签来配置。
需要在 <aop:aspect></aop:aspect>
标签里面写配置后置通知
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:after-returning method="returnPrintLog" pointcut="execution(public void com.main.service.IAccountService.*(..))"></aop:after-returning>
</aop:aspect>
</aop:config>
标签中的属性和前置通知是一样的意思。
环绕通知
环绕通知是什么?字面的意思是围绕着方法执行。
我们知道,整个 AOP 的实现类似于动态代理。代理可以在原有的代码上进行增强。那么本来如果我们的 saveAccount()
方法例如没有采用 AOP,那么动态代理是什么样的呢?
这里摘取核心代码
//生成被代理对象
final IAccountService target = new AccountService();
//生成代理对象
MyProxy myProxy = Proxy.newProxyInstance(IAccountService.getClass().getClassLoader(), new Class[] {IAccountService.class}, new InvocationHandler() {
//当调用目标方法的时候,都会被拦截,来到这个方法之下。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();//取得方法名称
Object result = null;
try {
//前置通知--------------
System.out.println("beforePrintLog打印日志中。。。。。。。。。");
result = method.invoke(target, args);//执行目标方法
//后置通知--------------
System.out.println("afterPrintLog打印日志中。。。。。。。。。");
} catch (Exception e) {
//异常通知--------------
System.out.println("throwPrintLog打印日志中。。。。。。。。。");
}
//返回通知---------------
System.out.println("returnPrintLog打印日志中。。。。。。。。。");
return result;//返回执行目标方法的结果
}
);
可以看到,这 AOP 的各种通知,就是对应这上面的动态代理这种打印的位置,只是 Spring 为了方便,只需要通过配置既可以使用,这样程序员无需连接动态代理如何书写。
而环绕通知,它是 spring 框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式,而不是在利用配置交给 spring 管理。你可以参考下面代码,发现环绕通知和动态代理有相似之处。
我们环绕通知的切面类配置方法内容如下:
public Object aroundPrintLog(ProceedingJoinPoint proccedingJoinPoint){
Object object = null;
try {
//相当于前置通知
System.out.println("aroundPrintLog前打印日志中。。。。。。。。。");
Object[] args = proccedingJoinPoint.getArgs();
object = proccedingJoinPoint.proceed(args);//调用业务层的方法(切入点方法)
//相当于后置通知
System.out.println("aroundPrintLog后打印日志中。。。。。。。。。");
} catch (Throwable throwable) {
//相当于异常通知
System.out.println("aroundPrintLog异打印日志中。。。。。。。。。");
throwable.printStackTrace();
}finally {
//相当于最终通知
System.out.println("aroundPrintLog终打印日志中。。。。。。。。。");
}
return object;
}
Spring 框架提供了一个接口:ProccedingJoinPoint。该接口有一个方法 proceed(),此方法相当于有一个明确的切入点方法。
-
该接口可以作为环绕通知的参数。在程序执行的时候,spring 框架可以为我们提供该接口的实现类供我们使用
那么在 XML 中如何使用?
通过 <aop:around></aop:around>
标签来配置。
需要在 <aop:aspect></aop:aspect>
标签里面写配置环绕通知
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:around method="aroundPrintLog" pointcut="execution(public void com.main.service.IAccountService.*(..))"></aop:around>
</aop:aspect>
</aop:config>
标签中的属性和前置通知是一样的意思。
那么运行上面的配置代码的例子,结果打印如下
aroundPrintLog前打印日志中。。。。。。。。。
执行了保存
aroundPrintLog后打印日志中。。。。。。。。。
aroundPrintLog终打印日志中。。。。。。。。。