如果说 IoC 是 Spring 的核心,那么面向切面编程就是 Spring 最为重要的功能之一了,在数据库事务中切面编程被广泛使用。

一、AOP介绍

AOP即 Aspect Oriented Program 面向切面编程

首先,在面向切面编程的思想里面,把功能分为核心业务功能,和周边功能。

  • 所谓的核心业务:比如登陆,增加数据,删除数据都叫核心业务
  • 所谓的周边功能:比如性能统计,日志,事务管理等等

周边功能在 Spring 的面向切面编程AOP思想里,即被定义为切面。

在面向切面编程AOP的思想里面,核心业务功能和切面功能分别独立进行开发,然后把切面功能和核心业务功能 "编织" 在一起,这就叫AOP。

AOP的目的

AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

AOP的核心概念
  • 切入点(Pointcut)
    在哪些类,哪些方法上切入(where
  • 通知(Advice)
    在方法执行的什么实际(when:方法前/方法后/方法前后)做什么(what:增强的功能)
  • 切面(Aspect)
    切面 = 切入点 + 通知,通俗点就是:在什么时机,什么地方,做什么增强!
  • 织入(Weaving)
    把切面加入到对象,并创建出代理对象的过程。(由 Spring 来完成)
五种通知形式

所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。

  • 前置通知:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。
  • 后置通知:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。
  • 异常通知:在连接点抛出异常后执行。
  • 返回通知:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。
  • 环绕通知:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。

二、AOP实现

 Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:

  • 默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了。
  • 当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB。

AOP编程其实是很简单的事情,步骤如下:

  • 选择连接点:哪些是方法是需要做增强处理的
  • 创建切面:相当于拦截器
  • 定义切入点:使用切入点表达式进行声明, 一个切入点可能横切多个业务组件;
  • 定义通知:通知就是在AOP框架为普通业务组件织入的处理动作

所以进行AOP编程的关键就是定义切入点定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法 = 增强处理+被代理对象的方法

pom.xml配置

<!-- aop -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>aopalliance</groupId>
    <artifactId>aopalliance</artifactId>
    <version>1.0</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.1</version>
</dependency>
选择连接点

Spring 是方法级别的 AOP 框架,我们主要也是以某个类额某个方法作为连接点,另一种说法就是:选择哪一个类的哪一方法用以增强功能。

public class RentingService {

    public void service() {
        // 仅仅只是实现了核心的业务功能
        System.out.println("签合同");
        System.out.println("收房租");
    }
}
创建切面(及通知)

选择好了连接点就可以创建切面了,我们可以把切面理解为一个拦截器,当程序运行到连接点的时候,被拦截下来,在开头加入了初始化的方法,在结尾也加入了销毁的方法而已。

public class Broker {

    public void around(ProceedingJoinPoint joinPoint) {
        System.out.println("带租客看房");
        System.out.println("谈价格");

        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        System.out.println("交钥匙");
    }
}
定义切点
<!-- 装配 Bean-->
<bean name="landlord" class="com.codedot.aop.RentingService"/>
<bean id="broker" class="com.codedot.aop.Broker"/>

<!-- 配置AOP -->
<aop:config>
    <!-- where:在哪些地方(包.类.方法)做增加 -->
    <aop:pointcut id="landlordPoint" expression="execution(* com.codedot.aop.RentingService.service())"/>
    <!-- what:做什么增强 -->
    <aop:aspect id="logAspect" ref="broker">
        <!-- when:在什么时机(方法前/后/前后) -->
        <aop:around pointcut-ref="landlordPoint" method="around"/>
    </aop:aspect>
</aop:config>

AOP 中可以配置的元素:

AOP 配置元素 用途 备注
aop:advisor 定义 AOP 的通知其 一种很古老的方式,很少使用
aop:aspect 定义一个切面 ——
aop:before 定义前置通知 ——
aop:after 定义后置通知 ——
aop:around 定义环绕通知 ——
aop:after-returning 定义返回通知 ——
aop:after-throwing 定义异常通知 ——
aop:config 顶层的 AOP 配置元素 AOP 的配置是以它为开始的
aop:declare-parents 给通知引入新的额外接口,增强功能 ——
aop:pointcut 定义切点 ——
aop测试
public class SpringMainTest {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        RentingService rentingService = (RentingService) ac.getBean("landlord");
        rentingService.service();
    }
}

输出:

带租客看房
谈价格
签合同
收房租
交钥匙

三、切入点表达式

切入点指示符用来指示切入点表达式目的,在Spring AOP中目前只有执行方法这一个连接点,Spring AOP支持的AspectJ切入点指示符如下:

  • execution:用于匹配方法执行的连接点;
  • within:用于匹配指定的类及其子类中的所有方法。
  • this:匹配可以向上转型为this指定的类型的代理对象中的所有方法。
  • target:匹配可以向上转型为target指定的类型的目标对象中的所有方法。
  • args:用于匹配运行时传入的参数列表的类型为指定的参数列表类型的方法;
  • @within:用于匹配持有指定注解的类的所有方法;
  • @target:用于匹配的持有指定注解目标对象的所有方法;
  • @args:用于匹配运行时 传入的参数列表的类型持有 注解列表对应的注解 的方法;
  • @annotation:用于匹配持有指定注解的方法;

  AspectJ切入点支持的切入点指示符还有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode;但Spring AOP目前不支持这些指示符,使用这些指示符将抛出IllegalArgumentException异常。

类型匹配语法

(1) *:匹配任何数量字符;

(2) ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数

(3) +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式。
在Schema风格下,由于在XML中使用“&&”需要使用转义字符“&amp;&amp;”来代替之,所以很不方便,因此Spring ASP 提供了and、or、not来代替&&、||、!。

execution

execution()是最常用的切点函数,其语法如下所示:

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)  除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。

参数模式如下:

() 匹配一个不接受任何参数的方法
(..) 匹配一个接受任意数量参数的方法
(*) 匹配了一个接受一个任何类型的参数的方法
(*,String) 匹配了一个接受两个参数的方法,其中第一个参数是任意类型,第二个参数必须是String类型

举例:

#匹配所有目标类的public方法
execution(public * *(..))

#匹配所有以To为后缀的方法
execution(* *To(..))

#匹配Waiter接口中的所有方法
execution(* com.aop.learn.service.Writer.*(..))

#匹配Waiter接口中及其实现类的方法
execution(* com.aop.learn.service.Writer+.*(..))

#匹配 com.aop.learn.service 包下所有类的所有方法
execution(* com.aop.learn.service.*(..))

#匹配 com.aop.learn.service 包,子孙包下所有类的所有方法
execution(* com.aop.learn.service..*(..))

#匹配 包名前缀为com的任何包下类名后缀为ive的方法,方法必须以Smart为前缀
execution(* com..*.*ive.Smart*(..))

# 匹配 save(String name,int age) 函数
execution(* save(String,int))

#匹配 save(String name,*) 函数 第二个参数为任意类型
execution(* save(String,*))

#匹配 save(String name,..) 函数 除第一个参数固定外,接受后面有任意个入参且入参类型不限
execution(* save(String,..))

#匹配 save(String+) 函数  String+ 表示入参类型是String的子类
execution(* save(String+))
with 

within是用来指定类型的,指定类型中的所有方法将被拦截。

举例:

#表示匹配包aop_part以及子包的所有方法
within(aop_part..*) 

#匹配UserServiceImpl类对应对象的所有方法外部调用,而且这个对象只能是UserServiceImpl类型,不能是其子类型。
within(com.elim.spring.aop.service.UserServiceImpl)

由于execution可以匹配包、类、方法,而within只能匹配包、类,因此execution完全可以代替within的功能。

this

Spring Aop是基于代理的,this就表示代理对象。this类型的Pointcut表达式的语法是this(type),当生成的代理对象可以转换为type指定的类型时则表示匹配。基于JDK接口的代理和基于CGLIB的代理生成的代理对象是不一样的。

举例:

#表示匹配了GodService接口的代理对象的所有连接点
this(aop_part.service.GodService)        
target
Spring Aop是基于代理的,target则表示被代理的目标对象。当被代理的目标对象可以被转换为指定的类型时则表示匹配。

举例:

#表示匹配实现了GodService接口的目标对象的所有连接点
target(aop_part.service.GodService)      
args

args用来匹配方法参数的。

  • “args()”匹配任何不带参数的方法。
  • “args(java.lang.String)”匹配任何只带一个参数,而且这个参数的类型是String的方法。
  • “args(..)”带任意参数的方法。
  • “args(java.lang.String,..)”匹配带任意个参数,但是第一个参数的类型是String的方法。
  • “args(..,java.lang.String)”匹配带任意个参数,但是最后一个参数的类型是String的方法。
@target

匹配当被代理的目标对象对应的类型及其父类型上拥有指定的注解时。

举例:

#匹配被代理的目标对象对应的类型上拥有MyAnnotation注解时
@target(com.elim.spring.support.MyAnnotation)

@args

匹配被调用的方法上含有参数,且对应的参数类型上拥有指定的注解的情况。

“@args(com.elim.spring.support.MyAnnotation)”匹配方法参数类型上拥有MyAnnotation注解的方法调用。如我们有一个方法add(MyParam param)接收一个MyParam类型的参数,而MyParam这个类是拥有注解MyAnnotation的,则它可以被Pointcut表达式“@args(com.elim.spring.support.MyAnnotation)”匹配上。
@within

用于匹配被代理的目标对象对应的类型或其父类型拥有指定的注解的情况,但只有在调用拥有指定注解的类上的方法时才匹配。

“@within(com.elim.spring.support.MyAnnotation)”匹配被调用的方法声明的类上拥有MyAnnotation注解的情况。比如有一个ClassA上使用了注解MyAnnotation标注,并且定义了一个方法a(),那么在调用ClassA.a()方法时将匹配该Pointcut;如果有一个ClassB上没有MyAnnotation注解,但是它继承自ClassA,同时它上面定义了一个方法b(),那么在调用ClassB().b()方法时不会匹配该Pointcut,但是在调用ClassB().a()时将匹配该方法调用,因为a()是定义在父类型ClassA上的,且ClassA上使用了MyAnnotation注解。但是如果子类ClassB覆写了父类ClassA的a()方法,则调用ClassB.a()方法时也不匹配该Pointcut。

@annotation

用于匹配方法上拥有指定注解的情况。

举例:

#匹配所有的方法上拥有MyAnnotation注解的方法外部调用。
@annotation(com.elim.spring.support.MyAnnotation)
bean

用于匹配当调用的是指定的Spring的某个bean的方法时。

  • “bean(abc)”匹配Spring Bean容器中id或name为abc的bean的方法调用。
  • “bean(user*)”匹配所有id或name为以user开头的bean的方法调用。
常入切入点表达式
#任意公共方法的执行: 
execution(public * *(..)) 

#任何一个以“set”开始的方法的执行: 
execution(* set*(..)) 

#AccountService 接口的任意方法的执行: 
execution(* com.xyz.service.AccountService.*(..)) 

#定义在service包里的任意方法的执行: 
execution(* com.xyz.service.*.*(..)) 

#定义在service包或者子包里的任意方法的执行: 
execution(* com.xyz.service..*.*(..)) 

#在service包里的任意连接点(在Spring AOP中只是方法执行) : 
within(com.xyz.service.*) 

#在service包或者子包里的任意连接点(在Spring AOP中只是方法执行) : 
within(com.xyz.service..*) 

#实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行) : 
this(com.xyz.service.AccountService) 

#实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行) : 
target(com.xyz.service.AccountService) 

#任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点 (在Spring AOP中只是方法执行) 
args(java.io.Serializable) 

#有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP中只是方法执行) 
@target(org.springframework.transaction.annotation.Transactional) 

#任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在Spring AOP中只是方法执行) 
@within(org.springframework.transaction.annotation.Transactional) 

#任何一个执行的方法有一个 @Transactional annotation的连接点(在Spring AOP中只是方法执行) 
@annotation(org.springframework.transaction.annotation.Transactional) 

#任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行) 
@args(com.xyz.security.Classified)

 

时刻与技术进步,每天一点滴,日久一大步!!! 本博客只为记录,用于学习,如有冒犯,请私信于我。