这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了 将不同的关注点分离出来的效果。
切面必须要完成的工作即称为通知。通知定义了切面是什么以及什么时候实用。
spring切面可以实用的5种类型通知:
我们的应用可能有数以千计的时机应用通知。这些时机被称 为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点定义了从何处切入。切点的定义会匹配通知所要织入的一个或多个连接点。通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容----它是什么,在何时和何处完成其功能。
引入允许我们向现有的类添加新方法或属性。
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。
Spring提供了4种类型的AOP支持:
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。
首先定义一个接口来作为切点:
public interface Performance { void perform(); } 复制代码
假设我们想编写Performance的perform()方法触发的通 知。下面的表达式能够设置当perform()方法执行时触发通知的调用。
execution(* com.wtj.springlearn.aop.Performance.perform(..)) 复制代码
execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明了不关心方法返回值的类型。然后指定了全限定类名和方法名。对于方法参数列表,使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。
如果我们需要设置切点匹配com.wtj.springlearn.aop包,可以使用within()来限定匹配。
execution(* com.wtj.springlearn.aop.Performance.perform(..)) && within(com.wtj.springlearn.aop.*) 复制代码
表示com.wtj.springlearn.aop包下任意类的方法被调用时。
使用“&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。
因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代替“||”和“!”。
还可以使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。
execution(* com.wtj.springlearn.aop.Performance.perform(..)) && bean('book') 复制代码
这里表示执行perform方法时通知,但是只限于bean的ID为book。
本篇主要介绍注解方式的切面定义方式
通过@Aspect进行标注,表示该Audience不仅是一个POJO还是一个切面。类中的方法表示了切面的具体行为。
Spring提供了五种注解来定义通知时间:
首先创建一个切面:
@Aspect public class Audience { //表演前 手机静音 @Before("execution(* com.wtj.springlearn.aop.Performance.perform(..))") public void silenceCellPhone(){ System.out.println("silence Cell Phone"); } //表演成功-clap @AfterReturning("execution(** com.wtj.springlearn.aop.Performance.perform(..))") public void clap(){ System.out.println("clap clap clap"); } //表演失败-退款 @AfterThrowing("execution(** com.wtj.springlearn.aop.Performance.perform(..))") public void refund(){ System.out.println("refund refund refund"); } } 复制代码
Performance的实现类:
@Component public class PerformanceImpl implements Performance { public void perform() { System.out.println("the perform is good"); } } 复制代码
最后还需要开启自动代理功能,通过JavaConfig进行配置,使用 @EnableAspectJAutoProxy
标签开启。
@Configuration @EnableAspectJAutoProxy @ComponentScan public class AudienceConfig { @Bean public Audience audience(){ return new Audience(); } } 复制代码
最后通过一个简单的测试用例就可以来验证了。
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = AudienceConfig.class) public class PerformanceTest { @Autowired private Performance performance; @Test public void perTest(){ performance.perform(); } } 复制代码
打印结果:
silence Cell Phone the perform is good clap clap clap 复制代码
你会发现上面切面的方法中,切点的声明都是一样的,这种情况下可以使用 @Pointcut
注解来定义切点。
@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(..))") public void per(){}; //表演前 手机静音 @Before("per()") public void silenceCellPhone(){ System.out.println("silence Cell Phone"); } 复制代码
per()方法本身并不重要,该方法只是一个标识,供@PointCut注解依附。
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
重写Audience切面,使用环绕通知替代之前多个不同的前置通知和后置通知。
@Around("per()") public void watch(ProceedingJoinPoint point) throws Throwable { try{ System.out.println("silence Cell Phone"); point.proceed(); System.out.println("clap clap clap"); }catch (Exception e){ System.out.println("refund refund refund"); } } 复制代码
首先注意到的可能是它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。
如果不调proceed()这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。同样的,你也可以调用多次。
上面我们创建的切面都很简单,没有任何参数。那么切面能访问和使用传递给被通知方法的参数么?
Performance中新增方法:
void perform(String name); 复制代码
实现类:
public void perform(String name) { System.out.println("下面请 "+name+" 开始他的表演"); } 复制代码
修改Audience中的切点和切面
@Pointcut("execution(* com.wtj.springlearn.aop.Performance.perform(String)) && args(name)") public void per(String name){}; @Around("per(name)") public void toWatch(ProceedingJoinPoint point,String name) throws Throwable { try{ point.proceed(); System.out.println(name +" 上场啦"); System.out.println(name +" 演出结束"); }catch (Exception e){ System.out.println("refund refund refund"); } } 复制代码
表达式 args(name)
限定符,它表示传递给perform(String name)方法的String类型参数也会传到通知中去,参数名与切点中的参数名相同。 perform(String)
指明了传入参数的类型。
然后在 @Around
注解中指明切点与参数名,这样就完成了参数转移。
最后修改一下测试用例就完成了
@Test public void perTest(){ performance.perform("渣渣辉"); } 复制代码
打印输出:
下面请 渣渣辉 开始他的表演 渣渣辉 上场啦 渣渣辉 演出结束 复制代码
如果我们想在一个类上新增方法,通常情况下我们会怎么做呢?最简单的办法就是在此目标类上增加此方法,但是如果原目标类非常复杂,动一发而牵全身。并且有些时候我们是没有目标类的源码的,哪这个时候怎么办呢?
我们可以为需要添加的方法建立一个类,然后建一个代理类,同时代理该类和目标类。用一个图来表示
当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。
还是上面的例子,假设我们需要让表演者跳起来。
新建Jump接口以及实现类:
public interface Jump { void duJump(); } 复制代码
public class JumpImpl implements Jump { public void duJump() { System.out.println("do Jump"); } } 复制代码
然后我们代理两个类:
@Aspect public class JumpIntroducer { @DeclareParents(value = "com.wtj.springlearn.aop.Performance+",defaultImpl = JumpImpl.class) public static Jump jump; } 复制代码
@DeclareParents注解由三部分组成:
@ComponentScan @Configuration @EnableAspectJAutoProxy public class JumpConfig { @Bean public JumpIntroducer jumpIntroducer(){ return new JumpIntroducer(); } } 复制代码
或者你也可以在JumpIntroducer类上加入 @Component
注解,就可以不用声明bean了。
最后通过测试用例进行测试:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = JumpConfig.class) public class PerformanceTest { @Autowired private Performance performance; @Test public void perTest(){ //类型转换 Jump jump = (Jump) performance; jump.duJump(); } } 复制代码
打印结果:
do Jump 复制代码