AOP——SpringAOP
一、AOP:
是对OOP编程方式的一种补充。翻译过来为“面向切面编程”。
可以理解为一个拦截器框架,但是这个拦截器会非常武断,如果它拦截一个类,那么它就会拦截这个类中的所有方法。如对一个目标列的代理,增强了目标类的所有方法。
两个解决办法:
1.不优雅的做法:
在添加增强时,根据方法名去判断,是否添加增强,但是这样就得一直去维护这个增强类。
2.面向切面:
将增强类和拦截条件组合在一起,然后将这个切面配置到 ProxyFactory 中,从而生成代理。
二、AOP 和 切面的关系
1.类比于 OOP 和 对象,AOP 和 切面就是这样的一种关系。
2.也可以将 切面 看成是 AOP 的一个工具。
三、几个概念
切面(Advisor):是AOP中的一个术语,表示从业务逻辑中分离出来的横切逻辑,比如性能监控,日志记录,权限控制等。
这些功能都可以从核心的业务逻辑中抽离出去。可以解决代码耦合问题,职责更加单一。封装了增强和切点。
增强(Advice):增强代码的功能的类,横切到代码中。
目标:目标方法(JDK代理)或目标类(CGLIB代理)
代理:JDK代理,CGLIB代理。或是通过 ProxyFactory 类生产。
切点:通过一个条件来匹配要拦截的类,这个条件称为切点。如拦截所有带 Controller 注解的类。增强的条件。
连接点:作为增强方法的入参,可以获取到目标方法的信息。
四、概括为一张图
五、增强
1.Weaving(织入):对方法进行增强
(1)前置增强(BeforeAdvice):在目标方法前调用。
(2)后置增强(AfterAdvice):在目标方法后调用。
(3)环绕增强(AroundAdvice):将 Before 和 After ,甚至抛出增强和返回增强合到一起。
(4)返回增强(AfterReturningAdvice):在方法返回结果后执行,该增强可以接收到目标方法返回结果。
(5)抛出增强(AfterThrowingAdvice):在目标方法抛出对应的类型后执行,可以接收到对应的异常信息。
2.Introduction(引入):对类进行增强
(1)引入增强(DeclareParentsAdvice):想让程序在运行的时候动态去实现某个接口,需要引入增强。
六、SpringAOP
1.编程式
(1)前置增强,需要实现:MethodBeforeAdvice 接口
增强类:
/** * @author solverpeng * @create 2016-07-27-11:07 */ public class CarBeforeAdvice implements MethodBeforeAdvice{ @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("before"); } }
测试方法:
@Test public void test03() { ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTarget(new Car()); proxyFactory.addAdvice(new CarBeforeAdvice()); Wheel carProxy = (Wheel)proxyFactory.getProxy(); carProxy.run(); }
(2)后置增强:实现 AfterReturningAdvice 接口
(3)环绕增强:实现 org.aopalliance.intercept.MethodInterceptor 接口,使用 Object result = methodInvocation.proceed(); 调用目标方法。在目标方法前后添加增强。
2.声明式:Spring + AspectJ
开发步骤:
(1)定义切面类,将该切面类放入到 IOC 容器中
/** * @author solverpeng * @create 2016-07-27-13:10 */ @Component public class XMLLoggingAspect { public void beforeAdvice(JoinPoint point) { System.out.println("xml aspects logging before"); } public void afterAdvice(JoinPoint point) { System.out.println("xml aspects logging after"); } public void afterReturningAdvice(JoinPoint point, Object result) { System.out.println("xml aspects logging afterReturningAdvice"); } public void afterThrowingAdvice(JoinPoint point, Exception e) { System.out.println("xml aspects logging afterThrowingAdvice"); } public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable { Object result = point.proceed(); System.out.println("xml aspects logging aroundAdvice"); return result; } }
(2)Spring AOP 配置都必须定义在 <aop:config>元素内部。
(3)在 <aop:config> 中,每个切面都需要创建一个 <aop:aspect> 元素
(4)为具体的切面实现引用后端的 bean 实例。
下面展示 前置增强、后置增强、环绕增强、返回增强、抛出增强 的一个例子:
<aop:config> <aop:aspect ref="XMLLoggingAspect"> <aop:pointcut id="carPointcut" expression="execution(void run())"/> <aop:before method="beforeAdvice" pointcut-ref="carPointcut"/> <aop:after method="afterAdvice" pointcut-ref="carPointcut"/> <aop:after-returning method="afterReturningAdvice" pointcut-ref="carPointcut" returning="result"/> <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="carPointcut" throwing="e"/> <aop:around method="aroundAdvice" pointcut-ref="carPointcut"/> </aop:aspect> </aop:config>
控制台输出:
xml aspects logging before
i am a car, i can run.
xml aspects logging aroundAdvice
xml aspects logging afterReturningAdvice
xml aspects logging after
基于声明式的 Spring AspectJ 织入增强配置说明:
支持配置两个级别的公共切点表达式,一个是针对某个切面的所有方法(定义在 <aop:aspect> 节点内),另一个是针对所有切面(定义在<aop:config>节点内)。使用 pointcut-ref 来引入切点。
下面展示 引入增强 的一个例子:
对于引入增强,只需要在配置在 <aop:aspect> 节点下就可以,不需要去切面类中添加任何属性。
<aop:config> <aop:aspect ref="XMLLoggingAspect"> <aop:declare-parents types-matching="com.nucsoft.spring.target.impl.Student" implement-interface="com.nucsoft.spring.target.Fly" default-impl="com.nucsoft.spring.target.impl.SuperMan"/> </aop:aspect> </aop:config>
测试:
@Test public void test04() { Person student = context.getBean(Student.class); System.out.println(student.say("james")); Fly fly = (Fly) student; fly.fly(); }
控制台输出:
hello,james
i am super man, i can fly.
基于声明式的 Spring AspectJ 引入增强配置说明:
(1)引入增强是类级别的,所以不存在切点表达式。
(1)利用 <aop:declare-parents> 节点在 <aop:aspect> 内部声明
(2)types-matching 属性,要增强的目标类,这里需要全类名。
(3)implement-interface 属性:动态的增强类接口。
(4)default-impl 属性:动态增强类接口的实现类。
3.注解:Spring + AspectJ
对切面类添加 @Aspect 注解,将切面类和目标类放入到 IOC 容器中,可以通过 <context:component-scan base-package=""/> 进行扫描。
添加增强方法(包括增强类型和切点表达式,以及连接点)。
在 Spring Config 文件中添加 <aop:aspectj-autoproxy proxy-target-class="true"/>, proxy-target-class属性,false 只能代理接口(JDK动态代理),true 代理类(CGLIB动态代理)
3.1 通过切点表达式(AspectJ execution)进行拦截
spring-config.xml
<context:component-scan base-package="com.nucsoft.spring"/> <aop:aspectj-autoproxy proxy-target-class="true"/>
Person 接口:
/** * Created by solverpeng on 2016/7/26. */ public interface Person { String say(String name); }
Person 实现类 Student:
/** * @author solverpeng * @create 2016-07-26-17:55 */ @Component public class Student implements Person{ @Override public String say(String name) { return "hello," + name; } }
测试:
/** * @author solverpeng * @create 2016-07-26-18:15 */ public class SpringTest { @Test public void test01() { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); Student student = context.getBean(Student.class); String james = student.say("james"); System.out.println(james); } }
(1)前置增强:关键字:@Before,JoinPoint,execution 切点表达式,表达式内容支持通配符
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @Before("execution(String say(String))") public void before(JoinPoint point) { System.out.println("before"); } }
控制台输出:
before
hello,james
(2)后置增强:关键字:@After
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @After("execution(String say(String))") public void afterAdvice(JoinPoint point) { System.out.println("after..."); } }
控制台输出:
after...
hello,james
(3)环绕增强:关键字:@Around,ProceedingJoinPoint
将 Student 类还原
切面类:
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @Around("execution(String say(String))") public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable { before(); Object result = point.proceed(); after(); return result; } private void before(){ System.out.println("before"); } private void after(){ System.out.println("after"); } }
控制台输出:
before
after
hello,james
注意:
<1>.增强方法的返回值为 Object 类型的,该返回值与目标方法返回值一致。
<2>.Object result = point.proceed(); 该 result 即为目标方法执行后的返回值。
<3>.在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法. 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行
<4>.环绕通知的方法需要返回目标方法执行之后的结果, 即调用 joinPoint.proceed(); 的返回值, 否则会出现空指针异常
(4)返回增强:关键字:@AfterReturning,returning,JoinPoint
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @AfterReturning(value = "execution(String say(String))", returning = "str") public void aferRetruningAdvice(JoinPoint point, String str) { System.out.println("str:" + str); System.out.println("aferRetruningAdvice"); } }
控制台输出:
str:hello,james
aferRetruningAdvice
hello,james
(5)抛出增强:关键字:@AfterThrowing,throwing。注意:抛出的异常类型必须和切面抛出增强接收的异常类型相同或是其子类。
更改 Student 类,手动抛出一个异常:
/** * @author solverpeng * @create 2016-07-26-17:55 */ @Component public class Student implements Person{ @Override public String say(String name) { throw new RuntimeException("exception"); } }
切面类:
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @AfterThrowing(value = "execution(String say(String))", throwing = "e") public void AfterThrowingAdvice(JoinPoint point, Exception e) { String message = e.getMessage(); System.out.println(message); System.out.println("AfterThrowingAdvice"); } }
控制台输出:
exception
AfterThrowingAdvice
3.2 通过切点注解表达式(AspectJ @annotation)进行拦截
开发步骤:
(1)定义注解类
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthorityTag {}
(2)为切面类中增强指定注解表达式
/** * Created by solverpeng on 2016/7/27. */ @Aspect @Component public class AuthorityAspect { @Before("@annotation(com.nucsoft.spring.annotation.AuthorityTag)") public void before(JoinPoint point) { System.out.println("authority before"); } }
(3)在目标类目标方法上标注注解
/** * @author xzsw * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @AuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
各种增强的使用:
(1)前置增强
上面介绍步骤的例子就是一个前置增强。
控制台输出:
authority before
i am a car, i can run.
(2)后置增强
可以为多个增强使用同一个注解,如:
/** * Created by solverpeng on 2016/7/27. */ @Aspect @Component public class AuthorityAspect { @Before("@annotation(com.nucsoft.spring.annotation.AuthorityTag)") public void before(JoinPoint point) { System.out.println("authority before"); } @After("@annotation(com.nucsoft.spring.annotation.AuthorityTag)") public void afterAdvice(JoinPoint point) { System.out.println("authority after"); } }
控制台输出:
authority before
i am a car, i can run.
authority after
也可以为每个增强使用不同的注解,如:
注解:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface BeforeAuthorityTag {}
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AfterAuthorityTag {}
切面:
/** * Created by solverpeng on 2016/7/27. */ @Aspect @Component public class AuthorityAspect { @Before("@annotation(com.nucsoft.spring.annotation.BeforeAuthorityTag)") public void before(JoinPoint point) { System.out.println("authority before"); } @After("@annotation(com.nucsoft.spring.annotation.AfterAuthorityTag)") public void afterAdvice(JoinPoint point) { System.out.println("authority after"); } }
使用:
/** * @author solverpeng * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @BeforeAuthorityTag @AfterAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
控制台输出:
authority before
i am a car, i can run.
authority after
(3)环绕增强
注解类:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AroundAuthorityTag {}
切面增强:
@Around(value = "@annotation(com.nucsoft.spring.annotation.AroundAuthorityTag)") public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable { Object result = point.proceed(); after(); System.out.println("authority aroundAdvice"); return result; } private void after() { System.out.println("after"); }
目标类:
/** * @author solverpeng * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @AroundAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
控制台输出:
i am a car, i can run.
after
authority aroundAdvice
(4)返回增强
注解类:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AfterReturningAuthorityTag {}
切面增强:
@AfterReturning(value = "@annotation(com.nucsoft.spring.annotation.AfterReturningAuthorityTag)", returning = "result") public void afterReturningAdvice(JoinPoint point, Object result) { System.out.println("authority afterReturning"); }
目标类:
/** * @author solverpeng * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @AfterReturningAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
控制台输出:
i am a car, i can run.
authority afterReturning
(5)抛出增强
注解类:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AfterThrowingAuthorityTag {}
切面增强:
@AfterThrowing(value = "@annotation(com.nucsoft.spring.annotation.AfterThrowingAuthorityTag)", throwing = "e") public void afterThrowAdvice(JoinPoint point, Exception e) { System.out.println(e.getMessage()); System.out.println("authority afterThrowing"); }
目标类:
@Component public class Car implements Wheel{ @AfterThrowingAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); throw new RuntimeException("throw a new runtimeException"); } }
控制台输出:
i am a car, i can run.
throw a new runtimeException
authority afterThrowing
java.lang.RuntimeException: throw a new runtimeException
(6)引入增强:关键字:@DeclareParents
将要引入的接口:
/**
* Created by solverpeng on 2016/7/26.
*/
public interface Fly {
void fly();
}
将要引入的接口的实现:
/**
* @author solverpeng
* @create 2016-07-26-20:55
*/
public class SuperMan implements Fly{
@Override
public void fly() {
System.out.println("i am super man, i can fly.");
}
}
切面类:
/**
* @author solverpeng
* @create 2016-07-26-17:42
*/
@Aspect
@Component
public class LoggingAspect {
@DeclareParents(value = "com.nucsoft.spring.target.impl.Student", defaultImpl = SuperMan.class)
private Fly fly;
}
测试类:
/**
* @author solverpeng
* @create 2016-07-26-18:15
*/
public class SpringTest {
@Test
public void test01() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
Person student = context.getBean(Student.class);
String james = student.say("james");
System.out.println(james);
Fly fly = (Fly) student;
fly.fly();
}
}
控制台输出:
hello,james
i am super man, i can fly.
说明:
<1>.在 Aspect 类中定义一个需要引入增强的接口,它也就是运行时需要动态实现的接口。
<2>.@DeclareParents 注解:
value 属性:目标类,可以是一个 AspectJ 类型的表达式,可以引入到多个类中。
defaultImpl 属性:引入接口的默认实现类。
<3>.虽然切面类中标注有@DeclareParents 注解 的属性可以是任意的,但是一般还是将其设置为 引入增强类型。
<4>.从 ApplicationContext 中获取到的 student 对象其实是一个代理对象,可以转型为自己静态实现的接口 Person,也可以转型为动态实现的接口 Fly,切换起来非常方便。
3.3 对比基于注解的切点表达式和注解表达式
注解表达式:更加灵活,但是相应的在开发的过程中,需要程序员手动的去为每个需要添加增强的方法添加对应注解。更加容易扩展。
切点表达式:可以写出通用的增强,也不需要程序员手动的去为每个方法添加增强,但是需要切点表达式适配。
七、小的知识点
1.利用方法签名编写 AspectJ 切点表达式
execution * com.nucsoft.spring.Calculator.*(..): 匹配 Calculator 中声明的所有方法,
第一个 * 代表任意修饰符及任意返回值. 第二个 * 代表任意方法. .. 匹配任意数量的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.
execution public * Calculator.*(..): 匹配 ArithmeticCalculator 接口的所有公有方法.
execution public double Calculator.*(..): 匹配 Calculator 中返回 double 类型数值的方法
execution public double Calculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数
execution public double Calculator.*(double, double): 匹配参数类型为 double, double 类型的方法.
2.可以合并切点表达式 使用 &&, ||, ! 来合并。如:
execution(void run()) || execution(void say())
3.切面优先级:
可以通过实现 Ordered 接口或利用 @Order 注解指定。
(1)实现 Ordered 接口,getOrder() 方法返回的值越小,优先级越高。
(2)使用 @Order 注解,需要出现在注解中,同样是值越小优先级越高。
4.重用切点定义
在 AspectJ 切面中, 可以通过 @Pointcut 注解将一个切入点声明成简单的方法. 切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的。
切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中。
在这种情况下, 它们必须被声明为 public。在引入这个切入点时, 必须将类名也包括在内。如果类没有与这个切面放在同一个包中, 还必须包含包名。
其他通知可以通过方法名称引入该切入点。
如:
@Pointcut("execution(void run())") public void LoggingPointcut(){}; @Before("LoggingPointcut()") public void before() { System.out.println("before"); }
八、总结
1.在学习 Spring AOP 之前,最好先理解了 静态代理,JDK动态代理,CGLIB动态代理。
2.明白切面和增强以及切点之间的关系
3.几个增强之间的区别
除环绕增强外,所有的连接点使用的都是 JoinPoint 类型的入参,而环绕增强使用的是 ProceedingJoinPoint。
返回增强能接受到返回值
抛出增强能接收到抛出的异常
环绕增强的返回值类型为目标方法返回值类型
4.Spring AOP 支持声明式和基于注解的方式,注解优先。
5.步骤:
1.定义切面,以及增强,编写切点表达式
2.如果切点表达式是基于注解的,还需要对目标方法添加对应的注解。