Spring面向切面编程(AOP)

810 阅读7分钟

Spring AOP

AOP概述

AOP全称为Aspect Oriented Programming的缩写,译为:面向切面编程是通过预编译方式和运行期间基于动态代理实现程序功能的一种技术。AOP是OOP的延续,是软件开发中的一个 热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑 的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时可提高开发的效率。

AOP相关术语

下面是spring官网原文给出的aop概念。

  • Aspect:表示切面,是切入点和通知的声明。
  • JoinPoint:程序执行过程中方法的连接点,如方法的执行或异常的处理。在AOP中连接点只表示方法类型
  • Advice:在特定通知连接点声明,通知包括前置、后置、环绕
  • Pointcut:切入点,通过表达式指定方法的切入点位置
  • Introduction:在不修改代码的情况下可以动态的添加方法或字段。
  • Target object:目标对象,也表示为一个代理对象
  • AOP proxy:表示一个Aop代理类对象
  • Weaving: 将切面与其他应用程序类型或对象进行连接,以创建代理对象

通知类型

spring Aop通知类型(advice)有以下五种:

  1. Before advice:前置通知,在方法执行前被调用,前置通知不会影响连接点的执行,除非抛出异常。

  2. After returning advice:返回通知后,在连接点正常完成后运行执行的通知,引发异常则不执行

  3. After throwing advice:异常通知后,如果方法通过抛出异常退出,将执行通知。

  4. After (finally) advice:最后通知,不管方法是否执行成功或有没有异常都会执行。

  5. Around advice:环绕通知,在方法执行前后都会执行。

基于XML的AOP配置

方式一:通过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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>

下面使用增加日志的方式来演示Aop的功能。

  1. 创建Maven项目并导入如下依赖。
<dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
  1. 编写UserService类,提供模拟的增删改查方法以作演示。
public interface UserService {
    /*
     *  模拟增删改查方法
     */
    String insert(String name);
    
    int delete(int id);
    
    String update(String name);
    
    int query(int id);
}
  1. 编写实现类,打印具体的功能,并最后利用aop技术添加额外的日志信息
public class UserServiceImpl implements UserService{
    
    @Override
    public String insert(String name) {
        System.out.println("添加了一个"+ name + "数据");
        return name;
    }
    @Override
    public int delete(int id) {
        System.out.println("删除了" + id + "号数据");
        return id;
    }
    @Override
    public String update(String name) {
        System.out.println("更新了" + name + "数据");
        return name;
    }
    @Override
    public int query(int id) {
        System.out.println("查询" + id + "号数据");
        return id;
    }

}
  1. 编写两个类,一个实现MethodBeforeAdvice接口,一个实现 AfterReturningAdvice接口,分别实现前置通知和后置返回通知的效果。
/*
	前置通知类
*/
public class BeforeLog implements MethodBeforeAdvice {
    /**
     * @param method:执行的目标对象方法
     * @param args:目标对象参数
     * @param target :目标对象
     * @throws Throwable :异常
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName()+"的" + method.getName() + "执行了");
    }
}

/*
	后置通知类
*/
public class AfterLog implements AfterReturningAdvice {
    /**
     * @param returnValue:执行后的返回值
     * @param method:目标对象方法
     * @param args:目标对象参数
     * @param target:目标对象
     * @throws Throwable:异常
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行了" + method.getName() + "方法,返回结果:" + returnValue);
    }
}
  1. 将编写的类注入到spring bean容器中,然后配置aop相关内容。

首先注册三个类到bean当中。

<!-- 注册三个bean,id是唯一标识  -->
<bean id="userService" class="com.spring.aop.service.UserServiceImpl"/>
<bean id="beforeLog" class="com.spring.aop.log.BeforeLog"/>
<bean id="afterLog" class="com.spring.aop.log.AfterLog"/>

第二步配置切入点和通知类,通知类为对目标类输出的信息,配置的方式有如下两种↓

方式一:直接在<aop:advisor/> 标签中指定通知类和切入点。

<aop:config>
 <aop:advisor advice-ref="beforeLog" pointcut="execution(* com.spring.aop.service.UserServiceImpl.*(..))"/>
 <aop:advisor advice-ref="afterLog" pointcut="execution(* com.spring.aop.service.UserServiceImpl.*(..))"/>
</aop:config>

方式二:根据<aop:pointcut/> 标签首先配置切入点,然后让配置通知再引用切入点

<aop:config>
    <aop:pointcut id="pointcut" expression="execution(* com.spring.aop.service.UserServiceImpl.*(..))"/>
    
		<!-- 配置切入的日志类,引用上面的切面  -->
    <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
    <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>

<!--
	参数解释:
	id:表示唯一标识
	expression表达式:execution表示要执行的类路径位置,*表示匹配所有的类型 .*表示执行类里面的所有方法,(..)表示匹配任意参数 
	advice-ref:引用注入的bean唯一标识id
	pointcut-ref:引用配置好的切面,内容为配置切面的id

	expression="execution(* *..*.*(..))":expression表达式还可以这么用,表示匹配所有包下的类。
-->

由上可见配置切面和通知的方法有两种,第一种少了一行配置,但每次都要写表达式指定切入点,而第二种方式则只需配置一次切入点即可让多个通知去引用这个切入点。

  1. 编写测试类

由于是根据spring接口实现的,所以获取bean的类型也必须为实现的UserService接口类型,不能是实现类,否则报错

@Test
public void test() {
    ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
    UserService service = context.getBean("userService",UserService.class);
    
    service.insert("Bear");
    service.delete(2);
    service.update("Bear");
    service.query(1);
}

测试结果:

在这里插入图片描述

由上可见四个方法分别都执行了前置通知和后置通知的打印日志信息,并且后置通知还执行了返回结果。

方式二:自定义类实现

第一种方式是通过类实现spring提供的接口来实现Aop的功能,优点是通过接口提供的反射机制可以使用更多的功能,缺点就是需要创建许多类去一一实现,由此还可以使用自定义类和通知方法的方式实现功能,把主要功能交给XML中配置的标签去实现,而自定义类中只需要编写要添加的日志方法或信息即可。

  1. 首先新建一个自定义的方法类。
public class General {

    public void before() {
        System.out.println("--------方法执行前的通知--------");
    }

    public void after() {
        System.out.println("--------方法执行后的通知--------");
    }

    public Object around(ProceedingJoinPoint point) throws Throwable {
        
        Object Value;
        Object[] args = point.getArgs();//得到方法执行所需的参数
        System.out.println("around前置执行了");
        Value = point.proceed(args); //明确调用业务层切入点方法
        System.out.println("around后置执行了");
        return Value;
    }
}

配置环绕通知需要使用ProceedingJoinPoint接口,该接口有一个方法proceed(),此方法相当于明确调用切入点的方法,该接口可作为怀绕通知的方法参数,在程序执行时会调用该接口的实现类使用。

  1. 配置xml文件
<!--    方法2:自定义类实现aop-->
<!-- 注入自定义类的bean实例 -->
<bean id="definition" class="com.spring.aop.log.General"/>

<aop:config>
    <!-- 配置切面,把自定义的方法横切进目标类当中-->
    <aop:aspect ref="definition">
        <!-- 配置切入点,也就是要增加功能的目标类 -->
        <aop:pointcut id="point" expression="execution(* com.spring.aop.service.UserServiceImpl.*(..))"/>
        
        <!-- 配置通知方法,指定自定义类中的三个方法 -->
        <aop:before method="before" pointcut-ref="point"/>
        <aop:after method="after" pointcut-ref="point"/>
        <aop:around method="around" pointcut-ref="point"/>
    </aop:aspect>
</aop:config>
  1. 测试类
@Test
public void test() {
    ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
    UserService service = context.getBean("userService", UserService.class);
    service.insert("Bear");
    service.delete(2);
    service.update("Bear");
    service.query(1);
}

测试结果。

在这里插入图片描述

上面测试结果显示了三种功能的通知都成功的执行在了输出的方法中,按照顺序,前置通知在最上面,后置通知在最下面,而环绕通知则被前置和后置通知包括在里面,真正的业务方法则在最里面,实现了不改动业务代码的前提下往代码中添加日志方法。

基于注解的AOP配置

通过注解实现可以实现自定义切面切入点和advice通知的配置,相比XML配置文件来说减少了更多繁杂的配置,只需少量的配置即可实现同样的功能。

下面基于xml配置的内容来演示。

首先自定义切面信息需要创建一个类,然后添加其注解和切入点还有通知方式。

  1. 业务类和其实现类。
public interface UserService {
    /*
     *  模拟增删改查方法
     */
    String insert(String name);

    int delete(int id);

    String update(String name);

    int query(int id);
}

/*
	实现类
*/
public class UserServiceImpl implements UserService{
    @Override
    public String insert(String name) {
        System.out.println("添加了一个"+ name + "数据");
        return name;
    }

    @Override
    public int delete(int id) {
        System.out.println("删除了" + id + "号数据");
        return id;
    }

    @Override
    public String update(String name) {
        System.out.println("更新了" + name + "数据");
        return name;
    }

    @Override
    public int query(int id) {
        System.out.println("查询" + id + "号数据");
        return id;
    }
}
  1. 新建自定义功能类
@Aspect //表示切面的注解
@Component //注入到spring容器当中
public class AopAnnotation {

    //定义一个切入点方法,统一其他方法的定义
    @Pointcut("execution(* com.spring.aop.service.UserServiceImpl.*(..))")
    public void log(){}

    @Before("log()") //前置通知
    public void before() {
        System.out.println("--------方法执行前的通知--------");
    }
    
    @After("log()") //后置通知
    public void after() {
        System.out.println("--------方法执行后的通知--------");
    }

    @Around("log()") //环绕通知
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Object Value;
        Object[] args = point.getArgs(); //得到方法执行所需的参数
        System.out.println("around前置执行了");
        Value = point.proceed(args);  //明确调用业务层切入点方法
        System.out.println("around后置执行了");

        return Value;
    }
}
  1. 注入bean和开启注解的支持
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd 
        http://www.springframework.org/schema/context 	
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 注入UserService的bean -->
    <bean id="userService" class="com.spring.aop.service.UserServiceImpl"/>
    <!-- 扫描自定义类注入的组件 -->
    <context:component-scan base-package="com.spring.aop.log"/>
    <!-- 开启对注解的支持 -->
    <aop:aspectj-autoproxy/>

</beans>
  1. 测试类。
@Test
public void test() {
    ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
    UserService service = context.getBean("userService", UserService.class);
    service.insert("Bear"); //这里只测试一个插入方法。
}

测试结果

在这里插入图片描述

可以看到在执行插入方法之前和之后分别都执行了Around环绕通知和前置后置通知。