【Spring4.0】基于注解方式配置SpringAOP

##一、什么是AOP?

面向侧面的程序设计(aspect-oriented programming,AOP,又译作面向方面的程序设计、观点导向编程、剖面导向程序设计)是计算机科学中的一个术语,指一种程序设计范型。该范型以一种称为侧面(aspect,又译作方面)的语言构造为基础,侧面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。

侧面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与侧面相关的编程概念还包括元对象协议、主题(subject)、混入(mixin)和委托。
——维基百科-面向切面编程



##二、我们为什么需要AOP?


    举一个例子,当我们写好一个方法后,突然接到需求说在这个方法运行之前、运行之后、出现异常时写一个日志出来(这里的日志我们当作时在控制台System.ouut.println()一些文字,如开始方法时提示“方法已启动”,方法结束时输出“方法运行结束”,如此类推)。
    我们在不使用AOP的情况下,只能时在方法的开头插入一个输出语句输出“方法已启动”,方法的结尾输出一个方法已结束。这样做当方法少只有十来二十个的时候工作量比较低,还能够接受,但是在工作中方法可能有很多,如果我们这样一个个方法来添加就会很麻烦,还有当日志的内容模板需要改动的时候又要给各个方法重新写一次,这样大大增加了后期维护工作的难度。
    还有当在逻辑代码中插入这些日志代码,那代码的整洁程度就不高,程序员看代码就会很乱,效果如下:

###1.创建一个名为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);
}

###2.创建一个名为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;
	}
}

###3.创建一个用于测试java类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的原理
这里写图片描述
原本我们写程序的时候都会按照上图的业务逻辑来做:先验证参数–>写前置日志–>执行方法–>写后置日志。这样做的话会昌盛和多验证参数,前置日志,后置日志的重复代码,在开发中也不宜代码有过多重复,会导致代码和软件体积臃肿。SpringAOP把验证参数,前置日志,后置日志分别抽取出来,各形成一个新的事务逻辑,这样每个事务逻辑位于一个位置,后期维护的时候更加轻松,也避免了代码重复的尴尬,让业务模块更简洁, 只包含核心业务代码,能够让开发开发人员更加注重逻辑代码。



##四、AOP术语

术语 解释
切面(Aspect) 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
通知(Advice) 切面必须要完成的工作
目标(Target) 被通知的对象
代理(Proxy) 向目标对象应用通知之后创建的对象
连接点(Joinpoint) 程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为ArithmethicCalculator#add();方位为该方法执行前的位置
切点(pointcut) 每个类都拥有多个连接点:例如ArithmethicCalculator的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。




##五、示例代码

###1.添加相关的JAR包
这里写图片描述

##2.新建一个top.cheungchingyin.spring.aop
新建一个applicationContext.xml

![这里写图片描述](https://img-blog.csdn.net/20180803115147142?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTk2OTc4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
**(项目结构)**

###3.创建一个名为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);
}

###4.创建一个名为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;
	}

}

###5.在applicationContext.xml增添配置

在 Spring 中启用 AspectJ 注解支持
(1)要在 Spring 应用中使用 AspectJ 注解, 必须在 classpath 下包含AspectJ类库: aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar
(2)将 aop Schema 添加到 <beans>根元素中.
(3)要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素
<aop:aspectj-autoproxy>
(4)当 Spring IOC 容器侦测到 Bean 配置文件中的<aop:aspectj-autoproxy> 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理.

<?xml version="1.0" encoding="UTF-8"?>
<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>
	<!-- 配置自动为匹配aspectJ注解的Java类生成代理对象 -->
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

###6.编写切面类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类型的参数可以决定是否执行目标方法。且环绕通知必须有返回值,返回值即为目标方法的返回值。

###7、通知优先级

  • 在同一个连接点上应用不止一个切面时, 除非明确指定, 否则它们的优先级是不确定的.
  • 切面的优先级可以通过实现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()注解来分辨优先级,数字越小,优先程度越高。

###8、重用切入点定义

(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(){}
**(LoggingAspect.java代码片段)**

(3) 切入点方法的访问控制符同时也控制着这个切入点的可见性. 如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中. 在这种情况下, 它们必须被声明为 public. 在引入这个切入点时, 必须将类名也包括在内. 如果类没有与这个切面放在同一个包中, 还必须包含包名,如LoggingAspect.java中的例子

(4) 其他通知可以通过方法名称引入该切入点(如VlidationAspect.java)。

@Before("LoggingAspect.declareJoinPointExpression()")

猜你喜欢

转载自blog.csdn.net/qq_33596978/article/details/81383498