面向切面编程(aspect-oriented programming),是一种将横切关注点与业务逻辑分离的编程方式。每个横切关注点都集中在一个地方,而不是分散在多处代码中。这样使我们的服务模块更加简洁,因为它们只包含了主要关注点的代码,而次要的功能或者说辅助的功能被转移到切面中了。
上图表示划分为三个服务模块的应用,每个模块提供相应的服务,而且这些模块都需要类似的辅助功能,例如日志、安全、事务等等。我们并不想在各个模块中写重复的日志、安全、事务的代码,那么就可以使用选用切面这个方案,来解决这个问题。
advice
– 通知
切面的具体行为,即要切入执行的代码
pointcut
– 切点
通知被应用的具体位置
join point
– 连接点
程序运行时,能够应用通知的所有点
aspect
– 切面
什么时候在什么地方做什么事情,是切点和通知的结合
target
– 目标对象
被切入功能的目标对象
introduction
– 引入
将新的方法或属性引入到现有的类中
weaving
– 织入
把切面应用到目标对象并创建新的代理对象的过程
代理模式是使用代理对象完成用户请求,屏蔽用户对真实对象访问的一种设计模式。现实生活中,代理人被授权执行当事人的一些事宜,无需当事人出面,从第三方的角度看,他只和代理人通信。而事实上代理人是要有当事人的授权,并且在核心问题上还需要请示当事人。
AOP 就是使用代理模式实现的,其中的代理类就相当于AOP中的切面。
之所以称为静态代理,是因为在程序运行前,代理类就已经存在了。
举个例子
一般艺人都需要助理,来帮他跑腿,演出前谈价格,演出后收钱,只有表演的时候艺人才亲自出马。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package org.nocoder.proxy.staticproxy; /** * 艺人接口 * @author jason * @date 2019/2/14. */ public interface Artist { /** * 表演 */ void perform(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package org.nocoder.proxy.staticproxy; /** * 艺人刘德华 * * @author jason * @date 2019/2/14. */ public class LiuDehua implements Artist { private LiuDehua() { } public static LiuDehua getInstance() { return new LiuDehua(); } public void perform() { System.out.println("刘德华出场演唱笨小孩..."); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package org.nocoder.proxy.staticproxy; /** * 艺人助理(代理类) * * @author jason * @date 2019/2/14. */ public class ArtistProxy implements Artist { private LiuDehua liuDehua; private ArtistProxy() { } public ArtistProxy(LiuDehua liuDehua) { this.liuDehua = liuDehua; } public void perform() { System.out.println("助理出面谈价格,签合同..."); liuDehua.perform(); System.out.println("助理出面收钱..."); } } |
main
方法,将艺人实例传入代理类的构造方法,然后调用代理类的perform()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package org.nocoder.proxy.staticproxy; /** * @author jason * @date 2019/2/14. */ public class StaticProxyDemo { public static void main(String[] args) { LiuDehua liuDehua = LiuDehua.getInstance(); ArtistProxy proxy = new ArtistProxy(liuDehua); proxy.perform(); } } |
1 2 3 4 5 |
助理出面谈价格,签合同... 刘德华出场演唱笨小孩... 助理出面收钱... Process finished with exit code 0 |
静态代理的缺点
假设主题接口中的方法很多,为每一个接口写一个代理方法也很麻烦。如果接口有变动,则真实主题和代理类都要修改,不利于系统维护;
动态代理是在程序运行时,利用Java反射机制动态的生成代理类的代理模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package org.nocoder.proxy.jdkproxy; /** * 图书服务接口 * @author jason * @date 2019/2/14. */ public interface BookService { /** * 新增图书接口 */ void addBook(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package org.nocoder.proxy.jdkproxy; /** * 图书服务实现类 * @author jason * @date 2019/2/14. */ public class BookServiceImpl implements BookService { @Override public void addBook() { System.out.println("新增图书"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package org.nocoder.proxy.jdkproxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * JDK 动态代理 * @author jason * @date 2019/2/14. */ public class BookServiceInvocationHandler implements InvocationHandler { private Object target; public BookServiceInvocationHandler(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("事务开始"); Object result = method.invoke(target, args); System.out.println("事务结束"); return result; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package org.nocoder.proxy.jdkproxy; import java.lang.reflect.Proxy; /** * JDK动态代理演示 * * @author jason * @date 2019/2/14. */ public class JdkProxyDemo { public static void main(String[] args) { BookServiceInvocationHandler invocationHandler = new BookServiceInvocationHandler(new BookServiceImpl()); // JDK 动态代理只支持接口 BookService bookServiceProxy = (BookService) Proxy.newProxyInstance( BookServiceImpl.class.getClassLoader(), BookServiceImpl.class.getInterfaces(), invocationHandler); bookServiceProxy.addBook(); } } |
1 2 3 4 5 |
事务开始 新增图书 事务结束 Process finished with exit code 0 |
JDK的动态代理依靠接口实现,如果有些类并没有实现接口,则不能使用JDK代理,这时就要使用cglib动态代理了。使用cglib需要依赖cglib
的jar,使用maven为例
1 2 3 4 5 |
<dependency> <groupId>cglib</groupId> <artifactId>cglib-nodep</artifactId> <version>3.2.4</version> </dependency> |
1 2 3 4 5 6 7 8 9 10 11 |
package org.nocoder.proxy.cglibproxy; /** * @author jason * @date 2019/2/14. */ public class BookService { public void addBook() { System.out.println("新增图书"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package org.nocoder.proxy.cglibproxy; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * @author jason * @date 2019/2/14. */ public class BookServiceMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("事务开始"); methodProxy.invokeSuper(o, objects); System.out.println("事务结束"); return null; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package org.nocoder.proxy.cglibproxy; import net.sf.cglib.proxy.Enhancer; /** * @author jason * @date 2019/2/14. */ public class CglibProxyDemo { public static void main(String[] args) { BookServiceMethodInterceptor cglibInterceptor = new BookServiceMethodInterceptor(); //cglib 强化剂 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(BookService.class); // 创建代理对象 enhancer.setCallback(cglibInterceptor); BookService bookServiceProxy = (BookService)enhancer.create(); bookServiceProxy.addBook(); } } |
1 2 3 4 5 |
事务开始 新增图书 事务结束 Process finished with exit code 0 |
Spring 提供了4中 aop 的支持,基于代理的经典SpringAOP,纯POJO切面,@Aspect注解驱动的切面,注入式AspectJ切面。前三种都是SpringAOP实现的变体,SpringAOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。第四种实际是使用AspectJ的解决方案。提供了SpringAOP所不能支持的许多类型的切点。我们可以借助Spring的依赖注入把bean装配进AspectJ切面中。Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP。
@Aspect
创建切面类Audience类使用@Aspect
注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。Audience有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐(takeSeats())并将手机调至静音状态(silenceCellPhones())。如果演出很精彩的话,观众应该会鼓掌喝彩(applause())。不过,如果演出没有达到观众预期的话,观众会要求退款(demandRefund())。
注解 | 通知 |
---|---|
@After | 通知方法会在目标方法返回或抛异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
@Pointcut
声明切点表达式@Pointcut 声明使用频繁的切点表达式后,在通知注解中引用即可
from:https://www.jianshu.com/p/be6cb39dbeb7