如何自己实现 Spring AOP?

共 5694字,需浏览 12分钟

 ·

2022-02-22 09:57

以下内容来自公众号逆锋起笔,关注每日干货及时送达

转自:张喜硕

链接:https://segmentfault.com/a/1190000019148468

正好遇到了一道这样的题:抛开Spring来说,如何自己实现Spring AOP?就喜欢这样的题,能把那些天天写增删改查从来不思考的人给PK下去,今天就和大家一切学习代理模式与Spring AOP

| 代理与装饰器

场景描述

代理,即替代之意,可替代所有功能,即和原类实现相同的规范
代理模式和装饰器模式很像,之前的装饰器讲的不是很好,这里换个例子再讲一遍。
宁静的午后,来到咖啡馆,想喝一杯咖啡。

基础实现

给你一个咖啡接口:
public interface Coffee { /** * 打印当前咖啡的原材料,即咖啡里有什么 */ void printMaterial();}
一个默认的苦咖啡的实现:
public class BitterCoffee implements Coffee {
@Override public void printMaterial() { System.out.println("咖啡"); }}
默认的点餐逻辑:
public class Main {
public static void main(String[] args) { Coffee coffee = new BitterCoffee(); coffee.printMaterial(); }}
点一杯咖啡。

装饰器模式

优雅的服务生把咖啡端了上来,抿了一口,有些苦。想加点糖,对服务生说:“您好,请为我的咖啡加些糖”。
/** * 糖装饰器,用来给咖啡加糖 */public class SugarDecorator implements Coffee {
/** * 持有的咖啡对象 */ private final Coffee coffee;
public SugarDecorator(Coffee coffee) { this.coffee = coffee; }
@Override public void printMaterial() { System.out.println("糖"); this.coffee.printMaterial(); }}
然后服务生就拿走了我的咖啡,去使用SugarDecorator为咖啡加糖,最后把加好糖的咖啡给我。
public class Main {
public static void main(String[] args) { Coffee coffee = new BitterCoffee(); coffee = new SugarDecorator(coffee); coffee.printMaterial(); }}
看一看咖啡的成分,对的,确实加上了糖!
注意看这两行:
Coffee coffee = new BitterCoffee(); // 点了一杯苦咖啡coffee = new SugarDecorator(coffee);       // 给咖啡加了点糖
装饰器模式适合什么场景,我有一个对象,但是这个对象的功能不能令我满意,我就拿装饰器给他装饰一下。

代理模式

周末了,又抱着iPad来到了咖啡馆,准备享受一个宁静的下午。
“先生,请问您要喝点什么?”一旁礼貌的服务生上前问道。
上次点的咖啡太苦了,这次直接要个加糖的吧。
“我要一杯加糖咖啡。”
public class CoffeeWithSugar implements Coffee {
private final Coffee coffee;
public CoffeeWithSugar() { this.coffee = new BitterCoffee(); }
@Override public void printMaterial() { System.out.println("糖"); this.coffee.printMaterial(); }}
这是加糖咖啡,其实内部仍然是咖啡,只是加了些配方,就产生了一种新类,一种新的可以在菜单上呈现的饮品。
点咖啡:
public class Main {
public static void main(String[] args) { Coffee coffee = new CoffeeWithSugar(); coffee.printMaterial(); }}
正合我意,在咖啡的陪伴下度过了一个美好的下午。

差别

故事讲完了,两者实现的都是对原对象的包装,持有原对象的实例,差别在于对外的表现。
装饰器模式:点了咖啡,发现太苦了,不是自己想要的,然后用装饰器加了点糖。
Coffee coffee = new BitterCoffee();coffee = new SugarDecorator(coffee);
代理模式:直接就点的加糖咖啡。
Coffee coffee = new CoffeeWithSugar();
很细微的差别,希望大家不要弄混。

批评

去看代理模式相关的资料,五花八门,怎么理解的都有。
我觉得菜鸟教程的代理模式解释的最为正宗:在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。微信搜索readdot,关注后回复视频教程获取23种精品资料
还有,网上许多设计模式的文章都是你抄我、我抄你,一个错了,全都错了。
我觉得我需要纠正一下。谁说代理模式一定要用接口的啊?代理模式是设计模式,设计模式不分语言,假如一门语言中没有接口,那它就不能代理模式了吗?只是Java中的接口可以让我们符合依赖倒置原则进行开发,降低耦合。用抽象类可以吗?可以。用类继承可以吗?也可以。
思想明白了,用什么写还不是像玩一样?

| AOP

设计模式是思想,所以我上面说的代理模式不是仅适用于接口便与Spring AOP息息相关。
AOPAspect Oriented Programming,面向切面编程,是面向对象编程的补充。如果你不明白这句话,好好去学学面向对象就知道为什么了。
我们会声明切面,即切在某方法之前、之后或前后都执行。而Spring AOP的实现就是代理模式。

场景

正好最近写过短信验证码,就拿这个来当例子吧。
public interface SMSService {
void sendMessage();}public class SMSServiceImpl implements SMSService {
@Override public void sendMessage() { System.out.println("【梦云智】您正在执行重置密码操作,您的验证码为:1234,5分钟内有效,请不要将验证码转发给他人。"); }}
主函数:
public class Main {
public static void main(String[] args) { SMSService smsService = new SMSServiceImpl(); smsService.sendMessage(); smsService.sendMessage(); }}

费用统计

老板改需求了,发验证码要花钱,老板想看看一共在短信上花了多少钱。
正常按Spring的思路,肯定是声明一个切面,来切发短信的方法,然后在切面内统计短信费用。
只是现在没有框架,也就是这道题:抛开Spring来说,如何自己实现Spring AOP
写框架考虑的自然多些,我上文讲的代理是静态代理,编译期间就决定好的。而框架实现却是动态代理,需要在运行时生成代理对象,因为需要进行类扫描,看看哪些个类有切面需要生成代理对象。

JDK动态代理

编写一个统计短信费用的类实现InvocationHandler接口。
写到这,终于明白为什么每次后台Debug的时候都会跳转到invoke方法。
public class MoneyCountInvocationHandler implements InvocationHandler {
/** * 被代理的目标 */ private final Object target;
/** * 内部存储钱的总数 */ private Double moneyCount;
public MoneyCountInvocationHandler(Object target) { this.target = target; this.moneyCount = 0.0; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = method.invoke(target, args); moneyCount += 0.07; System.out.println("发送短信成功,共花了:" + moneyCount + "元"); return result; }}
将主函数里的smsService替换为使用MoneyCountInvocationHandler处理的代理对象。
public class Main {
public static void main(String[] args) { SMSService smsService = new SMSServiceImpl(); smsService = (SMSService) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{SMSService.class}, new MoneyCountInvocationHandler(smsService)); smsService.sendMessage(); smsService.sendMessage(); }}


根据InvocationHandler中的invoke方法动态生成一个类,该类实现SMSService接口,代理对象,就是用这个类实例化的。


AOP实现

上面的都实现了?写一个AOP是不是也不是难事?
主函数的代码,应该放在IOC容器初始化中,扫描包,去看看哪些个类需要生成代理对象,然后构造代理对象到容器中。
然后在invoke方法里,把统计费用的逻辑改成切面的逻辑不就好了吗?

不足分析

结束了吗?当然没有,上面的方法实现仅对接口有效。
因为JDK的动态代理,是生成一个实现相应接口的代理类。但是Spring又不是只能通过接口注入。
@Autowiredprivate Type xxxxx;
Spring@Autowired是通过声明的类型去容器里找符合的对象然后注进来的,接口是类型,类不也是类型吗?
@Autowiredprivate SMSService smsService;
这样能注进来。
@Autowiredprivate SMSServiceImpl smsService;
这样呢?也能注进来。
所以,JDK动态代理针对直接注入类类型的,就代理不了。

cglib动态代理

自古以来,从来都是时势造英雄,而不是英雄创造了时代。
出现了问题,自然会有英雄出来解决。拯救世界的就是cglib
JDK动态代理解决不了的,统统交给cglib
就这个来说:
@Autowiredprivate SMSServiceImpl smsService;
不是使用接口注入的,JDK动态代理解决不了。cglib怎么解决的呢?它会根据当前的类,动态生成一个子类,在子类中织入切面逻辑。
然后使用子类对象代理父类对象。这就是为什么我上面说:代理模式,不要拘泥于接口。
所以织入成功的,都是子类能把父类覆盖的方法。
所以cglib也不是万能的,方法是final的,子类重写不了,它当然也无计可施了。

| 总结

读书读的是什么?是真正理解作者的思想,明白作者想歌颂什么、批判什么。
框架学的是什么?不只是为了提高开发效率,而是在使用的时候,就像与设计者交流一样,能真正明白框架设计者的思想,才算用明白一款框架。
如果我们都能做到这般,又何愁设计不出一款真正属于自己的框架呢?

9 个实用 shell 脚本,建议收藏!

IDEA 官方数据库管理神器,比 Navicat 还香?(文末赠书)

图解 SpringMVC 五大组件

编写 Spring MVC 控制器的 14 个技巧!

SpringBoot 打包部署最佳实践



浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报