接着上一篇文章的内容Spring框架完全掌握(上),我们继续深入了解Spring框架。
Spring_AOP
考虑到AOP在Spring中是非常重要的,很有必要拿出来单独说一说。所以本篇文章基本上讲述的就是关于Spring的AOP编程。
简介
先看一个例子:
package com.itcast.spring.bean.calc;
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int num1, int num2) {
int result = num1 + num2;
return result;
}
@Override
public int sub(int num1, int num2) {
int result = num1 - num2;
return result;
}
@Override
public int mul(int num1, int num2) {
int result = num1 * num2;
return result;
}
@Override
public int div(int num1, int num2) {
int result = num1 / num2;
return result;
}
}
这是一个实现四则运算接口的实现类,能够进行两个数之间的加减乘除。而这个时候,我们有一个需求,就是在每个方法执行前后都必须输出日志信息,那么我们就得在每个方法中都加上日志信息:
...
@Override
public int add(int num1, int num2) {
System.out.println("add method start with[" + num1 + "," + num2 + "]");
int result = num1 + num2;
System.out.println("add method start with[" + num1 + "," + num2 + "]");
return result;
}
...
这样所带来的问题是什么呢?
- 代码混乱:越来越多的非业务需求(例如日志、参数验证等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾其它多个关注点。
- 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块里多次重复相同的日志代码,如果日志需求发生变化,必须修改所有模块中的日志代码。
既然问题出现了,该如何解决呢?(使用动态代理)
public class ArithmeticCalculatorLoggingProxy {
private ArithmeticCalculator target;
public ArithmeticCalculator getLoggingProxy() {
ArithmeticCalculator proxy = null;
ClassLoader loader = target.getClass().getClassLoader();
Class[] interfaces = new Class[] { ArithmeticCalculator.class };
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "method start with[ " + Arrays.asList(args) + "]");
Object result = method.invoke(target, args);
System.out.println(method.getName() + "method end with[ " + result + "]");
return result;
}
};
proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
}
这样我们就可以去获取代理对象从而实现日志业务却不改变基本业务代码。
其实这样实现还是略显麻烦,但不用担心,Spring框架为我们提供了一种实现方式——AOP。
AOP(Aspect-Oriented Programming,面向切面编程):这是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充,AOP的主要编程对象是切面。
在应用AOP编程时,仍然需要定义公共功能,但可以明确地定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类,这样一来,横切关注点就被模块化到特殊的对象里。
好处:
- 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
- 业务模块更简洁,只包含核心业务代码
这样来看,AOP能够非常精准地解决我们遇到了问题。
前置通知
在Spring中,可以使用基于AspectJ注解或基于XML配置的AOP。AspectJ是Java社区里最完整最流行的AOP框架,所以我们以AspectJ注解方式为例进行讲解。
首先导入AOP框架的jar包:
然后我们在上面的案例中进行修改:
@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int num1, int num2) {
int result = num1 + num2;
return result;
}
@Override
public int sub(int num1, int num2) {
int result = num1 - num2;
return result;
}
@Override
public int mul(int num1, int num2) {
int result = num1 * num2;
return result;
}
@Override
public int div(int num1, int num2) {
int result = num1 / num2;
return result;
}
}
这里在实现类的开头加上了一个注解,目的是将该类交由Spring容器管理,其它代码不作改动。
//将该类声明为一个切面
@Aspect
@Component
public class LoggingAspect {
// 声明该方法是一个前置通知:在目标方法开始之前执行
@Before("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.add(int,int))")
public void beforeMethd(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method start with" + args);
}
}
接着我们将输出日志的业务看成一个切面,创建一个类,然后任意地定义一个方法,该方法要添加一个注解:Before。用于声明该方法是一个前置通知,前置通知方法会在目标方法开始之前执行。所以我们还需要在Before中声明目标方法。该方法可以添加一个参数为JoinPoint类型,执行方法的方法名和参数都封装在该对象中。其次,该类必须也交由Spring容器管理,所以添加注解@Component,且该类为一个切面,添加注解@Aspect。
然后要在配置文件中进行配置:
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan
base-package="com.itcast.aop.impl"></context:component-scan>
<!-- 使AspjectJ注解起作用:自动为匹配的类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
这样,框架会去自动寻找匹配的类并生成代理对象。
最后编写测试代码:
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);
int result = ac.add(1, 1);
System.out.println("result:" + result);
}
运行结果:
add method start with[1, 1]
result:2
但是当你调用其它的运算方法时发现日志信息又无法打印了,这是因为你在配置目标方法的时候配置的仅仅是add()方法,所以可以采用通配符的方式将类中的所有方法都配置进去。
@Before("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")
这里的exeution是执行的意思,也就是说,该属性的括号内填写的是目标方法,对于该目标方法,可以更加抽象地进行表示,例如权限修饰符、返回值等等都可以用通配符进行替换。
到这里,SpringAOP就轻松实现了我们开始遇到的问题。
后置通知
既然有前置通知,那肯定就会有后置通知,后置通知的实现方式和前置通知类似:
@After("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")
public void afterMetohd(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method ends with" + args);
}
运行测试代码,结果如下:
add method start with[1, 1]
add method ends with[1, 1]
result:2
后置通知是在目标方法执行后执行,但需要注意的是,后置通知不管目标方法是否成功执行,就算目标方法在执行过程中产生了异常,后置通知仍然会执行,而且在后置通知中无法访问到目标方法的执行结果。
返回通知
返回通知和后置通知类似,但是返回通知只在目标方法正确执行完成后才执行,如果目标方法在执行过程中产生了错误,返回通知将不起作用。所以返回通知能够获取目标方法的执行结果:
// 声明该方法是一个返回通知:在方法正常执行结束后执行
// 返回通知是可以访问到目标方法的返回值的
@AfterReturning(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method ends with" + result);
}
运行结果:
add method start with[1, 1]
add method ends with[1, 1]
add method ends with2
result:2
异常通知
异常通知是在目标方法执行过程中产生了异常后才会执行,异常通知能够获取到目标方法产生的异常信息:
// 声明该方法是一个异常通知:在方法执行产生异常时执行
// 异常通知可以获取到产生的异常信息
@AfterThrowing(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method's exception is " + ex);
}
我们人为产生一个异常来测试一下:
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);
result = ac.div(10, 0);
System.out.println("result:" + result);
}
运行结果:
div method start with[10, 0]
div method ends with[10, 0]
div method's exception is java.lang.ArithmeticException: / by zero
环绕通知
对于环绕通知,这在所有通知中是功能最强大的通知,其实它并不常用,但是我们还是得了解一下它的用法:
// 声明该方法是一个环绕通知,环绕通知需要携带ProceedingJoinPoint类型的参数
// 环绕通知类似于动态代理的全过程
// ProceedingJoinPoint类型的参数可以决定是否执行目标方法
// 且环绕通知必须有返回值,返回的是目标方法的返回值
@Around(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")
public Object aroundMethod(ProceedingJoinPoint point) {
Object result = null;
String methodName = point.getSignature().getName();
// 执行目标方法
try {
// 前置通知
System.out.println(methodName + " method' start with" + Arrays.asList(point.getArgs()));
result = point.proceed();
// 返回通知
System.out.println(methodName + " method' end with " + result);
} catch (Throwable e) {
// 异常通知
System.out.println(methodName + " method's exception is " + e);
}
// 后置通知
System.out.println(methodName + " method' end with");
return result;
}
环绕通知能够实现其它所有通知的功能,但是它有很多限制。
- 必须要携带ProceedingJoinPoint类型的参数
- 环绕通知必须有返回值,返回的是目标方法的返回值
测试代码:
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);
int result = ac.add(1, 1);
System.out.println("result:" + result);
}
运行结果:
add method' start with[1, 1]
add method' end with 2
add method' end with
result:2
切面的优先级
在具有多个切面的项目中,我们可以指定切面的优先级,决定切面的先后执行顺序。使用@Order()注解来配置优先级(在类开头注解),括号里填入一个整数,值越小优先级越高。
例如:
@Order(1)
public class LoggingAspect {
......
......
}
关于SpringAOP的相关内容就说到这里,如有错误,欢迎指正。