面向侧面的程序设计(aspect-oriented programming,AOP,又译作面向方面的程序设计、观点导向编程、剖面导向程序设计)是计算机科学中的一个术语,指一种程序设计范型。该范型以一种称为侧面(aspect,又译作方面)的语言构造为基础,侧面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。
侧面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与侧面相关的编程概念还包括元对象协议、主题(subject)、混入(mixin)和委托。
——维基百科-面向切面编程
举一个例子,当我们写好一个方法后,突然接到需求说在这个方法运行之前、运行之后、出现异常时写一个日志出来(这里的日志我们当作时在控制台System.ouut.println()
一些文字,如开始方法时提示“方法已启动”,方法结束时输出“方法运行结束”,如此类推)。
我们在不使用AOP的情况下,只能时在方法的开头插入一个输出语句输出“方法已启动”,方法的结尾输出一个方法已结束。这样做当方法少只有十来二十个的时候工作量比较低,还能够接受,但是在工作中方法可能有很多,如果我们这样一个个方法来添加就会很麻烦,还有当日志的内容模板需要改动的时候又要给各个方法重新写一次,这样大大增加了后期维护工作的难度。
还有当在逻辑代码中插入这些日志代码,那代码的整洁程度就不高,程序员看代码就会很乱,效果如下:
ArithmeticCalculator
的java接口public interface ArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
ArithmeticCalculatorImpl
的java类public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
System.out.println("The method add begins with["+i+","+j+"]");
int result = i + j;
System.out.println("The method add ends with"+result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("The method sub begins with["+i+","+"]");
int result = i - j;
System.out.println("The method sub ends with"+result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("The method mul begins with["+i+","+j+"]");
int result = i * j;
System.out.println("The method mul ends with"+result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("The method div begins with["+i+","+j+"]");
int result = i / j;
System.out.println("The method div ends with"+result);
return result;
}
}
Main.java
public static void main(String[] args) {
int result = arithmeticCalculator.add(11, 12);
System.out.println("result:" + result);
result = arithmeticCalculator.div(21, 3);
System.out.println("result:" + result);
}
这样虽然要求的日志效果都出来了,但是我们发现ArithmeticCalculatorImpl
类中的那些方法都很乱,由于逻辑代码中混入了太多不相干的代码,导致看起来很混乱。在开发过程中,我们应该要保持代码的整洁,所以AOP因应而生。
原本我们写程序的时候都会按照上图的业务逻辑来做:先验证参数–>写前置日志–>执行方法–>写后置日志。这样做的话会昌盛和多验证参数,前置日志,后置日志的重复代码,在开发中也不宜代码有过多重复,会导致代码和软件体积臃肿。SpringAOP把验证参数,前置日志,后置日志分别抽取出来,各形成一个新的事务逻辑,这样每个事务逻辑位于一个位置,后期维护的时候更加轻松,也避免了代码重复的尴尬,让业务模块更简洁, 只包含核心业务代码,能够让开发开发人员更加注重逻辑代码。
术语 | 解释 |
---|---|
切面(Aspect) | 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象 |
通知(Advice) | 切面必须要完成的工作 |
目标(Target) | 被通知的对象 |
代理(Proxy) | 向目标对象应用通知之后创建的对象 |
连接点(Joinpoint) | 程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为ArithmethicCalculator#add(); 方位为该方法执行前的位置 |
切点(pointcut) | 每个类都拥有多个连接点:例如ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。 |
top.cheungchingyin.spring.aop
包新建一个applicationContext.xml
(项目结构)
ArithmeticCalculator
的java接口public interface ArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
ArithmeticCalculatorImpl
的java实现类!!注意!!
这个类和上面曾经提到过的类功能是一样的,但是这个类少了那些输出语句。
package top.cheungchingyin.spring.aop;
import org.springframework.stereotype.Component;
package top.cheungchingyin.spring.aop;
import org.springframework.stereotype.Component;
@Component()
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
applicationContext.xml
增添配置在 Spring 中启用 AspectJ 注解支持
(1)要在 Spring 应用中使用 AspectJ 注解, 必须在 classpath 下包含AspectJ
类库: aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar
(2)将 aop Schema 添加到
根元素中.
(3)要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素
(4)当 Spring IOC 容器侦测到 Bean 配置文件中的
元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">
<context:component-scan base-package="top.cheungchingyin.spring.aop">context:component-scan>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
beans>
LoggingAspect.java
一般编写流程:
(1)一个一般的 Java 类
(2)在其中添加要额外实现的功能。配置切面:
切面必须是 IOC 中的 bean: 实际添加了@Component
注解
声明是一个切面: 添加 @Aspect
声明通知: 即额外加入功能对应的方法。
以下为全部的代码,解释可以接着在下面看或者查看代码注释
package top.cheungchingyin.spring.aop;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(2)
@Component
@Aspect
public class LoggingAspect {
/*
* 定义一个方法,用于声明切入点的表达式,一般地,该方法中不需要添加其他代码
* 使用@Pointcut来声明切入点表达式
* 后面的其他通知直接使用方法名来引用切入点表达式
*/
@Pointcut("execution(int top.cheungchingyin.spring.aop.ArithmeticCalculator.*(..))")
public void declareJoinPointExpression(){}
/*
* 在top.cheungchingyin.spring.aop.
* ArithmeticCalculator接口的每一个实现类的每一个方法开始之前执行一段代码
*/
@Before("declareJoinPointExpression()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("The Method " + methodName + " Begins with " + Arrays.asList(args));
}
/*
* 返回通知:在方法执行后执行的代码,无论方法是否发生异常
*/
@After("declareJoinPointExpression()")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The Method " + methodName + " ends with");
}
/**
* 返回通知:在方法正常结束后执行代码 返回通知是可以访问到方法的返回值
*
* @param joinPoint
*/
@AfterReturning(value = "declareJoinPointExpression()", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The Method " + methodName + " ends with " + result);
}
/**
* 在方法出现异常的时候会执行代码 可以访问到的异常对象,且可以指定出现特定异常的时候才执行通知代码
*
* @param joinPoint
* @param ex
*/
@AfterThrowing(value = "declareJoinPointExpression()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The Method " + methodName + " occurs with " + ex);
}
/**
* 环绕通知需要携带ProceedingJoinPoint类型参数
* 环绕通知类似于动态代理的全过程,ProceedingJoinPoint类型的参数可以决定是否执行目标方法
* 且环绕通知必须要有返回值,返回值即为目标方法的返回值
*
* @param pjd
* @return
*/
@Around("declareJoinPointExpression()")
public Object arroundMethod(ProceedingJoinPoint pjd) {
Object result = null;
String methodName = pjd.getSignature().getName();
// 执行目标方法
try {
// 前置通知
System.out.println("【Around】The method 【" + methodName + "】 begins with" + Arrays.asList(pjd.getArgs()));
result = pjd.proceed();
// 后置通知
System.out.println("【Around】The Method " + " ends with 【" + result + "】");
} catch (Throwable e) {
// 异常通知
System.out.println("The method occurs exception" + e);
}
// 后置通知
System.out.println("【Around】The Method 【" + methodName + "】 ends ");
return result;
}
}
AspectJ
支持 5 种类型的通知注解:
注解 | 解释 |
---|---|
@Before | 前置通知, 在方法执行之前执行 |
@After | 在目标方法执行后(无论是否发生异常),执行额通知。在后置通知中还不能访问目标方法的执行结果 |
@AfterRunning | 在方法正常结束后执行的代码,返回通知是可以访问到方法的返回值的 |
@AfterThrowing | 在目标方出现异常时会执行的代码,可以访问到异常对象,且可以指定在出现特定异常时执行的通知代码 |
@Around | 环绕通知:需要携带ProceedingJoinPoint类型的参数,类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法。且环绕通知必须有返回值,返回值即为目标方法的返回值。 |
Ordered
接口或利用@Order
注解指定.Ordered
接口,getOrder()
方法的返回值越小, 优先级越高.@Order
注解, 序号出现在注解中举个例子,首先创建一个VlidationAspect
的java类
package top.cheungchingyin.spring.aop;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/*
* 可以使用@order注解来指定切面的优先级,值越小优先级越高
*/
@Order(1)
@Aspect
@Component
public class VlidationAspect {
@Before("LoggingAspect.declareJoinPointExpression()")
public void validateArgs(JoinPoint joinpoint){
System.out.println("validate :"+Arrays.asList(joinpoint.getArgs()));
}
}
这里使用了和LoggingAspect
类中的相同切点declareJoinPointExpression()
,这是如果使用到这个切点的时候Spring会分不清该先执行VlidationAspect#validateArgs(JoinPoint joinpoint)
这个方法还是先执行LoggingAspect#beforeMethod(JoinPoint joinPoint)
方法。这个时候优先级就是为此而生的,通过使用@Order()
注解来分辨优先级,数字越小,优先程度越高。
(1) 在编写 AspectJ 切面时, 可以直接在通知注解中书写切入点表达式. 但同一个切点表达式可能会在多个通知中重复出现如:
@Before("execution(int top.cheungchingyin.spring.aop.ArithmeticCalculator.*(..))")
@After("execution(int top.cheungchingyin.spring.aop.ArithmeticCalculator.*(..))")
(2) 在 AspectJ 切面中, 可以通过@Pointcut
注解将一个切入点声明成简单的方法. 切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的。
@Pointcut("execution(int top.cheungchingyin.spring.aop.ArithmeticCalculator.*(..))")
public void declareJoinPointExpression(){}
(3) 切入点方法的访问控制符同时也控制着这个切入点的可见性. 如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中. 在这种情况下, 它们必须被声明为 public. 在引入这个切入点时, 必须将类名也包括在内. 如果类没有与这个切面放在同一个包中, 还必须包含包名,如LoggingAspect.java
中的例子
(4) 其他通知可以通过方法名称引入该切入点(如VlidationAspect.java
)。
@Before("LoggingAspect.declareJoinPointExpression()")