这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。正则表达式
AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,能够将一些系统性相关的编程工做,独立提取出来,独立实现,而后经过切面切入进系统。从而避免了在业务逻辑的代码中混入不少的系统相关的逻辑——好比权限管理,事物管理,日志记录等等。这些系统性的编程工做均可以独立编码实现,而后经过AOP技术切入进系统便可。从而达到了 将不一样的关注点分离出来的效果。spring
切面必需要完成的工做即称为通知。通知定义了切面是什么以及何时实用。编程
spring切面能够实用的5种类型通知:bash
咱们的应用可能有数以千计的时机应用通知。这些时机被称 为链接点。链接点是在应用执行过程当中可以插入切面的一个点。这个点能够是调用方法时、抛出异常时、甚至修改一个字段时。切面代码能够利用这些点插入到应用的正常流程之中,并添加新的行为。测试
切点定义了从何处切入。切点的定义会匹配通知所要织入的一个或多个链接点。一般使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。ui
切面是通知和切点的结合。通知和切点共同定义了切面的所有内容----它是什么,在什么时候和何处完成其功能。编码
引入容许咱们向现有的类添加新方法或属性。spa
织入是把切面应用到目标对象并建立新的代理对象的过程。切面在指定的链接点被织入到目标对象中。代理
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注解由三部分组成:
经过配置将JumpIntroducer声明
@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
复制代码