一、什么是AOP?
AOP是面向切面编程(Aspect-Oriented Programming),它是一种新的方法论,是对传统的面向对象编程的一种补充,更具体的说是在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
引用知乎用户的描述:地址https://www.zhihu.com/question/24863332/answer/48376158
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。
从技术上来说,AOP基本上是通过代理机制实现的。
二、需求
从AOP角度来看,我们开发过程中有哪些现有的需求可以改造成以Spring AOP来实现?以下举个简单例子:
首先,定义一个计算器接口
public interface Calculator { // 加法接口 int add(int x,int y); // 减法接口 int sub(int x,int y); }
接着,定义一个计算器实现接口,并在其中加入操作日志
@Service public class CalculatorImpl implements Calculator{ @Override public int add(int x, int y) { System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y)); int z = x + y; System.out.println(String.format("接口执行结果,z=%s", z)); return z; } @Override public int sub(int x, int y) { System.out.println(String.format("接口接收参数,x=%s,y=%s", x,y)); int z = x - y; System.out.println(String.format("接口执行结果,z=%s", z)); return z; } }
然后,在IOC容器上添加注解扫描包
<?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" 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"> <!-- 开启注解扫描包 --> <context:component-scan base-package="com.spring"></context:component-scan> </beans>
最后,写个测试方法
public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Calculator cal = ctx.getBean(Calculator.class); cal.add(20, 10); cal.sub(20, 10); /** * 执行结果: * 接口接收参数,x=20,y=10 * 接口执行结果,z=30 * 接口接收参数,x=20,y=10 * 接口执行结果,z=10 */ } }
我们在每个接口执行真正的业务之前都添加了日志输出,在真正的业务执行之后也添加了日志输出,看着很简单,但如果一个接口内几百个乃至上千个接口呢,那么就得写好多日志;再者,如果你拼命的把日志写好了,发现里面有个错别字或者不符合规范,又
得重新改几百个乃至几千个日志输出的信息。
解决方案:
1.使用java动态代理来完成这个事情
2.使用Spring的AOP面向切面编程
三、动态代理
首先,我们先将service上的打印日志输出注释掉,引入动态代理类:
public class CalculatorProxy { // 要代理的对象(注意:代理的是接口) private Calculator target; // 初始化 public CalculatorProxy(Calculator target) { super(); this.target = target; } // 返回代理对象 public Calculator getLoggingProxy(){ Calculator proxy = null; ClassLoader loader = target.getClass().getClassLoader(); Class[] interfaces = new Class[]{Calculator.class}; InvocationHandler handler = new InvocationHandler() { /** * proxy: 代理对象。 一般不使用该对象 * method: 正在被调用的方法 * args: 调用方法传入的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args)throws Throwable { String methodName = method.getName(); //打印日志 System.out.println(String.format("接口接收参数,x=%s,y=%s", Arrays.asList(args).get(0),Arrays.asList(args).get(1))); //调用目标方法 Object result = null; result = method.invoke(target, args); //打印日志 int res = 0; if("add".equals(methodName)) { res = Integer.parseInt(Arrays.asList(args).get(0).toString()) + Integer.parseInt(Arrays.asList(args).get(1).toString()); }else { res = Integer.parseInt(Arrays.asList(args).get(0).toString()) - Integer.parseInt(Arrays.asList(args).get(1).toString()); } System.out.println(String.format("接口执行结果,z=%s", res)); return result; } }; /** * loader: 代理对象使用的类加载器。 * interfaces: 指定代理对象的类型. 即代理代理对象中可以有哪些方法. * h: 当具体调用代理对象的方法时, 应该如何进行响应, 实际上就是调用 InvocationHandler 的 invoke 方法 */ proxy = (Calculator) Proxy.newProxyInstance(loader, interfaces, handler); return proxy; } }
测试动态代理日志输出:
public class Main { public static void main(String[] args) { // 被代理对象,是一个接口实现类,即哪个实现类被代理 Calculator calculator = new CalculatorImpl(); // 返回代理对象 calculator = new CalculatorProxy(calculator).getLoggingProxy(); calculator.add(20, 10); calculator.sub(20, 10); /** * 执行结果: * 接口接收参数,x=20,y=10 * 接口执行结果,z=30 * 接口接收参数,x=20,y=10 * 接口执行结果,z=10 */ } }
四、AOP
我们必须要清楚的几个概念:
①.切面(Aspect): 横切关注点被模块化的特殊对象,图示如下:
即前置日志、后置日志两个需求是横切关注点,而抽取出横切关注点形成的就是切面
②.通知(Advice):切面必须要完成的工作,比如日志切面要完成的工作是日志,所以通知在这里就是日志
③.目标(Target):被通知的对象,即真正的业务方法,比如我们在calculator.add方法执行前添加日志,calculator.add方法就是目标
④.代理(Proxy):向目标对象应用通知之后创建的对象
⑤.连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等,这就是连接点。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。
例如 calculator.add() 方法执行前的连接点,执行点为 calculator.add();方位为该方法执行前的位置
⑥.切点(pointcut):每个类都拥有多个连接点:例如 calculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。