作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
到目前为止,在Java基础进阶这个章节,我们已经帮大家梳理了很多晦涩但极其重要的知识点,包括反射、注解和泛型。这些都是我们迈向中高级程序员的小碎步,我们已经离“成熟的码农”越来越近了,但还不够。今天,我们仍需一起再往前走一小步:JDK动态代理。个人认为Java基础有“四大神兽”,除了刚才说的反射、注解和泛型,JDK动态代理就是最后一道坎。
“动态代理”四个字一出来,估计很多初学者已经开始冒冷汗。它之所以给人感觉很难,有三点原因:
- 代码形式很诡异,让人搞不清调用逻辑
- 用到了反射,而很多初学者不了解反射(现在你应该感觉好些了)
- 包含代理模式的思想,本身比较抽象
尽管动态代理看起来似乎有一定难度,但却必须拿下。因为Spring的事务控制依赖于AOP,AOP底层实现便是动态代理 + 责任链,环环相扣。所以说,搞编程的,拼到到最后还是看基本功,要么是语言基础、要么是计算机基础。
一个小需求:给原有方法添加日志打印
假设你刚进入一个项目组,项目中存在一个Calculator类,代表一个计算器,它可以进行加减乘除操作:
public class Calculator {// 加public int add(int a, int b) {int result = a + b;return result;}// 减public int subtract(int a, int b) {int result = a - b;return result;}// 乘法、除法...
}
现在老大给你提了一个需求:在每个方法执行前后打印日志。
你有什么好的方案?
方案一:直接修改
很多人最直观的想法是直接修改Calculator类:
public class Calculator {// 加public int add(int a, int b) {System.out.println("add方法开始...");int result = a + b;System.out.println("add方法结束...");return result;}// 减public int subtract(int a, int b) {System.out.println("subtract方法开始...");int result = a - b;System.out.println("subtract方法结束...");return result;}// 乘法、除法...
}
上面的方案是有问题的:
- 直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭
- 如果Calculator类内部有几十个、上百个方法,修改量太大
- 存在重复代码(都是在核心代码前后打印日志)
- 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了,于是你又要打开Calculator花十分钟删除日志打印的代码(或回滚分支)!
所以,此种方案PASS!
方案二:静态代理实现日志打印
“静态代理”四个字包含了两个概念:静态、代理。我们先来了解什么叫“代理”,至于何为“静态”,需要和“动态”对比着讲。
代理是一种模式,提供了对目标对象的间接访问方式,即通过代理访问目标对象。如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。
常用的代理方式可以粗分为:静态代理和动态代理。
静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。
是不是有点晕?是的,我最讨厌这种干巴巴的描述。简而言之,就是这样:
按上面的描述,代理类和目标类需要实现同一个接口,所以我打算这样做:
- 将Calculator抽取为接口
- 创建目标类CalculatorImpl实现Calculator
- 创建代理类CalculatorProxy实现Calculator
抽取接口
/*** Calculator接口*/
public interface Calculator {int add(int a, int b);int subtract(int a, int b);
}
原目标类实现接口
/*** 目标类,实现Calculator接口(如果一开始就面向接口编程,其实是不存在这一步的,CalculatorImpl原本就实现Calculator接口)*/
public class CalculatorImpl implements Calculator {// 加public int add(int a, int b) {int result = a + b;return result;}// 减public int subtract(int a, int b) {int result = a - b;return result;}// 乘法、除法...
}
新增代理类并实现接口
/*** 静态代理类,实现Calculator接口*/
public class CalculatorProxy implements Calculator {// 代理对象内部维护一个目标对象引用private Calculator target;// 通过构造方法,传入目标对象public CalculatorProxy(Calculator target) {this.target = target;}// 调用目标对象的add,并在前后打印日志@Overridepublic int add(int a, int b) {System.out.println("add方法开始...");int result = target.add(a, b);System.out.println("add方法结束...");return result;}// 调用目标对象的subtract,并在前后打印日志@Overridepublic int subtract(int a, int b) {System.out.println("subtract方法开始...");int result = target.subtract(a, b);System.out.println("subtract方法结束...");return result;}// 乘法、除法...
}
测试案例
使用代理对象完成加减乘除,并且打印日志:
public class Test {public static void main(String[] args) {// 把目标对象通过构造器塞入代理对象Calculator calculator = new CalculatorProxy(new CalculatorImpl());// 代理对象调用目标对象方法完成计算,并在前后打印日志calculator.add(1, 2);calculator.subtract(2, 1);}
}
静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了上一种方案4大缺点中的第1、4两点:
- 直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭(✅,如果一开始就面向接口编程,这一步其实是不需要的)
- 如果Calculator类内部有几十个、上百个方法,修改量太大(❎,目标类有多少个方法,代理类就要重写多少个方法)
- 存在重复代码(都是在核心代码前后打印日志)(❎,代理类中的日志代码是重复的)
- 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了(✅,别用代理类就好了)
静态代理的问题
上面的代码中,为了给目标类做日志增强,我们编写了代理类,而且准备了一个构造器接收目标对象。代理代理对象构造器的参数类型是Calculator,这意味着它只能接受Calculator的实现类对象,亦即我们写的代理类CalculatorProxy只能给Calculator做代理,它们绑定死了!
如果现在我们系统需要全面改造,要给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类...
自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!
你细品上面加粗的这句话,是不是好像一句废话?没有类哪来的对象?!
其实我的意思是,能否让JVM根据接口自动生成代理对象呢?
比如,有没有一个方法,我传入接口+增强的代码(比如打印日志),它就给我自动返回代理对象呢?这样就能省去编写代理类这个无用的“中介”了,没有中间商赚差价,岂不爽哉?
JDK,能做到吗?
预知后事如何,请听下回分解~
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬