AOP(Aspect-Oriented Programming, 面向切面编程)
Aspect(切面):指对横切关注点的抽象即为切面。
Joinpoint(连接点):被拦截到的点,一般是方法的调用。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置。
Pointcut(切入点):对joinpoint进行拦截的定义,每个类都拥有多个连接点。
例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
Advice(通知):指拦截到joinpoint之后所做的事情就是通知。前置、后置、异常、最终、环绕等。
Target(目标对象):代理的目标对象
Proxy(代理):向目标对象应用通知之后创建的对象
AspectJ:Java 社区里最完整最流行的 AOP 框架
在Bean 配置文件中定义一个空的 XML 元素 <aop:aspectj-autoproxy>
<aop:aspectj-autoproxy> 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理.
AspectJ 注解切面声明
要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例.
在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类.
通知是标注有某种注解的简单的 Java 方法.
AspectJ 支持 5 种类型的通知注解:
@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行
@AfterRunning: 返回通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行
例子:
@Aspect
public class LogAspects {
//抽取公共的切入点表达式
//1、本类引用
//2、其他的切面引用
@Pointcut("execution(public int com.aop.MathCalculator.*(..))")
public void pointCut(){};
//@Before在目标方法之前切入;切入点表达式(指定在哪个方法切入)
@Before("pointCut()")
public void logStart(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
System.out.println(""+joinPoint.getSignature().getName()+"运行。。。 @Before:参数列表是:{"+Arrays.asList(args)+"}");
}
@After("com.aop.LogAspects.pointCut()")
public void logEnd(JoinPoint joinPoint){
System.out.println(""+joinPoint.getSignature().getName()+"结束。。。 @After");
}
//JoinPoint一定要出现在参数表的第一位
@AfterReturning(value="pointCut()",returning="result")
public void logReturn(JoinPoint joinPoint,Object result){
System.out.println(""+joinPoint.getSignature().getName()+"正常返 回。。。@AfterReturning:运行结果:{"+result+"}");
}
@AfterThrowing(value="pointCut()",throwing="exception")
public void logException(JoinPoint joinPoint,Exception exception){
System.out.println(""+joinPoint.getSignature().getName()+"异常。。。 异常信息:{"+exception+"}");
}
}
AspectJ 切入点表达式
1 execution(“ * com.spring.ArithmeticCalculator.*(..)”): 匹配 ArithmeticCalculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值. 第二个 * 代表任意方法. .. 匹配任意数量的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.
2 execution(“ public * ArithmeticCalculator.*(..)”): 匹配 ArithmeticCalculator 接口的所有公有方法.
3 execution(“ public double ArithmeticCalculator.*(..)”): 匹配 ArithmeticCalculator 中返回 double 类型数值的方法
4 execution(“ public double ArithmeticCalculator.*(double, ..)”): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数
5 execution(“ public double ArithmeticCalculator.*(double, double)”): 匹配参数类型为 double, double 类型的方法.
环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点. 甚至可以控制是否执行连接点.
对于环绕通知来说, 连接点的参数类型必须是 ProceedingJoinPoint . 它是 JoinPoint 的子接口, 允许控制何时执行, 是否执行连接点.
在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法. 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.
注意: 环绕通知的方法需要返回目标方法执行之后的结果, 即调用 joinPoint.proceed(); 的返回值, 否则会出现空指针异常
切面优先级
在同一个连接点上应用不止一个切面时, 它们的优先级是不确定的.
1 切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定.
2 实现 Ordered 接口, getOrder() 方法的返回值越小, 优先级越高.
@Aspect
@Order(1)
public class LogAspects{}
@Aspect
public class LogAspects implements Ordered{
public int getOrder() {
return 1;
}
}
切点
切入点方法的访问控制符同时也控制着这个切入点的可见性. 如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中. 在这种情况下, 它们必须被声明为 public. 在引入这个切入点时, 必须将类名也包括在内. 如果类没有与这个切面放在同一个包中, 还必须包含包名.
引入通知
在开发中有时候需要给一些类添加接口,spring提供了一种快速的方法,可以在切面中通过@DeclareParents指定满足指定表达式的类将自动实现某些接口。
Value: 属性值也可以是一个 AspectJ 类型的表达式, 以将一个即可引入到多个类中. defaultImpl: 属性中指定这个接口使用的实现类。
例如:
下面是新定义了一个Calculator接口
public interface Calculator {
public int get(float a);
}
下面是两种Calculator的具体实现
public class CalculatorImpl implements Calculator{
public int get(float a) {
return Math.round(a);
}
}
public class CalculatorImpl2 implements Calculator{
public int get(float a) {
return (int)Math.floor(a);
}
}
下面是指定切面来做实现MathCalculator类动态实现接口
@Aspect
public class LogAspects implements Ordered{
@DeclareParents(value="com.aop.MathCalculator",defaultImpl=CalculatorImpl.class)
private Calculator calculator;
}
public void test01(){
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);
MathCalculator mathCalculator = applicationContext.getBean(MathCalculator.class);
Calculator calculator=(Calculator) mathCalculator;
System.out.println(calculator.get(4.5f));
applicationContext.close();
}
如上动态接口的实现,现需要我们声明一个Calculator 类型的属性,然后通过@DeclareParents指定的value匹配类及defaultImpl的具体实现,可以得出不同的结果。
XML 的配置声明切面
<aop:config> 元素用来定义切面配置。
<aop:aspect> 元素来为具体的切面定义
XML声明实现:
<!-- 定义LoggingAspect切面bean -->
<bean id="loggingAspect"
class="com.spring.aop.xml.LoggingAspect"></bean>
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut expression="execution(* com.spring.aop.xml.ArithmeticCalculator.*(int, int))"
id="pointcut"/>
<!-- 配置切面及通知 -->
<aop:aspect ref="loggingAspect">
<!-- 前置通知 -->
<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
<!-- 后置通知 -->
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
<!-- 异常通知 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
<!-- 返回通知 -->
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
</aop:aspect>
</aop:config>
在这里提及一下tx事务的XML配置:
<!-- 2. 配置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRES_NEW"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 3. 配置事务切入点, 以及把事务切入点和事务属性关联起来 -->
<aop:config>
<aop:pointcut expression="execution(* com.atguigu.spring.tx.xml.service.*.*(..))"
id="txPointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>