"中国最好面试官"
自从上次写了一篇 "[面试] 我是如何面试别人 List 相关知识的, 深度有点长文" 的文章后, 有读者专门加我微信, 说我是 "中国最好面试官", 这个我可受不起呀.
我只是希望把面试当作是一次交流, 像朋友那样, 而不是像一场 Q & A. 但也有人觉得, 我对应聘者 "太好了", 这完全没必要, 反正最后他也不会来.
好吧, 那这次我就 "使点坏","套路" 一下面试者.
记一次 "带套路" 的面试
与这个面试者聊了一会儿, 咦, 发现他水平还可以, 我内心有点儿喜出望外, 终于遇到一个 "合格" 的 "陪聊者" 了, 我要用 Spring 事务 "好好套路" 他一下.
我: 你在开发中, 一般都把事务加到哪一层?
他: 都加到 Service 层.
我: 现在基本都是基于注解的配置了, 那和事务相关的注解是哪个?
他: 我不太会读那个单词, 就是以 @T 开头的那个.
我: 我明白你的意思, 就是 @Transactional.
他: 是的.
我: 与自己写代码来开启和提交事务相比,(先给他来个小的套路), 这种通过注解来使用事务的方式叫什么?
他:(犹豫了两, 三秒), 不知道.
我: 如果把写代码那种叫编程式事务, 那与之相对的应该是什么式事务?
他: 哦, 声明式事务.
我:(先铺垫), 不加注解, 没有事务, 加上注解, 就有事务, 可见事务和注解有莫大的关系.(开始套路), 那加上注解后, 到底发生了什么变化呢, 就有了事务?
他:(犹豫了几秒钟), 不知道.
我:(哈哈, 意料之中), 那我换一问法, Spring 声明式事务的底层是怎么实现的?
他: 是通过代理实现的.
我:(铺垫), 代理这个词不仅计算机里有, 现实生活中也经常见到代理, 比如招华北地区总代理等等.(套路), 那你能不能在生活中举一个代理的例子?
他:(想了一会儿), 我没有想到什么好例子.
我:(开始聊会天), 我看你老家离这还挺远的, 你一般都什么时候回去啊?
他: 一般都国庆节或春节会回去. 其它时间假期短就不回去了.
我:(引子), 国庆节和春节, 人都很多啊, 票不好买吧?
他: 是啊, 都在网上抢高铁票, 不停地刷.
我:(引子), 现在有了高铁, 出行确实方便了很多. 那你知道以前没有高铁, 没有 12306 的时候, 人们都是怎么买票的吗?
他: 我虽然没有经历过, 但是我知道. 那时候春运, 都在火车站售票大厅买票, 人们排很长的队, 有时需要等半天, 还不一定有票.
我:(切入正题), 除了火车站售票大厅外, 你有没有见过在城市里分布的一些火车票代售点?
他: 现在偶尔还能见到几个, 但都已经关门了.
我: 是啊, 现在都网上买票了, 代售点算是被历史抛弃了.(开始套路), 那你觉得代售点算不算火车站售票大厅的代理呢?
他: 火车站售票大厅可以买票, 代售点也可以买票, 应该算是代理吧.
我: 从广义讲算是代理. 但有两点需要注意:
一是, 代售点卖的也是售票大厅的票, 它自己是没有票的, 它只是行使售票大厅的权利.
二是, 它可以有属于自己的行为特征, 比如不需要排队啊, 每张硬座票收 5 元手续费啊等等.
我们平时听到的中间商 / 代理商, 其实都差不多是一回事儿.
他: 经你这么一说, 我明白了.
我: 那我们再说回到 Spring 中的代理, 在 Spring 中生成代理的方式有几种?
他: 两种, JDK 动态代理和 CGLIB.
我: 那它们分别用于什么情况下?
他: JDK 动态代理只能用于带接口的, CGLIB 则带不带接口都行.
我:(铺垫), 假如有个接口, 它包含两个方法 a 和 b, 然后有一个类实现了该接口. 在该实现类里在 a 上标上事务注解, b 上不标, 此时事务是怎样的?
他: a 标注解了, 肯定有事务, b 没有注解, 所以没有事务.
我: 嗯, 是这样的.(开始套路), 现在来做个简单的修改, 在方法 b 里调用方法 a, 其它保持不变, 此时再调用方法 b, 会有事务吗?
他: 应该有吧, 虽然 b 没有注解, 但 a 有啊.
我:(我需要带带他), 假设现在你和我都不知道有没有事务, 那我们来分析分析, 看能不能找出答案. 你有分析思路吗?
他: 没有.
我: 行吧, 那我们开始. 这是一个带接口的, 那就假定使用 JDK 动态代理吧. 从宏观上看, 就是 Spring 使用 JDK 动态代理为这个类生成了一个代理, 并为标有注解的方法添加了和事务相关的代码, 所以就具有了事务. 那你知道这个代理大概会是什么样子的吗?
他: 这个不知道.
我: 通过代售点的例子我们应该知道, 所有的代理都具有以下特点:
代理是一个空壳, 它背后才是真正的老板.
代理可以行使老板的权力, 所以它看起来 "很像" 老板, 除非仔细查看, 否则不易区分.
代理自己可以按需加进去一些行为特征, 除非仔细查看, 否则老板都不一定知道这些.
那我们回到程序世界, 使用接口和类再套一下上面的特点:
代理类是一个空壳 (或外观), 它背后才是真正的类, 通常称为目标类. 由此得出代理类要包含目标类.
对目标类和代理类的使用方式是一样的, 甚至你都不知道它是代理类. 由此得出代理类和目标类的类型要兼容, 对外接口一致. 所以目标类实现的接口, 代理类也要实现.
代理类在把执行流程代理给目标类的过程中, 可以添加一些行为代码, 如开启事务, 提交事务等.
他: 经你这么一分析啊, 我知道该怎么写代码了, 应该是这样的, 请仔细看下代码, 虽然很简单:
- // 接口
- interface Service {
- void doNeedTx();
- void doNotneedTx();
- }
- // 目标类, 实现接口
- class ServiceImpl implements Service {
- @Transactional
- @Override
- public void doNeedTx() {
- System.out.println("execute doNeedTx in ServiceImpl");
- }
- //no annotation here
- @Override
- public void doNotneedTx() {
- this.doNeedTx();
- }
- }
- // 代理类, 也要实现相同的接口
- class ProxyByJdkDynamic implements Service {
- // 包含目标对象
- private Service target;
- public ProxyByJdkDynamic(Service target) {
- this.target = target;
- }
- // 目标类中此方法带注解, 进行特殊处理
- @Override
- public void doNeedTx() {
- // 开启事务
- System.out.println("-> create Tx here in Proxy");
- // 调用目标对象的方法, 该方法已在事务中了
- target.doNeedTx();
- // 提交事务
- System.out.println("<- commit Tx here in Proxy");
- }
- // 目标类中此方法没有注解, 只做简单的调用
- @Override
- public void doNotneedTx() {
- // 直接调用目标对象方法
- target.doNotneedTx();
- }
- }
我: 目标类是我们自己写的, 肯定是没有事务的. 代理类是系统生成的, 对带注解的方法进行事务增强, 没有注解的方法原样调用, 所以事务是代理类加上去的.
那回到一开始的问题, 我们调用的方法不带注解, 因此代理类不开事务, 而是直接调用目标对象的方法. 当进入目标对象的方法后, 执行的上下文已经变成目标对象本身了, 因为目标对象的代码是我们自己写的, 和事务没有半毛钱关系, 此时你再调用带注解的方法, 照样没有事务, 只是一个普通的方法调用而已.
他: 所以这个问题的答案就是没有事务.
我: 这是我们分析推理的结果, 究竟对不对呢, 还需要验证一下. 验证过程如下:
找一个正常可用的 Spring 项目, 把一个 @Service 的接口注入到一个 @Controller 类里面, 进行检测, 请仔细看下代码:
- // 是否是 JDK 动态代理
- System.out.println("isJdkDynamicProxy =>" + AopUtils.isJdkDynamicProxy(exampleService));
- // 是否是 CGLIB 代理
- System.out.println("isCglibProxy =>" + AopUtils.isCglibProxy(exampleService));
- // 代理类的类型
- System.out.println("proxyClass =>" + exampleService.getClass());
- // 代理类的父类的类型
- System.out.println("parentClass =>" + exampleService.getClass().getSuperclass());
- // 代理类的父类实现的接口
- System.out.println("parentClass's interfaces => " + Arrays.asList(exampleService.getClass().getSuperclass().getInterfaces()));
- // 代理类实现的接口
- System.out.println("proxyClass's interfaces => " + Arrays.asList(exampleService.getClass().getInterfaces()));
- // 代理对象
- System.out.println("proxy =>" + exampleService);
- // 目标对象
- System.out.println("target =>" + AopProxyUtils.getSingletonTarget(exampleService));
- // 代理对象和目标对象是不是同一个
- System.out.println("proxy == target =>" + (exampleService == AopProxyUtils.getSingletonTarget(exampleService)));
- // 目标类的类型
- System.out.println("targetClass =>" + AopProxyUtils.getSingletonTarget(exampleService).getClass());
- // 目标类实现的接口
- System.out.println("targetClass's interfaces => " + Arrays.asList(AopProxyUtils.getSingletonTarget(exampleService).getClass().getInterfaces()));
- System.out.println("----------------------------------------------------");
- // 自己模拟的动态代理的测试
- Service target = new ServiceImpl();
- ProxyByJdkDynamic proxy = new ProxyByJdkDynamic(target);
- proxy.doNeedTx();
- System.out.println("-------");
- proxy.doNotneedTx();
- System.out.println("-------");
以下是输出结果:
- // 是 JDK 动态代理
- isJdkDynamicProxy => true
- // 不是 CGLIB 代理
- isCglibProxy => false
- // 代理类的类型, 带 $ 的
- proxyClass => class com.sun.proxy.$Proxy82
- // 代理类的父类
- parentClass => class java.lang.reflect.Proxy
代理类的父类实现的接口
- parentClass's interfaces => [interface java.io.Serializable]
- // 代理类实现的接口, 包含了目标类的接口 IExampleService, 还有其它的
- proxyClass's interfaces => [interface org.eop.sb.example.service.IExampleService,
- interface org.springframework.aop.SpringProxy,
- interface org.springframework.aop.framework.Advised,
- interface org.springframework.core.DecoratingProxy]
- // 代理对象
- proxy => org.eop.sb.example.service.impl.ExampleServiceImpl@54561bc9
- // 目标对象
- target => org.eop.sb.example.service.impl.ExampleServiceImpl@54561bc9
- // 代理对象和目标对象输出的都是 @54561bc9, 还真有点懵逼
- // 进行测试后发现, 其实不是同一个, 只是 toString() 的问题
- proxy == target => false
- // 目标类, 我们自己写的
- targetClass => class org.eop.sb.example.service.impl.ExampleServiceImpl
- // 目标类实现的接口, 我们自己写的
- targetClass's interfaces => [interface org.eop.sb.example.service.IExampleService]
- ----------------------------------------------------
- // 带注解的方法调用, 有事务的开启和提交
- -> create Tx here in Proxy
- execute doNeedTx in ServiceImpl
- <- commit Tx here in Proxy
- -------
- // 没有注解的方法调用, 是没有事务的
- execute doNeedTx in ServiceImpl
- -------
经过测试后, 发现和我们推断的一模一样.
他: 你真是打破砂锅问到底, 把这个事情彻底弄明白了.
我: 对于没有实现接口的类, 只能使用 CGLIB 来生成代理.(开始套路), 假设有这样一个类, 它里面包含 public 方法, protected 方法, private 方法, package 方法, final 方法, static 方法, 我都给它们加上事务注解, 哪些方法会有事务呢?
他: 那我就现学现卖, 事务是由代理加进去的, 所以关键就是代理如何生成. 按照上面所说的代理应该具备的特点来看, 只能通过继承的方式生成一个子类来充当代理, 看起来就是这样的:
- class Target {
- @Transactional
- public void doNeedTx() {
- System.out.println("execute doNeedTx in Target");
- }
- //no annotation here
- public void doNotneedTx() {
- this.doNeedTx();
- }
- }
- class ProxyByCGLIB extends Target {
- private Target target;
- public ProxyByCGLIB(Target target) {
- this.target = target;
- }
- @Override
- public void doNeedTx() {
- System.out.println("-> create Tx in Proxy");
- target.doNeedTx();
- System.out.println("<- commit Tx in Proxy");
- }
- @Override
- public void doNotneedTx() {
- target.doNotneedTx();
- }
- }
而且, 必须在代理类里重写带注解方法以添加开启事务, 提交事务的代码. 从这个角度来说, private 方法不能被继承, final 方法不能被重写, static 方法和继承不相干, 所以它们 3 个的事务不起作用.
public 方法, protected 方法可以被重写以添加事务代码, 对于 package 方法来说, 如果生成的子类位于同一个包里, 就可以被重写以添加事务代码. 所以 public 方法事务肯定起作用, 剩下那 2 个就不确定了, 只能说它们有这个可能性.
我: 你分析的很好, CGLIB 确实是按照这种方式生成了子类作为代理, 而且和父类在同一个包下. 不过 Spring 选择让 protected 方法和 package 方法不支持事务, 所以只有 public 方法支持事务.
使用和上面一样的方法进行了测试, 结果如下:
- // 不是 JDK 动态代理
- isJdkDynamicProxy => false
- // 是 CGLIB 代理
- isCglibProxy => true
- // 生成的代理类的类型, 带 $$ 的
- proxyClass => class org.eop.sb.example.service.impl.ExampleServiceImpl$$EnhancerBySpringCGLIB$$5320b86e
- // 代理类的父类, 就是目标类
- parentClass => class org.eop.sb.example.service.impl.ExampleServiceImpl
- // 父类实现的接口, 就是我们自己写的接口
- parentClass's interfaces => [interface org.eop.sb.example.service.IExampleService]
- /** 代理类实现的接口, 并不包含目标类的接口 */
- proxyClass's interfaces => [interface org.springframework.aop.SpringProxy,
- interface org.springframework.aop.framework.Advised,
- interface org.springframework.cglib.proxy.Factory]
- // 代理对象
- proxy => org.eop.sb.example.service.impl.ExampleServiceImpl@1b2702b1
- // 目标对象
- target => org.eop.sb.example.service.impl.ExampleServiceImpl@1b2702b1
- // 代理对象和目标对象不是同一个
- proxy == target => false
- // 目标类, 我们自己写的类
- targetClass => class org.eop.sb.example.service.impl.ExampleServiceImpl
- // 目标类实现的接口
- targetClass's interfaces => [interface org.eop.sb.example.service.IExampleService]
由于采用的是相同的测试代码, 所以目标类是实现了接口的, 不过这并不影响使用 CGLIB 来生成代理. 可见, 代理类确实继承了目标类以保持和目标类的类型兼容, 对外接口相同. 注: 只要是以代理方式实现的声明式事务, 无论是 JDK 动态代理, 还是 CGLIB 直接写字节码生成代理, 都只有 public 方法上的事务注解才起作用. 而且必须在代理类外部调用才行, 如果直接在目标类里面调用, 事务照样不起作用. 他: 以前在网上也看到过有人说事务不生效的情况, 我想, 这个问题不会发生在我身上了.
后记
本文循序渐进地介绍了什么是代理, 代理具备的特征, 以及如何实现代理. 它可是声明式事务赖以存在的基石.
当然, 除此之外, Spring 事务还有很多其它方面的设计哲学和细节问题, 后续再进行解说, 也欢迎持续关注.
(END)
编程新说, 本号由工作 10 年的
架构师维护, 洞察技术本质,
生动幽默有趣, 欢迎关注!
来源: https://www.cnblogs.com/lixinjie/p/a-interver-about-spring-tx.html