Talk is cheap , show me your code!
欢迎来到付振南Java博客,让我们一起学习Java吧!

spring之AOP(基于xml方式配置)

在学完了spring的ioc特性之后,我又开始学了spring的另一个核心,AOP。

什么是AOP呢? AOP 即 Aspect Oriented Programming ,大家都知道Java是面向对象的的,也就是OOP,而spring是面向切面编程的,是对OOP的一个扩展,这就是AOP最简单的概念。

那么到底什么是面向切面呢?AOP思想是什么呢?我们先来看百度百科对于AOP的定义。

简单的说它就是把我们程序中重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强的一种技术。所以学习AOP之前我们还得知道什么是动态代理模式

从AOP的定义中我们能知道AOP有以下作用和优势。
作用:
在程序运行期间,不改变源码对已有方法进行增强。
优势:
减少重复代码,提高开发效率,降低程序耦合,维护方便。

我们来用以下案例实现一个简单的spring AOP。

首先我们在spring配置文件中把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"
       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">
</beans>

然后再去maven中导我们要用到的坐标:

<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>

然后我们创建一个service包,再它包下新建一个IAccountService接口,代码如下:

package cn.fuzhennan.service;

/**
 * @author fuzhennan
 * 账户的业务层接口
 */
public interface IAccountService {
    /**
     * 模拟保存账户,无返回值无参
     */
    void saveAccount();

    /**
     * 模拟更新账户,无返回值有参
     * @param i
     */
    void updateAccount(int i);

    /**
     * 模拟删除账户,有返回值无参
     * @return
     */
    int deleteAccount();
}

业务层的接口写好了,我们再来写一个实现类,实现类中有三个方法,一个是无返回值无参的保存方法,一个是无返回值有参的更新方法,最后一个是有返回值无参的删除方法,具体代码如下:

package cn.fuzhennan.service.impl;

import cn.fuzhennan.service.IAccountService;

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;
    }
}

业务层实现类写好了,接下来我们就是要对方法进行增强了。我们新建一个utils包,在utils包下创建一个通知类Logger,用于记录日志的公共类,提供公共代码,我们在这主要是打印几句话。然后我们去spring配置文件中配置bean对象。

我们把accoutService和Logger都配置进来,代码如下:

<!-- 配置accountService对象 -->
<bean id="accountService" class="cn.fuzhennan.service.impl.AccountServiceImpl"/>

<!-- 配置通知类Logger类 -->
<bean id="logger" class="cn.fuzhennan.utils.Logger"/>

此时我们bean对象已经交给spring容器管理了,前面我们说spring是 把我们程序中重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强的一种技术。 现在我们要开始写业务方法增强的代码了。

现在我们有一个需求,在业务层的saveAccount方法执行之前打印一句话,执行之后打印一句话,发生异常的时候打印一句话,最终方法运行结束之前打印一句话,那么怎么在不改动saveAccount方法的基础上实现呢? 其实啊,这就得用到我们的spring AOP的四种通知了。

在spring AOP中,有四种类型的通知,分别是前置通知,后置通知,异常通知,最终通知。我们在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方法开始记录日志了。。。");
    }
}

光写这几个方法spring怎么知道你是用AOP?所以我们还得去配置文件告诉spring我们是使用AOP来实现的。

配置AOP,使用<aop:config></aop:config>标签,声明配置AOP,然后我们这个标签里面配置<aop:aspect >标签,表明开始配置切面,这个aspect标签有两个属性需要我们填写,一个是id属性 ,给切面一个唯一标识,另一个是ref属性,指定通知类bean的id,这里我们写logger,所以配置完的代码是这样的:

<aop:config>-->
    <!-- 配置切面 -->
    <aop:aspect id="logAdvice" ref="logger" >
    </aop:aspect>
</aop:config>

我们现在的需求是在业务层的saveAccount方法执行之前打印一句话,执行之后打印一句话,发生异常的时候打印一句话,最终方法运行结束之前打印一句话 ,所以是前置通知,所以在aspect标签内部需要配置一个前置通知,后置通知,异常通知,最终通知,我们先来讲解配置前置通知。

<aop:before method="beforePrintLog"
pointcut="execution( * cn.fuzhennan.service.impl.*.*(..))">
</aop:before>

这个标签也有两个属性需要我们填写,一个是method属性,用于指定Logger类中的哪个方法是前置通知,另一个是pointcut属性,用于指定切入点表达式,该表达式的含义指的是对业务层哪些方法进行增强。

切入点表达式的写法:关键词:execution(表达式)
execution中表达式的写法是:访问修饰符 返回值 包名.类名.方法名(参数列表)

下面来一个标准的切入点表达式写法:

public void cn.fuzhennan.service.impl.AccountServiceImpl.saveAccount()

通常在实际开发中,我们都用通配符写法来写。可以省略访问修饰符,返回值用*代替,然后包名.类名只需要具体到业务层的具体实现,然后.*代表具体实现类型,在加一个.*代表方法名,括号里的参数列表用..代表有无返回值均可。标准的通配符写法也就是上面的 * cn.fuzhennan.service.impl.*.*(..)

前置通知配完了,其他三个也是大同小异,直接上代码:

<!-- 配置后置通知,异常通知和后置通知相斥,两者只有一个发生 -->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution( * cn.fuzhennan.service.impl.*.*(..))">
</aop:after-returning>
<!-- 配置异常通知,异常通知和后置通知相斥,两者只有一个发生 -->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution( * cn.fuzhennan.service.impl.*.*(..))">
</aop:after-throwing>
<!-- 配置最终通知,无论程序是否发生异常,都会执行 -->
<aop:after method="afterPrintLog" pointcut="execution( * cn.fuzhennan.service.impl.*.*(..))">
</aop:after>

然后我们编写一个测试类,执行saveAccount方法,代码如下:

package cn.fuzhennan;

import cn.fuzhennan.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAop {
public static void main(String[] args){
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("ApplicationContext.xml");
//2.获取对象
IAccountService as = (IAccountService) ac.getBean("accountService");
//3.执行方法
as.saveAccount();
}

}

我们来看运行结果:

需求我们基本上算是实现了,细心的朋友会发现,咦?异常通知嘞?哈哈,异常通知当然是发生异常才会执行的啦,我们来演示一下异常通知,我们只需要在saveAccount方法中人为制造一个错误,就行了,看下面代码:

public void saveAccount() {
System.out.println("执行了保存方法");
int i = 1/0; //用于引出异常通知
}

这个错误就是除数不能为零,我们来看看运行结果:

异常通知执行了,成功了,细心的同学又要问了,咦?后置通知咋不见了嘞?哈哈,这是因为异常通知和后置通知是相斥的,水火不相容,其实也很好理解,正常执行就不会有异常,有异常就不会正常执行下去。

可能有的朋友要问了,spring不是把重复代码抽取出来吗?这配置文件配置四种通知怎么这么麻烦,基本都是重复的,能不能精简?

答案是肯定的,这时候就需要我们的环绕通知帮忙了!首先我们在配置文件中这么配置:

<aop:config>
    <!-- 配置切面 -->
    <aop:pointcut id="pt1" expression="execution( * cn.fuzhennan.service.impl.*.*(..))"/>
    <aop:aspect id="logAdvice" ref="logger" >
        <aop:around method="aroundPrintLog" pointcut-ref="pt1"/>
    </aop:aspect>
</aop:config>

这里我们把pointcut单独拿出来了,不再是像之前一样写在aspect标签内部了,这是因为写在aspect标签内部表示的是只能在当前切面使用,写在aop:aspect外面。此时就变成了所有切面可用,写在外面时有约束要求,必须写在aop:config下面,切面前面。

环绕通知需要一个环绕通知方法,所以我们去Logger类中写方法,代码如下:

public Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
Object rtValue = null;
try{
//得到方法执行所需参数
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("Logger类中的afterPrintLog方法开始记录日志了。。。前置");
//明确调用业务层方法(切入点方法),在proceed方法前面就是前置,后面就是后置,catch就是异常,finally就是最终
rtValue = proceedingJoinPoint.proceed(args);
System.out.println("Logger类中的afterPrintLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable throwable){
System.out.println("Logger类中的afterPrintLog方法开始记录日志了。。。异常");
throw new RuntimeException(throwable);
}finally {
System.out.println("Logger类中的afterPrintLog方法开始记录日志了。。。最终");
}
}

spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个proceed()方法,此方法就相当于明确调用切入点方法,该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。

它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。

方法很简单,第一步我们通过proceedingJoinPoint.getArgs()方法得到增强方法执行所需要的参数,用Object类型的args数组去装它,然后再通过proceedingJoinPoint.proceed(args)方法明确调用切入点,也是用一个Object类型的变量去装它并且返回。在这个方法前面就是前置通知,在这个方法后面就是后置通知,在catch异常语句块里面就是异常通知,在finally语句块中就是最终通知,我们来看一下运行结果。

同样也是可以完美的运行。我们通过一个简单的案例实现了最简单的springAOP,觉得xml方式配置繁琐的也可以用注解方式配置,整个基于xml实现的spring AOP的工程目录图如下图所示:

基于注解的方式配置AOP可以看这篇文章

赞(2) 打赏
未经允许不得转载:付振南Java博客 » spring之AOP(基于xml方式配置)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏