【面试】我是如何在面试别人Spring事务时“套路”对方的
“中国最好面试官”
自从上次写了一篇“【面试】我是如何面试别人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年的
架构师维护,洞察技术本质,
生动幽默有趣,欢迎关注!

【面试】我是如何在面试别人Spring事务时“套路”对方的的更多相关文章
- 面试突击87:说一下 Spring 事务传播机制?
Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的. 既然是"事务传播",所以事务的数量应该在两个或两个以上,Spring 事务传播机制的 ...
- 【面试】Spring事务面试考点吐血整理(建议珍藏)
Spring和事务的关系 关系型数据库.某些消息队列等产品或中间件称为事务性资源,因为它们本身支持事务,也能够处理事务. Spring很显然不是事务性资源,但是它可以管理事务性资源,所以Spring和 ...
- Spring事务管理详解_基本原理_事务管理方式
1. 事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,那在没有Spring帮我们管理事 ...
- spring事务传播行为之使用REQUIRES_NEW不回滚
最近写spring事务时用到REQUIRES_NEW遇到一些不回滚的问题,所以就记录一下. 场景1:在一个服务层里面方法1和方法2都加上事务,其中方法二设置上propagation=Propagati ...
- 聊聊spring事务失效的12种场景,太坑了
前言 对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了. 在某些业务场景下,如果一个请求中,需要同时写入多张表的数据.为了保证操作的原子性(要么同时成功,要么同时失败),避免数据 ...
- 这12种场景Spring事务会失效!
前言 对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了.在某些业务场景下,如果一个请求中,需要同时写入多张表的数据.为了保证操作的原子性 (要么同时成功,要么同时失败),避免数据 ...
- (转)面试必备技能:JDK动态代理给Spring事务埋下的坑!
一.场景分析 最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务parent和事务child,在Controller里边同时调用这两个方法,示例代码如下: 1.场景A ...
- 【面试普通人VS高手系列】Spring中事务的传播行为有哪些?
一个工作了2年的粉丝,私信了一个比较简单的问题. 说: "Spring中事务的传播行为有哪些?" 他说他能记得一些,但是在项目中基本上不需要配置,所以一下就忘记了. 结果导致面试被 ...
- 【面试】足够应付面试的Spring事务源码阅读梳理(建议珍藏)
Starting from a joke 问:把大象放冰箱里,分几步? 答:三步啊,第一.把冰箱门打开,第二.把大象放进去,第三.把冰箱门带上. 问:实现Spring事务,分几步? 答:三步啊,第一. ...
随机推荐
- Python数据结构应用6——树
数据结构中的树的结点和机器学习中决策树的结点有一个很大的不同就是,数据结构中的树的每个叶结点都是独立的. 树的高度(Height)指叶结点的最大层树(不包含根结点) 一.树的建立 树可以这样定义:一棵 ...
- Flash与EEPROM
网上找的,感觉说的不错 FLASH 和EEPROM的最大区别是FLASH按扇区操作,EEPROM则按字节操作,二者寻址方法不同,存储单元的结构也不同,FLASH的电路结构较简单,同样容量占芯片面积较小 ...
- ACM——八大输出方式总结
个人做题总结,希望能够帮助到未来的学弟学妹们的学习! 永远爱你们的 ----新宝宝 1: 题目描述 Your task is to Calculate a + b. Too easy?! Of cou ...
- 搞定! iTunes 不能添加铃声进去
最近换个新铃声,但转换成.m4r 怎么都拖不到铃声里很莫名奇妙,首先确定苹果是允许自己定义铃声的,然后网上查了不少文章,都无济于事所以我想自己记录下自己成功搞定的方法,供各位参考第一步: 把自己想转为 ...
- 大白话5分钟带你走进人工智能-第十五节L1和L2正则几何解释和Ridge,Lasso,Elastic Net回归
第十五节L1和L2正则几何解释和Ridge,Lasso,Elastic Net回归 上一节中我们讲解了L1和L2正则的概念,知道了L1和L2都会使不重要的维度权重下降得多,重要的维度权重下降得少,引入 ...
- 基于JavaMail开发邮件发送器工具类
基于JavaMail开发邮件发送器工具类 在开发当中肯定会碰到利用Java调用邮件服务器的服务发送邮件的情况,比如账号激活.找回密码等功能.本人之前也碰到多次这样需求,为此特意将功能封装成一个简单易用 ...
- JavaScript使用闭包实现单例模式
闭包是JS的一种特性,其中一点就是:可以将外部函数的变量保存在内存中,利用这一特性,我们可以用来实现类的单例模式. 首先需要了解何为单例模式: 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问 ...
- Hadoop HA高可用集群搭建(Hadoop+Zookeeper+HBase)
声明:作者原创,转载注明出处. 作者:帅气陈吃苹果 一.服务器环境 主机名 IP 用户名 密码 安装目录 master188 192.168.29.188 hadoop hadoop /home/ha ...
- 搭建基于Docker社区版的Kubernetes本地集群
Kubernetes的本地集群搭建是一件颇费苦心的活,网上有各种参考资源,由于版本和容器的不断发展,搭建的方式也是各不相同,这里基于Docker CE的18.09.0版本,在Mac OS.Win10下 ...
- asp.net core 系列之webapi集成Dapper的简单操作教程
Dapper也是是一种ORM框架 这里记录下,使用ASP.NET 集成 Dapper 的过程,方便自己查看 至于Dapper的特性以及操作可以参考Dapper官方文档 1.创建数据库相关 在Sql S ...