背景问题
- 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀.每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点
- 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志打印代码.如果日志需求发生变化,必须修改所有模块
AOP简介
- AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充
- AOP的主要编程对象是切面(aspect),而切面模块化横切关注点
- 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类.这样一来横切关注点就被模块化到特殊的对象(切面)里.
- AOP的好处:
- 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
- 业务模块更简洁,只包含核心业务代码
AOP术语
- 切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
- 通知(Advice):切面必须要完成的工作
- 目标(Target):被通知的对象
- 代理(Proxy):向目标对象应用通知之后创建的对象
- 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如ArithmethicCalculator#add()方法执行前的连接点,执行点为ArithmethicCalculator#add();方位为该方法执行前的位置
- 切点(pointcut):每个类都拥有多个连接点:例如ArithmethicCalculator的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件
用AspectJ注解声明切面
- 要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为Bean实例.当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那么与AspectJ切面相匹配的Bean创建代理
- 在AspectJ注解中,切面只是一个带有@Aspect注解的Java类
- 通知是标注有某种注解的简单的Java方法
- AspectJ支持5中类型的通知注解:
- @Before:前置通知,在方法执行之前执行
- @After:后置通知,在方法执行之后执行
- @AfterRunning:返回通知,在方法返回结果之后执行
- @AfterThrowing:异常通知,在方法抛出异常之后执行
- @Around:环绕通知,围绕着方法执行
前置通知
- @Before(“execution(方法路径)”)
- AspectJ:Java社区里最完整最流行的AOP框架,在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP
- 在Spring中启用AspectJ注解支持:
1. 要在Spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:aoplliance.jar、aspectj.weaver.jar和spring-aspects.jar
2. 将aopSchema添加到<beans>
根元素中.
3. 要在SpringIOC容器中启用AspectJ注解支持,只要在Bean配置文件中定义一个空的XML元素<aop:aspectj-autoproxy>
4. 当Spring IOC容器侦测到Bean配置文件中的<aop:aspectj-autoproxy>
元素时,会自动为与AspectJ切面匹配的Bean创建代理
/声明该方法是一个前置通知:在目标方法开始前执行/
@Before("execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method is begining");
System.out.println("The method is " + methodName + " and args are " + args);
}
后置通知
// 声明该方法是一个后置通知:在目标方法结束后执行(无论是否发生异常)
// 在后置通知中不能访问目标方法执行的结果
@After("execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method is end");
System.out.println("The method is " + methodName + " and args are " + args);
}
返回通知
// 声明该方法是一个返回通知:在目标方法正常结束后执行
// 在返回通知中可以访问目标方法执行的结果
@AfterReturning(value = "execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))", returning = "result")
public void afterReturnMethod(Object result){
System.out.println("The method is end");
System.out.println("The result is " + result);
}
异常通知
// 声明该方法是一个异常通知:在目标方法抛出异常后执行
@AfterThrowing(value = "execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))", throwing = "exception")
public void afterThrowMethod(NullPointerException exception){
System.out.println("The method throwing exception ");
System.out.println("The exception is " + exception);
}*/
环绕通知
// 声明该方法是一个环绕通知,需要携带ProceedingJoinPoint类型的参数
// 环绕通知类似于动态代理的全过程;ProceedingJoinPoint类型的参数可以决定是否执行目标方法
// 且环绕通知必须有返回值,返回值即为目标方法的返回值
@Around("execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
String methodName = proceedingJoinPoint.getSignature().getName();
// 执行目标方法
try{
// 前置通知
System.out.println("The method:" + methodName + " begings with:" + Arrays.asList(proceedingJoinPoint.getArgs()));
result = proceedingJoinPoint.proceed();
System.out.println("The result is:" + result);
} catch (Throwable throwable) {
System.out.println("异常通知");
throwable.printStackTrace();
}
// 后置通知
System.out.println("The method is ended");
return result;
}
引入通知(较少使用)
引入通知是一种特殊的通知类型。它通过为接口提供实现类,允许对象动态的实现接口,就像对象已经在运行时扩展了实现类一样
切面的优先级
使用@Order类注解指定优先级,值越小优先级越高如@Order(2)
重用切点表达式
可以定义一个方法,用于声明切入点表达式.一般地,该方法中不再需要填入其他的代码,例如:
在切面类中定义方法:
//使用@Pointcut来声明切入点表达式,其他通知直接使用方法名来引用当前的切入点表达式
@Pointcut("execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))")
public void declareJointPointExpression(){}
然后,上面前置通知、后置通知…使用引用“execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))”的地方可以替换为declareJointPointExpression(),如果是在其他类(包)中引用,需要带上类名(及包名)
基于配置文件配置AOP
- 先在配置文件中配置bean
- 配置AOP:
//<!-- 配置AOP -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut expression="execution(public int spring_aop.ArithmeticCalculatorImpl.*(String))" id="pointcut">
<!-- 配置切面及通知-->
<aop:aspect ref="切面bean的id" order="2">
<aop:before method="beforeMethod" pointcut-ref="pointcut">
</aop:aspect>
</aop:config>