Spring – AOP(10)
- 2020 年 3 月 17 日
- 筆記
编写一个简单的需求:要求在程序执行期间追踪正在发生的活动
// 接口 public interface AtithmeticCalculator { 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 AtithmeticCalculatorImpl implements AtithmeticCalculator { @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 add begins with [" + i + "," + j + "]"); int result = i - j; System.out.println("The method add ends with " + result); return result; } @Override public int mul(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 div(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; } }
// Main public class Main { public static void main(String[] args) { AtithmeticCalculator atithmeticCalculator = new AtithmeticCalculatorImpl(); int result = atithmeticCalculator.add(1,2); System.out.println(result); result = atithmeticCalculator.div(4,2); System.out.println(result); } } // Output The method add begins with [1,2] The method add ends with 3 3 The method add begins with [4,2] The method add ends with 2 2
上面例子中的实现代码存在的问题:
- 代码混乱:非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点
- 代码分散:以日志需求为例,只是为了满足这个单一的需求,就不得不在多个模块(方法)里多次重复相同的日志代码,如果日志需求发生变化,必须修改所有的模块
使用动态代理解决上述问题 代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及合适将方法调用转到原始对象上
// 接口 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 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 ArithmeticCalculatorLoggingProxy { // 要代理的对象 private ArithmeticCalculator target; public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target){ this.target = target; } public ArithmeticCalculator getLoggingProxy(){ ArithmeticCalculator proxy = null; // 代理对象由哪一个类加载器负责加载 ClassLoader loader = target.getClass().getClassLoader(); // 代理对象的类型,即其中有哪些方法 Class [] interfaces = new Class[]{ArithmeticCalculator.class}; // 当调用代理对象其中的方法时,该执行的代码 InvocationHandler h = new InvocationHandler() { /** * * @param proxy 正在返回的那个代理对象,一般情况下,在invoke方法中都不使用该对象 * @param method 正在被调用的方法 * @param args 调用方法时,传入的参数 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // 日志 System.out.println("The method" + methodName + "begins with " + Arrays.asList(args)); // 执行方法 Object result = method.invoke(target,args); // 日志 System.out.println("The method" + methodName + "ends with " + result); return result; } }; proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader,interfaces,h); return proxy; } }
// Main public class Main { public static void main(String[] args) { ArithmeticCalculator target = new ArithmeticCalculatorImpl(); ArithmeticCalculator proty = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy(); int result = proty.add(1,2); System.out.println(result); result = proty.div(8,2); System.out.println(result); } }
使用AOP
AOP面向切面编程,是一种新的方法论,并对传统OOP(面向对象编程)的补充
在应用AOP编程时,仍然需求定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类,这样依赖横切关注点就被模块化到特殊的对象(切面)里

AOP术语
切面(Aspect):横切关注点 (跨越应用程序多个模块的功能)被模块化的特殊对象 通知(Advice):切面必须要完成的工作 目标(Target):被通知的对象 代理(Proxy):向目标对象应用通知之后创建的对象 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前,调用后,方法抛出异常后等。 切点(pointcut):每个类都拥有多个连接点。即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点
Spring中启用AspectJ注解支持
AspectJ:Java社区里最完整最流行的AOP框架
通知是标注有注解的简单的Java方法: @Before
前置通知,在方法执行之前执行 @After
后置通知,在方法执行之后执行 @AfterRunning
返回通知,在方法返回结果之后执行 @AfterThrowing
异常通知,在方法抛出异常之后 @Around
环绕通知,环绕着方法执行
// maven注入依赖 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.1.12.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency>
// 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); }
// ArithmeticCalculatorImpl @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; } }
// ArithmeticCalculatorLoggingProxy public class ArithmeticCalculatorLoggingProxy { // 要代理的对象 private ArithmeticCalculator target; public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target){ this.target = target; } public ArithmeticCalculator getLoggingProxy(){ ArithmeticCalculator proxy = null; // 代理对象由哪一个类加载器负责加载 ClassLoader loader = target.getClass().getClassLoader(); // 代理对象的类型,即其中有哪些方法 Class [] interfaces = new Class[]{ArithmeticCalculator.class}; // 当调用代理对象其中的方法时,该执行的代码 InvocationHandler h = new InvocationHandler() { /** * @param proxy 正在返回的那个代理对象,一般情况下,在invoke方法中都不使用该对象 * @param method 正在被调用的方法 * @param args 调用方法时,传入的参数 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // 日志 System.out.println("The method" + methodName + "begins with " + Arrays.asList(args)); // 执行方法 Object result = method.invoke(target,args); // 日志 System.out.println("The method" + methodName + "ends with " + result); return result; } }; proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader,interfaces,h); return proxy; } }
// LoggingAspect @Component @Aspect public class LoggingAspect { // 声明该方法是一个前置通知:在目标方法开始之前执行 // @Before("execution(public int com.sangyu.test10.ArithmeticCalculator.add(int, int))") // 只针对add方法 @Before("execution(public int com.sangyu.test10.ArithmeticCalculator.*(int, int))") // 所有方法:add、mul、div、sub public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("The method " + methodName + " begins with" + args); } }
// Main public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); ArithmeticCalculator arithmeticCalculator = ctx.getBean(ArithmeticCalculator.class); int result = arithmeticCalculator.add(3,6); System.out.println("result: " + result); result = arithmeticCalculator.sub(2,1); System.out.println("result: " + result); result = arithmeticCalculator.mul(2,1); System.out.println("result: " + result); result = arithmeticCalculator.div(6,3); System.out.println("result: " + result); } }
前置通知
在方法执行之前执行的通知,使用@Before注解,并将切入点表达式的值作为注解值

编写AspectJ切入点表达式
通过方法的签名来匹配各种方法: execution * com.sangyu.test10.ArithmeticCalculator.*(...)
匹配ArithmeticCalculator中声明的所有方法,第一个代表任意修饰符及任意返回值,第二个 代表任意方法,…表示匹配任意数量的参数(若目标类于接口与该切面在同一个包中,可以省略包名)
execution public * com.sangyu.test10.ArithmeticCalculator.*(...)
匹配ArithmeticCalculator接口的所有共有方法
execution public double com.sangyu.test10.ArithmeticCalculator.*(...)
匹配ArithmeticCalculator中返回double类型数值的方法
execution public double com.sangyu.test10.ArithmeticCalculator.*(double,...)
匹配第一个参数为double类型的方法 …匹配任意数量任意类型的参数
execution public double com.sangyu.test10.ArithmeticCalculator.*(double,double)
匹配参数类型为double,double类型的方法
execution * *,*(...)
执行任意类的任意方法
JoinPoint
通知方法中beforeMethod()中声明一个类型为JoinPoint的参数,可以访问链接细节,如方法名称和参数值

后置通知
在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候,下面的后置通知记录了方法的终止,一个切面可以包括一个或者多个通知
@Component @Aspect public class LoggingAspect { // 声明该方法是一个前置通知:在目标方法开始之前执行 @Before("execution(public int com.sangyu.test10.ArithmeticCalculator.add(int, int))") // 只针对add方法 // @Before("execution(public int com.sangyu.test10.ArithmeticCalculator.*(int, int))") // 所有方法:add、mul、div、sub public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("The method " + methodName + " begins with" + args); } // 后置通知:在目标方法执行后(无论是否发生异常),执行的通知 @Before("execution(public int com.sangyu.test10.ArithmeticCalculatorImpl.*(int, int))") // 所有方法:add、mul、div、sub public void afterMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); System.out.println("The method " + methodName + " end"); } }