一、前言
在上一篇中,介绍了使用代理来实现日志的记录,该方法在平时工作中不易于使用,因为要有一定的设计模式的基础。下面就来介绍下Spring的一个非常核心的概念AOP,即面向切面编程。
二、AOP术语说明
为了理解AOP,必须先了解AOP的相关术语:
1、通知(Advice):
在AOP中,切面的工作被称为通知。通知定义了切面“是什么”以及“何时”使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以应用5种类型的通知:
(1)前置通知(Before):前置通知, 在方法执行之前执行;
(2)后置通知(After):后置通知, 在方法执行之后执行 ;
(3)返回通知(After-returning):返回通知, 在方法返回结果之后执行;
(4)异常通知(After-throwing):异常通知, 在方法抛出异常之后;
(5)环绕通知(Around):环绕通知, 围绕着方法执行;
2、连接点(Join point):
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加行为。
3、切点(Pointcut):
如果说通知定义了切面“是什么”和“何时”的话,那么切点就定义了“何处”。比如我想把日志引入到某个具体的方法中,这个方法就是所谓的切点。
4、切面(Aspect):
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容———它是什么,在何时和何处完成其功能。
三、使用基于注解的方式实现AOP
1、需要引入相关的jar,使用maven,此处略;
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
2、在Spring的配置文件中加入 aop 的命名空间,此处略;
3、需要在Spring的配置文件中,加入如下配置:
<!-- 自动扫描的包 -->
<context:component-scan base-package="com.scorpios.spring.aop.impl"></context:component-scan>
<!-- 使 AspectJ 的注解起作用 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
组件扫描(component scanning):Spring 能够从classpath下自动扫描, 侦测和实例化具有特定注解的组件。
特定组件包括:
@Component: 基本注解, 标识了一个受Spring管理的组件
@Respository: 标识持久层组件
@Service: 标识服务层(业务层)组件
@Controller: 标识表现层组件
对于扫描到的组件, Spring有默认的命名策略: 使用非限定类名, 第一个字母小写。 也可以在注解中通过value属性值标识组件的名称
3、编写业务方法接口
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、实现业务方法 (注意此处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;
}
}
5、 编写切面类:(是一般的Java类,在其中添加要额外实现的功能,注意注解)
@Order(2)
@Aspect
@Component
public class LoggingAspect {
/**
* 定义一个方法,用于声明切入点表达式,一般的,该方法中不再需要其他的代码 使用@Pointcut来声明切入点表达式
* 后面的其他通知直接使用方法名来引用当前的切入点表达式。
*/
@Pointcut("execution(public * com.scorpios.spring.aop.impl.ArithmeticCalculator.*(..))")
public void declareJoinPointExpression() {
}
// 声明该方法是一个前置通知:在目标方法开始之前执行
@Before("declareJoinPointExpression()")
public void BeforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("[BeforeMethod] the method " + methodName + " begins with " + args);
}
// 后置通知:在目标方法执行后(无论是否发生异常),执行的通知
// 在后置通知中还不能访问目标方法执行的结果
@After("declareJoinPointExpression()")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[afterMethod] the method " + methodName + " ends.");
}
// 在方法正常结束时执行的代码
// 返回通知时可以访问到方法的返回值的!
@AfterReturning(value = "declareJoinPointExpression()", returning = "result")
public void afterReturing(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[afterReturing] the method " + methodName + " ends with :" + result);
}
// 在目标方法出现异常时会执行的代码
// 可以访问到异常对象;且可以指定在出现特定异常时在执行通知代码
@AfterThrowing(value = "declareJoinPointExpression()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[afterThrowing] the method " + methodName + " occurs with :" + ex);
}
// 环绕通知需要携带ProceedingJoinPoint类型的参数
// 环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法
// 且环绕通知必须有返回值,返回值即为目标方法的返回值
@Around("declareJoinPointExpression()")
public Object aroundMethod(ProceedingJoinPoint pjp) {
Object result = null;
String methodName = pjp.getSignature().getName();
try {
// 前置通知
System.out.println(
"[aroundMethod before] the method " + methodName + " begins with " + Arrays.asList(pjp.getArgs()));
result = pjp.proceed();
// 返回通知
System.out.println("[aroundMethod returning] the method ends with " + result);
} catch (Throwable e) {
// 异常通知
System.out.println("[aroundMethod exception] the method " + methodName + "occurs exception:" + e);
}
// 后置通知
System.out.println("[aroundMethod after] the method " + methodName + " ends");
return result;
}
}
6、测试代码
public class Main {
public static void main(String[] args) {
ApplicationContext apx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator arithmeticCalculator = apx.getBean(ArithmeticCalculator.class);
int result = arithmeticCalculator.add(2, 8);
System.out.println("-->" + result);
}
}
7、运行结果
8、具有优先级的切面类(注意切入点表达式,引用LoggingAspect中定义的表达式)
@Order(1)
@Aspect
@Component
public class ValidateAspect {
// 声明该方法是一个前置通知:在目标方法开始之前执行
@Before("LoggingAspect.declareJoinPointExpression()")
public void ValidateBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("**** validate " + args);
}
}
9、再次执行测试代码,结果如下
四、总结
1、引入jar包;
2、在Spring的配置文件中加入aop的命名空间。
3、在配置文件中配置自动扫描的包:
<context:component-scan base-package="com.scorpios.spring.aop.*"></context:component-scan>
4、加入使 AspjectJ 注解起作用的配置:
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>为匹配的类自动生成动态代理对象.
5、编写切面类:
6、配置切面
a.切面必须是IOC中的bean: 实际添加了@Component注解
b.声明是一个切面: 通过添加@Aspect注解声明一个bean是一个切面!
c.声明通知: 即额外加入功能对应的方法.
前置通知: @Before("execution(public int com.scorpios.spring.aop.ArithmeticCalculator.*(int, int))")
@Before 表示在目标方法执行之前执行 @Before 标记的方法的方法体.
@Before 里面的是切入点表达式:
7、在通知中访问连接细节: 可以在通知方法中添加 JoinPoint 类型的参数, 从中可以访问到方法的签名和方法的参数.
五、补充:
关于切入点表达式的解释:
如图所示,我们使用execution()指示器选择UserServiceImpl的sayHello方法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的sayHello()方法,无论该方法的入参是什么。