AOP基础
通过例子来看 AOP 优势
想要为写好的 ArithmeticCalculator 添加日志
即每次运算前后添加
采用以下方法太过繁琐,修改内容需要每个跟着都修改,可维护性差
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);
}
public class ArithmeticCalculatorImp implements ArithmeticCalculator {
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;
}
public int sub(int i, int j) {
System.out.println("The method sub begins with["+i+","+j+"]");
int result = i - j;
System.out.println("The method sub ends with["+result+"]");
return result;
}
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;
}
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;
}
}
public class Main {
public static void main(String[] args) {
ArithmeticCalculator a = new ArithmeticCalculatorImp();
int result = a.add(1, 2);
System.out.println("-->" + result);
result = a.mul(5, 2);
System.out.println("-->" + result);
}
}
运行结果
The method add begins with[1,2]
The method add ends with[3]
-->3
The method mul begins with[5,2]
The method mul ends with[10]
-->10
问题:
代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾替他多个关注点。
代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码,如果日志需求发生变化,必须修改所有模块。
采用动态代理后的代码优势
public class ArithmeticCalculatorImp2 implements ArithmeticCalculator {
public int add(int i, int j) {
int result = i + j;
return result;
}
public int sub(int i, int j) {
int result = i - j;
return result;
}
public int mul(int i, int j) {
int result = i * j;
return result;
}
public int div(int i, int j) {
int result = i / j;
return result;
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class ArithmeticCalculatorImpLogging{
//要代理的对象
private ArithmeticCalculator target;
public ArithmeticCalculatorImpLogging(ArithmeticCalculator target){
this.target = target;
}
public ArithmeticCalculator getArithmeticCalculatorLogging() {
ArithmeticCalculator proxy = null;
//加载对象由哪一个类加载器负责加载
ClassLoader loader = target.getClass().getClassLoader();
//代理对象的类型,即其中有哪些方法
Class[] interfaces = target.getClass().getInterfaces();
//当调用代理对象其中的方法时,该执行的代码
InvocationHandler h = new InvocationHandler() {
/**
* proxy: 正在返回的那个代理对象,一般情况下,在 invoke 方法中都不使用该方法
* method: 正在被调用的方法
* args: 调用方法时,传入的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//日志
System.out.println("Proxy--The method"+methodName+"begins with"+ Arrays.asList(args));
//执行方法
Object result = method.invoke(target, args);
//日志
System.out.println("Proxy--The method"+methodName+"begins with"+ Arrays.asList(args));
return result;
}
};
proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
}
public class Main {
public static void main(String[] args)
ArithmeticCalculator target = new ArithmeticCalculatorImp2();
ArithmeticCalculator proxy = new ArithmeticCalculatorImpLogging(target)
.getArithmeticCalculatorLogging();
int result = proxy.add(1, 2);
System.out.println("-->" + result);
result = proxy.mul(5, 2);
System.out.println("-->" + result);
}
}
运行结果
Proxy--The methodaddbegins with[1, 2]
Proxy--The methodaddbegins with[1, 2]
-->3
Proxy--The methodmulbegins with[5, 2]
Proxy--The methodmulbegins with[5, 2]
-->10
AOP简介
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 接口进行描述,它使用类和方法作为连接点的查询条件。
AOP的五种通知
-前置通知
-简述-
1). 加入 jar 包
aopalliance-1.0.jar
aspectjweaver-1.8.7.jar
spring-aspects-4.0.2.RELEASE.jar
spring-aop-4.0.2.RELEASE.jar
commons-logging-1.1.1.jar
spring-beans-4.0.2.RELEASE.jar
spring-context-4.0.2.RELEASE.jar
spring-core-4.0.2.RELEASE.jar
spring-expression-4.0.2.RELEASE.jar
2). 在配置文件中加入 aop 的命名空间
3). 基于注解的方式
①. 在配置文件中加入如下配置:
<!-- 使 AspectJ 注解起作用: 自动为匹配的类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
②. 把横切关注点的代码抽象到切面的类中。
a). 切面首先是一个 IOC 中的 bean, 即加入 @Component 注解
b). 切面还需要加入 @Aspect 注解
③. 在类中声明各种通知:
a). 声明一个方法
b). 在方法前加入 @Before 注解
④. 可以在通知方法中声明一个类型为 JoinPoint 的参数, 然后就能访问链接细节, 如方法名称和参数值
//把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面
@Aspect
@Component
public class LoggingAspect {
//声明该方法是一个前置通知: 在目标方法开始之前执行
//@Before("execution(public int com.anqi.spring.aop.imp.ArithmeticCalculator.add(int, int))")
@Before("execution(public int com.anqi.spring.aop.imp.ArithmeticCalculator.*(int, int))")
public void beforeMehod(JoinPoint join) {
String methodName = join.getSignature().getName();
List<Object> args = Arrays.asList(join.getArgs());
System.out.println("The Method"+methodName+" begins"+ args);
}
}
<?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: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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.anqi.spring.aop.imp,com.anqi.spring.aop.imp"></context:component-scan>
<!-- 使 AspectJ 注解起作用: 自动为匹配的类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
//把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面
@Aspect
@Component
public class LoggingAspect {
/**
* 声明该方法是一个前置通知: 在目标方法开始之前执行
* @param join
*/
//@Before("execution(public int com.anqi.spring.aop.imp.ArithmeticCalculator.add(int, int))")
@Before("execution(public int com.anqi.spring.aop.imp.ArithmeticCalculator.*(int, int))")
public void beforeMehod(JoinPoint join) {
String methodName = join.getSignature().getName();
List<Object> args = Arrays.asList(join.getArgs());
System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
}
}
public class Main {
public static void main(String[] args) {
//1. 创建 Spring 的 IOC 容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 从 IOC 容器中获取 bean 的实例
ArithmeticCalculator arithmetic = ctx.getBean(ArithmeticCalculator.class);
//3. 使用 bean
int result = arithmetic.add(3, 6);
System.out.println(result);
result = arithmetic.mul(3, 6);
System.out.println(result);
result = arithmetic.div(12, 6);
System.out.println(result);
result = arithmetic.div(12, 0);
System.out.println(result);
}
}
输出结果
前置通知 --> The Methodadd begins[3, 6]
9
前置通知 --> The Methodmul begins[3, 6]
18
前置通知 --> The Methoddiv begins[12, 6]
2
前置通知 --> The Methoddiv begins[12, 0]
java.lang.ArithmeticException: / by zero
-后置通知
/**
* 后置通知: 在目标方法执行后(无论是否发生异常), 执行的通知
* @param join
*/
@After("execution(* com.anqi.spring.aop.imp.ArithmeticCalculator.div(int, int))")
public void afterMethod(JoinPoint join) {
String methodName = join.getSignature().getName();
List<Object> args = Arrays.asList(join.getArgs());
System.out.println("后置通知 --> The Method"+methodName+" ends"+ args);
}
输出结果
9
18
后置通知 --> The Methoddiv ends[12, 6]
2
后置通知 --> The Methoddiv ends[12, 0]
-返回通知
/**
* 返回通知
* 在方法正常结束执行的代码
* 返回通知是可以访问到方法的返回值的
* @param join
* @param result
*/
@AfterReturning(value="execution(public int com.anqi.spring.aop.imp.ArithmeticCalculator.*(..))"
,returning="result")
public void afterReturning(JoinPoint join, Object result) {
String methodName = join.getSignature().getName();
System.out.println("返回通知 --> The Method"+methodName+" ends with"+ result);
}
输出结果
返回通知 --> The Methodadd ends with9
9
返回通知 --> The Methodmul ends with18
18
返回通知 --> The Methoddiv ends with2
2
java.lang.ArithmeticException: / by zero
-异常通知
/**
* 在目标方法出现异常时会执行的代码
* 可以访问到异常对象; 且可以指定出现特定异常时再执行通知代码
* @param join
* @param ee
*/
@AfterThrowing(value="execution(public int com.anqi.spring.aop.imp.ArithmeticCalculator.*(..))"
,throwing="ee")
public void afterThrowing(JoinPoint join, Exception ee) {
String methodName = join.getSignature().getName();
System.out.println("异常通知 --> The Method"+methodName+" occur exceptions"+ ee);
}
输出结果
9
18
2
异常通知 --> The Methoddiv occur exceptionsjava.lang.ArithmeticException: / by zero
-环绕通知(最强大,但并不是最常用)
/**
* 环绕通知需要携带 ProceedingJoinPoint 类型的参数
* 环绕通知类似于动态代理的全过程:ProceedingJoinPoint 类型的参数可以决定是否执行目标方法
* 且环绕通知必须有返回值, 返回值即为目标的返回值
* @param pjp
* @return
*/
@Around("execution(public int com.anqi.spring.aop.imp.ArithmeticCalculator.*(..))")
public Object aroundMehod(ProceedingJoinPoint pjp) {
Object result = null;
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
try {
//前置通知
System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));
//执行目标方法
result = pjp.proceed();
//返回通知
System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);
}catch(Throwable e) {
//异常通知
System.out.println("!!!异常通知 --> The Method"+methodName+" ends with"+ result);
}
//后置通知
System.out.println("!!!后置通知 --> The Method"+methodName+" ends"+ args);
return result;
}
输出结果
!!!前置通知 --> The Methodadd begins[3, 6]
!!!返回通知 --> The Methodadd ends[Ljava.lang.Object;@647e447
!!!后置通知 --> The Methodadd ends[Ljava.lang.Object;@647e447
9
!!!前置通知 --> The Methodmul begins[3, 6]
!!!返回通知 --> The Methodmul ends[Ljava.lang.Object;@41fbdac4
!!!后置通知 --> The Methodmul ends[Ljava.lang.Object;@41fbdac4
18
!!!前置通知 --> The Methoddiv begins[12, 6]
!!!返回通知 --> The Methoddiv ends[Ljava.lang.Object;@3c407114
!!!后置通知 --> The Methoddiv ends[Ljava.lang.Object;@3c407114
2
!!!前置通知 --> The Methoddiv begins[12, 0]
!!!异常通知 --> The Methoddiv ends withnull
!!!后置通知 --> The Methoddiv ends[Ljava.lang.Object;@35ef1869
AOP切面的优先级
//参数验证切面
@Order(1)
@Aspect
@Component
public class ValidateAspect {
@Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
public void validateArgs(JoinPoint join) {
String methodName = join.getSignature().getName();
Object[] args = join.getArgs();
System.out.println("validate"+methodName+Arrays.asList(args));
}
}
//把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面
@Order(2)
@Aspect
@Component
public class LoggingAspect2 {
/**
* 声明该方法是一个前置通知: 在目标方法开始之前执行
* @param join
*/
@Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
public void beforeMehod(JoinPoint join) {
String methodName = join.getSignature().getName();
List<Object> args = Arrays.asList(join.getArgs());
System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
}
}
前置通知 --> The Methodadd begins[5, 5]
validateadd[5, 5]
result->10
使用 @Order 以后
validateadd[5, 5]
前置通知 --> The Methodadd begins[5, 5]
result->10
重用切点表达式
//把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面
@Order(2)
@Aspect
@Component
public class LoggingAspect {
/**
* 定义一个方法, 用于声明切入点表达式, 一般地, 该方法中再不需要填入其他代码
*/
@Pointcut("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
public void declareJointPointExpression() {}
/**
* 声明该方法是一个前置通知: 在目标方法开始之前执行
* @param join
*/
@Before("declareJointPointExpression()")
public void beforeMehod(JoinPoint join) {
String methodName = join.getSignature().getName();
List<Object> args = Arrays.asList(join.getArgs());
System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
}
}
//参数验证切面
@Order(1)
@Aspect
@Component
public class ValidateAspect {
//如果在相同的包,前面可以不写包名
@Before("com.anqi.spring.aop.order.LoggingAspect.declareJointPointExpression()")
public void validateArgs(JoinPoint join) {
String methodName = join.getSignature().getName();
Object[] args = join.getArgs();
System.out.println("validate"+methodName+Arrays.asList(args));
}
}
基于配置文件(XML)的方式来配置 AOP
<?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: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/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- 配置 bean -->
<bean id="arithmeticCalculator"
class = "com.anqi.spring.aop.xml_imp.ArithmeticCalculatorImp2"></bean>
<!-- 配置切面的 bean -->
<bean id = "loggingAspect"
class = "com.anqi.spring.aop.xml_imp.LoggingAspect"></bean>
<bean id = "validateAspect"
class = "com.anqi.spring.aop.xml_imp.ValidateAspect"></bean>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut expression="execution(public int com.anqi.spring.aop.xml_imp.ArithmeticCalculator.*(int,int))"
id="pointcut"/>
<!-- 配置切面及通知 -->
<aop:aspect ref="loggingAspect" order="2">
<aop:before method="beforeMehod" pointcut-ref="pointcut"/>
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ee"/>
<!-- <aop:around method="aroundMehod" pointcut-ref="pointcut" /> -->
</aop:aspect>
<aop:aspect ref="validateAspect" order="1">
<aop:before method="validateArgs" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
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);
}
public class ArithmeticCalculatorImp2 implements ArithmeticCalculator {
public int add(int i, int j) {
int result = i + j;
return result;
}
public int sub(int i, int j) {
int result = i - j;
return result;
}
public int mul(int i, int j) {
int result = i * j;
return result;
}
public int div(int i, int j) {
int result = i / j;
return result;
}
}
public class LoggingAspect {
public void beforeMehod(JoinPoint join) {
String methodName = join.getSignature().getName();
List<Object> args = Arrays.asList(join.getArgs());
System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
}
public void afterMethod(JoinPoint join) {
String methodName = join.getSignature().getName();
List<Object> args = Arrays.asList(join.getArgs());
System.out.println("后置通知 --> The Method"+methodName+" ends"+ args);
}
public void afterReturning(JoinPoint join, Object result) {
String methodName = join.getSignature().getName();
System.out.println("返回通知 --> The Method"+methodName+" ends with"+ result);
}
public void afterThrowing(JoinPoint join, Exception ee) {
String methodName = join.getSignature().getName();
System.out.println("异常通知 --> The Method"+methodName+" occur exceptions"+ ee);
}
public Object aroundMehod(ProceedingJoinPoint pjp) {
Object result = null;
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
try {
//前置通知
System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));
//执行目标方法
result = pjp.proceed();
//返回通知
System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);
}catch(Throwable e) {
//异常通知
System.out.println("!!!异常通知 --> The Method"+methodName+" ends with"+ result);
}
//后置通知
System.out.println("!!!后置通知 --> The Method"+methodName+" ends"+ args);
return result;
}
}
public class ValidateAspect {
//如果在相同的包,前面可以不写包名
public void validateArgs(JoinPoint join) {
String methodName = join.getSignature().getName();
Object[] args = join.getArgs();
System.out.println("validate"+methodName+Arrays.asList(args));
}
}
public class Main {
public static void main(String[] args) {
//1. 创建 Spring 的 IOC 容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext_xml.xml");
//2. 从 IOC 容器中获取 bean 的实例
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
//3. 使用 bean
int result = arithmetic.add(3, 6);
System.out.println(result);
result = arithmetic.mul(3, 6);
System.out.println(result);
result = arithmetic.div(12, 6);
System.out.println(result);
result = arithmetic.div(12, 0);
System.out.println(result);
}
}