真实案例测试你spring水平!

所有文章已迁移至csdn,csdn个人主页https://blog.csdn.net/chaitoudaren

前言

近两年网课上到处是《一天精通springboot》《2小时精通springboot》等课程,相信很多新手入门Java后端也都是从springboot开始的。2小时用搭建spring + mvc + mybatis后端常常让人有一种架构师的感觉,那么学完springboot到底还有必要学spring? 答案很负责任告诉你肯定是:有必要!简单、开箱即用等特性让springboot大受欢迎,但是请一定记住:一个框架越简单易用,说明框架在背后为开发人员做了越多复杂的工作。在笔者眼里springboot只不过是spring的一个整合,整合了tomcat、spring mvc一些常用组件,而本质依旧是spring

如果你还意识不到spring的重要性,以下有几个很基础的真实案例,如果发现不了案例问题的, 精通Spring 请再斟酌下要不要写入你的简历中

在这里插入图片描述

案例


难度★:单例(如: service, dao层)禁止使用非静态成员变量

使用误区

业务需求是需要导入10w张机票左右的Excel,PagService(单例)依赖ImportExcelListener(单例)进行Excel导入。使用到阿里的easyexcel,这里不讨论该框架,只需要知道每读取一行数据ImportExcelListener将调用invoke返回一行数据,读到末行将触发doAfterAllAnalysed。考虑到数据量较大,每读一行插入一次显然不可行,10w行插入一次也太大,于是小A同学经过评估后决定1000行插入一次

@Service
public class PagService extends BaseService<Pag> {
    @Autowired
    private ImportExcelListener importExcelListener;

    public void  improt(File file) {
        // 调用importExcelListener实现excel导入
        importExcelListener.read(file);
    }
}
@Component
public class ImportExcelListener extends AnalysisEventListener<Pag> {
    @Autowired
    private PagService pagService;
    
    // 用于保存1000张票
    List<Pag> list = new ArrayList<Pag>();

    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        // 每获取一张票将票加入list
        list.add(data);
        // 票数到达1000则批量保存
        if (list.size() >= 1000) {
            pagService.save(list);
            list.clear();
        }

    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 读到最后一行则批量保存
        pagService.save(list);
        list.clear();
    }
}

造成BUG

此时的Web项目是正常使用的,但是将引发一个严重的BUG,线程不安全!1. 营业点A与营业点B同时导入数据,A和B的数据将完全混在一起 2. 一旦程序出现异常,之后的Excel提交将永远抛出唯一索引冲突异常

造成原因

ImportExcelListener@Component设置成了单例,同时使用到了非静态成员变量List<Pag> list = new ArrayList<Pag>();这违反了单例的使用原则,造成线程不安全的问题。ImportExcelListener作为单例,也就是说在该Web项目中,将有且仅有一个实例。

  1. 当A、B营业处同时导入票证时,使用了同一个ImportExcelListener的同一个list,当A导入向list写入数据时B也同时在同一个list写入,因此导致A发现其导入的数据中杂糅着B的数据。
  2. 并且当导入数据发生异常抛出时,例如读取到501行数据出现异常,但是list中将包含前500行数据,且没有被清空。而第二次导入因为ImportExcelListener为单例,缓存在Ioc容器中,因此ImportExcelListener还是原来那个ImportExcelListenerlist也还是之前那个list,后续导入的数据又将1-500行加入list,导致永远导入都是唯一索引冲突

难度★★:加上@Scope("Prototype")就是多例(原型模式)了吗?

使用误区

小A在发现问题后,也知道了问题的原因出在单例上,于是想当然的将上@Scope("Prototype"),把ImportExcelListener变成多例(也就是原型模式),不就解决问题了吗?这样的做法真的能解决问题吗?

@Component
@Scope("Prototype")
public class ImportExcelListener extends AnalysisEventListener<Pag> {
    ...
}

造成BUG

小A改完代码提交后,业务人员反应BUG依然存在,并没有解决

造成原因

BUG依旧存在,那么说明线程不安全的问题依旧存在,这说明多例同样也是线程不安全的吗?显然不是,多例情况下每次请求使用的ImportExcelListener都是新的实例,不存在互相干扰的情况,也就没有所谓的线程安全问题可言。那么问题出在哪里?主要问题出在,即使小A加了@Scope("Prototype")但是在本项目中ImportExcelListener依旧是单例。

原因是PagService依赖ImportExcelListenerPagService是单例ImportExcelListener是多例。当spring创建PagService时,发现其依赖ImportExcelListener,而ImportExcelListener是多例,因此新创建出importExcelListener@001对象并且把地址给PagService,创建完成后,PagService将被缓存到spring Ioc容器中,下次需要PagService时则直接从缓存中取。因此,在创建完成后,A售票处与B售票处实际上调用的PagService是同一个实例,而导致其引用的importExcelListener@001也是同一个,也就造成了即使加上@Scope("Prototype")却还是同一个实例,因此线程不安全的BUG依旧存在。

这里贴一张单例setter的循环依赖流程图,a为PagService,b为ImportExcelListener。当PagService第一次创建时,将走完1-17步骤完成PagService的创建。但是第二次再需要ImportExcelListener时,将在步骤1. 尝试从各级缓存中获取bean就会直接返回缓存中的PagService,而不会再去管ImportExcelListener是不是多例是不是需要重新创建。详情请参考spring 循环依赖

单例setter循环依赖.jpg

难度★★★:那就让PagService也变成多例!

使用误区

小A同学发现错误后,最后发狠,那我就让他们全部都变成多例!这总可以了吧?且先不论小A同学并不知道所有的spring mvc的controller都是单例(注:Struts框架的Action则是多例,这也是跟spring mvc最大的区别),并解决不了问题。但是出发点是好的,让他们都是多例,这似乎解决了问题。代码如下

@Component
@Scope("Prototype")
public class PagService extends BaseService<Pag> {
    @Autowired
    private ImportExcelListener importExcelListener;

    ...
}
@Component
@Scope("Prototype")
public class ImportExcelListener extends AnalysisEventListener<Pag> {
    @Autowired
    private PagService pagService;

    ...
}

造成BUG

如果你也觉得上面的代码没有问题,那么我们来看下结果,项目直接连跑都跑不起来了

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'pagService': Unsatisfied dependency expressed through field 'importExcelListener'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'importExcelListener': Unsatisfied dependency expressed through field 'pagService'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'pagService': Requested bean is currently in creation: Is there an unresolvable circular reference?

    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:667)
...

造成原因

其实从异常上面也看出来了,PagServiceImportExcelListener循环依赖了。小A表示很疑惑,我第一次第二次的代码也是循环依赖啊,为什么第一次第二次就没问题?这次就有问题了?这里先给出结论,spring仅可以解决单例setter方式注入的循环依赖问题,对于原型模式和单例构造器注入模式都解决不了。

通俗一点来说,PagService为多例,也就是说每次获取都需要是一个新的实例。而当PagService创建时,发现需要依赖ImportExcelListener也是多例,因此又新生产了ImportExcelListener实例,此时ImportExcelListener又发现需要依赖PagService,同时PagService是多例,便不会去缓存中取,而是又新建一个多例。造成的结果就是循环依赖死循环,spring能做的就是帮你抛出异常...

spring循环依赖是面试最喜欢问的题目,如何检测循环依赖?spring能解决哪些循环依赖?总的可以归结成2点

  1. 必须提前曝光对象
  2. 曝光时机必须在实例化之后

缺一不可,关于循环依赖请参考spring 循环依赖


解决方案

就上述问题,归纳起来就是单例如何引用多例,使其在每次调用时都能保证使多例的问题。spring作为Java后端的元老,早就提供了解决方案,这里提供3种解决方案供参考:

  1. spring官方使用@Lookup或<lookup-method>标签,解决单例引用多例的问题
  2. 阿里官方推荐,这种方法最暴力,ImportExcelListener直接不使用spring管理,也就是不加@Component标签,让用户调用方法时,自己new一个。这种做法即完全脱离spring管理,用户自己负责实例的生命周期
@Service
public class PagService extends BaseService<Pag> {
    public void  improt(File file) {
    ImportExcelListener importExcelListener = new ImportExcelListener(this);
    // 调用importExcelListener实现excel导入
        importExcelListener.read(file);
    }
}
  1. 实现BeanFactoryAware,从BeanFactory中获取ImportExcelListener多例,以确保每次调用都新建一个。这种方案并不可取,他加大了代码的耦合程度,只是提供给大家另外一种思路。对spring Aware感知器不熟悉的可以参考spring 生命周期
@Service
public class PagService extends BaseService<Pag> implements BeanFactoryAware{
    private BeanFactory beanFactory;
  
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
  
    public void  improt(File file) {
    ImportExcelServic importExcelServic = (ImportExcelServic)beanFactory.getBean("importExcelServic", this);
    // 调用importExcelServic实现excel导入
        importExcelServic.read(file);
    }
}

总结

  1. spring单例绝对不能使用非静态成员变量(静态成员变量一个类只有一份,也就不存在线程安全问题)
  2. 单例依赖多例,想确保每次调用都使用全新的多例,需要使用@Lookup或lookup-method标签
  3. spring只能解决单例下set注入但是的循环依赖问题

笔者也使用springboot,但是精通springboot并不是指2小时学几个类似@Cacheable这样的标签就算精通了,如果是这样相信我在面试中你会被锤的很惨。我的建议是好好学习spring,如果有想进大厂的同学最好能够系统的学习一下spring 源码,对代码风格,设计思想都有很大帮助。毕竟大厂的面试官是不可能问出你会不会用@Cacheable、@Tranactional等标签的,而是想让你说出@Cacheable是如何通过AOP切面编程实现的、更甚是如何使用Jdk动态代理或者Cglib代理实现@Cacheable

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容