
暂时未有相关通用技术能力~
阿里云技能认证
详细说明分享、成长,拒绝浅藏辄止。关注公号【BAT的乌托邦】,回复专栏获取原创专栏:重学Spring、重学MyBatis、中间件、云计算...本文已被 https://www.yourbatman.cn 收录。 本文提纲 Tips:文末加了个奖励,阳光普照奖祛除2020阴霾,大步向前跨入2021 ✍前言 你好,我是A哥(YourBatman)。 2020年,庚子年,注定会在历史的长河里被深深记住。历史将怎样记载2020年?这是震慑之年,这是突破之年,也是转折之年。疫情之下,见证了人类的坚忍与脆弱、团结与分裂、担当与推诿,无知与无助。 每到年末,朋友圈、公众号、各大社区网站都会成为“成功人士”的show场,盘点自己这一年的成就、挣了多少钱、完成多少个小目标,为来年再立Flag......逛着逛着,一个不小心就被凡尔赛(灬ꈍ ꈍ灬) 2020眨眼就快结束了,你的年终复盘写好了吗?复盘的重要性到底是什么?你是否总在表表面面的,像记流水账一样的回顾自己过去做过的事、踩过的坑、取得的成绩敷衍了事,明年又重复一样的过程?而复盘真正的意义是让我们更了解自己,自己的短板在哪?该怎么补?通过自己学习还是通过整合资源互补,自己的价值在哪?如何放大,怎样被更多的人看到?实现自我成长的同时影响更多人~ 按照我自行约定的惯例,每年年末定心回首这一年,然后在最后一天留下些笔墨。可能不同于晒成绩show自己的那种文章,我更多是倾向于记录下自己的感悟,与你分享,共勉。 版本约定 年份:2020 ✍正文 作为一个IT从业人员,我的视角自然和本圈分不开,所以可能狭隘,可能有偏见,可能不深刻,也可能有错误“”。若你有不同感悟,强烈建(欢)议(迎)你在文末留下你的文字,组团思考。 说明:本文感悟来自于自己以及结合了几位职场老兵一起畅聊的总结 科比 科比事件,堪称是2020年给我触动最大,感触最深的一次事件。 美国当地时间2020年1月26号上午10点(北京时间:2020年1月27号凌晨2点)左右,NBA传奇巨星科比·布莱恩特于加利福尼亚州洛杉矶县卡拉巴萨斯的一场直升机坠机事故中身亡,年仅41岁。 那天是中国农历春节的大年初三,早上6点多我就朦朦胧的醒来,像平常一样睁眼第一件事便是拿起手机,关注关注疫情动态。可没想到,满屏的手机通知(标题大致都是:科比疑似坠机身亡。)让我元神瞬间归位。 从看见通知,到确认通知,我的心里状态起起伏伏,大致是这样变化的: 那会不愿去相信这是真事,从国内的新闻频道、朋友圈、微博,到搭梯子去外网求证,发现此新闻霸占了美国各大媒体头条:科比坠机事件坐实了。 所以这个纪念性的朋友圈也就有了,逝去的青春: 一时间,科比坠机事件瞬间席卷整个网络、微博、朋友圈。眼瞅2020年就快结束了,事情过去也将近一年。百度在12月中旬发布了2020年百度沸点,即使在如此不平凡的一年里,科比二字在年度关键词榜单中“高居”前10,年度泪点榜单中更是“高居”第三,影响力可见一斑: 天妒英才,可谁知那一句“Manba out”在2020年1月27日成为了永恒。 故人已逝,但科比的那份执念,那份曼巴精神永存,奉他为斯台普斯永远的王。关于老大流传的佳话、佳句、故事、遗憾实在太多太多,为了节约篇幅我在这就不做重复的搜集工作了。我相信广大读者不乏也有老大的死忠粉,期待留言区见。 综合全网,我个人有且仅向你推荐这一个小视频,3分钟时长剪辑版。不管你懂不懂篮球,都呼吁你安静的把它看完:2013年常规赛最后阶段,科比为了兑现把湖人送进季后赛的承诺,使尽全力,最终拼到脚跟建断裂,职业生涯就此遭遇滑铁卢,那个不服输精神,不甘心的眼神,令人动容。 答应我 安静的看完科比跟腱断裂一战答应我 安静的看完科比跟腱断裂一战答应我 安静的看完科比跟腱断裂一战 抖音观看(推荐此观看方式) PC观看:https://www.xiaohongshu.com/discovery/item/5fc0adbf0000000001003117 裁员 画风一转,咱们继续聊聊职场那些事。 犹记得2019年下半年,中国整个互联网圈一片“哀鸿遍野”的景象:小到初创公司,大到互联网寡头几乎无一幸免的进行了裁员,更有甚者“暴力裁员”。那会的空气中弥漫的裁员气氛不仅让很多人中枪,更是让那些“幸免者”也忧郁匆匆,人心惶惶,不可为不“热闹”。 本以为2020年会有所好转,可谁知新冠疫情这个黑天鹅事件爆发,更是冰上加霜。“裁员年年有,今年特别多”,真是不知道意外和裁员,哪个会先到来。没有什么是亘古不变的,你喜欢岁月静好,现实却波涛汹涌! 也许此时,对于企业,2020年甚至成为了生死之年;对于个人,职场中的人也都需要认真思考这个问题:我会被裁吗? 如何避免被裁? 首先需要明确,裁员是企业断臂求生的最有效手段,在经济下滑的趋势下不可避免。 在企业上班的多数人都背着房贷、车贷、教育、养老等压力,倘若被裁员,恰恰是把人推向了更加艰难窘迫的境地。即使没有被裁员,面对将下来越来越严峻的形势,也非常令人担忧和不安。虽然暂时安稳,但这并不是长久的,毕竟你的饭碗不是铁铸造。 公司裁员的对象,大体上遵循这个规律:裁掉“性价比”低的员工。那么,什么是性价比? 在企业里,你产生的价值、你的人脉圈、你的不可替代性决定了你的“性”(分子,也称价值),你的工资薪酬、你的职位层级、你的权利决定了你的“价”(分母,也称成本)。想要提高自己的性价比有这两条路可走: 减小分母:降薪、降级、降权利 加大分子:多付出、扩大圈子、提升自身综合能力 虽说有两条路子可选,但很明显减小分母这条路具有不可实操性。你选择降薪?还是选择降级?还是会自愿放弃权利呢?成年人哪会做这种选择题,肯定一个都不会选的嘛。 可更为残酷的现实是,被裁员的大都是“底层员工”,是无职级、无权利、无降薪空间的“三无员工”,分母连减小的空间都不存在。所以说减小分母这条路理论上可行,但不具备实操性,充其量当做退无可退的无奈之举。 别无选择,想屹立于职场只能pick第二条路:加大分子。如果说分母的改变具有一定被动性,那么分子的变化一切都由你主观决定。你可以选择为公司多付出(如996、10107),你也可努力成为公司那个不可或缺人(如掌握核心技术专利)。总之你的“命脉”掌握在你自己手上,多一份付出、多一点思考、多一些不可替代性都是可努力的方向。 既然加大分子这条路子能通过主观能动性来解决,那么下面就来唠几句,有哪些理论可以参考学习,甚至当作行为规范来用。 1、不要迷恋管理,一味追求“当官” 中国人自古以来都有“官本位”的思想:在职场上混迹几年,如果不混个一官半职,就感觉挺丢人(无言面对父老乡亲)。有这种想法是完全可以被理解,甚至被普遍接受的,衣锦还乡说的就是这个情况。但绝大多数人对此都少了一个概念上的区别:事业单位or企业单位,体制内or体制外: 事业单位:只要不违法乱纪,不犯大的错误,乌纱帽这辈子就稳了 企业单位(非国企):失去职位的“机会”太多了。且不说公司内部的激烈斗争这方面,就算你总是能不湿鞋且八面玲珑的能应对各种关系,但是,你也控制不了你所在的团队、事业部、或者是行业的衰退。比如公司战略调整裁掉整个事业部,你不能说是因为你领导架构设计得不好;比如二手车行业不景气了,你不能说是因为你总监的能力不行 迷恋管理本身没有任何错,毕竟下命令的快感是顺应人性的每个人期望得到。但是身处职场的我们,应该看清自己所处的单位性质、环境,然后控制好这个度,才能能游刃有余。 2、别以为裁员只裁一线,不裁管理层 从裁员的新闻中总结出规律,各公司裁员的目标人群基本保持一致:主要裁基层员工、基层管理、中层管理。为何裁的是这三类职位呢?其实这蛮好理解,这三类职位一般占据公司80%的人员甚至更多,站在企业的角度来看在这里“动手术”是最见效且是最安全的(人数多就不存在单点风险)。 所以误以为裁员只裁一线员工,不裁领导(管理层)是非常错误的观点,毕竟员工都没了,要“领导”又有何用呢?难道让1个领导管1名员工,甚至光杆司令?这明显违背了公司的“金字塔结构”嘛。 3、即使步入管理,建议不要脱离技术 不乏有很多人把不用写代码当作步入管理层的一个标志,其实这个是有很大误导性的。 舞蹈领域流传这么一句话:一天不练,自己知道!两天不练,同行知道!三天不练,观众知道!程序员又何尝不是呢,本行业内也盛传这么一句话:脱离技术,最多只用3年。而一旦脱离,就像断了头,捡起来的难度非常之大。 因此,即使已经步入管理,也不要把这条路走成一条断头路,尤其是在一日千里的IT领域,唯一不变的就是变化,甚至瞬息万变,所以埋头管理的同时,也得经常抬头看路(埋头写代码的同时,也别忘了抬头望望O(∩_∩)O哈哈~)。 4、平台高 ≠ 能力强 把时钟往前拨5年,从BAT跳出来面试的人是自带光环的,面试官自然而然的会高看你一眼,甚至职级评定、薪资上也是给得非常慷慨。就因为如此,那些年出现了非常多的先去大公司“镀金”的现象。也正因为如此,伴随着中国互联网的快速发展,这种镀过金的人越来越多,市场上鱼龙混杂,然后浑水摸鱼的现象级越来越常见了。 我猜测:你现在或者曾经肯定吐槽过身边的同事前BAT员工,然后“我靠”一句:这也太菜了吧 那企业招聘怎么办???很简单:一刀切呗(talk is cheap, show me the code)。时代在变迁,镀金思维几年前好用,现在已经过时了。需要保持时俱进,随时纠正自己。只有自己真的是铁,才不怕面试的三位真火,保持自己的核心竞争力才是屹立于市场之王道。能者上,平者让,庸者下,这是亘古不变的道理。 5、跳出舒适区,居安思危 在企业里混,在职场上混,永远没有谁是真正安全的,无论是做技术,还是做管理。舒适区呆着固然舒服,但很容易就被温水煮青蛙,一煮就煮到你35岁,慌不慌? 人生已经这么艰难了,没想到越来越难。本着居安思危,如履薄冰的态度,才能让自己一直保持可靠的竞争力。常出来看看,跳出舒适区,挑战些之外的东西,保持激情和热泪盈眶。 说明:出来看看并不是指的跳槽,也可以是和牛人交流、参与公开项目、做公开演讲、直播等 6、技术转产品,也请确保技术扎实 坊间传闻,寡头企业阿里巴巴、腾讯等现在招聘产品经理,倾向于从技术流里面转的这种。阿里腾讯作为中国互联网企业的翘楚和领军,不客气的说它们的某些行为就是标准,就是规范。通过观念的慢慢渗透,其它中小企业会慢慢效仿,这或许就是将来的一种趋势~ 技术转产品并不新鲜,甚至是一个很有“前景”的方向。毕竟靠技术没法“改变”世界,但产品可以。比如大家都非常熟悉的业界名人(国内):张小龙、雷军、周鸿祎、马化腾......他们均为技术出生转向产品从而“改变”世界。在IT领域里,产品经理是个很容易被羡慕的工种:门槛低、工资高;压力小,不背锅;有假期,加班少......这些标签被贴上,广大程序员看后还不哗啦啦的流口水麽?可世间哪有那么好的事,远远没有空想的那么简单,毕竟简单的事做起来一般都不具价值的,做难事才有所得。 也许表面上看,从“幸福生活”的角度去这么思考无可厚非,产品经理的职位很容易就让旁人心生羡慕。但永远相信:人的选择可能会错,但社会的选择永远不会有误,适者生存。技术人转产品岗其实是一个比较好的归宿,可以间接绕过35岁魔咒,从此对年龄不再那么敏感和恐惧。但是,在纯技术和纯产品都受到了前所未有的挑战的现在,倘若真要转产品路线,也请做一个有扎实技术基础的产品经理,而不是仅仅为了“逃”到舒适区,否则很容易就被温水煮青蛙甚至直接淘汰。 管理层 2020年,中国的大多数企业都有一个共同感受:增长乏力。其实这个现象在去年就已经比较明显了,其核心原因可能有二: 中国网民数量(9.4亿)已经饱和,流量红利期已彻底结束 既然再无增量市场,那就只能深挖存量市场,但这条路上布满荆棘 增长是企业经营情况的晴雨表,增长快证明公司向好,有些问题也就不是问题。增长放缓伴随着的一般都会是带来变革,涉及到最重要的就是公司的领导班子,他们的战略制定、规则调整时时刻刻都和公司命运相关联。 随着时间推移和经验的积累,我们职业生涯中或许很多人会步入到管理层岗位。但是在增长乏力、大环境向下的时局下,对管理层这个岗位我们可能需要有更多的思考、更深刻的认知。手握“宝典”,方能尽可能的保全自己。 更大危机感 对于基层员工来说,基本不与公司形成命运共同体,大不了换个坑就行了。但对于管理岗位来说,有必要建立起更大的危机感:对绝大多数中级管理者来说,无论是个人的价值,还是地位,都严重依赖于当前所在的组织/公司,以及赋予的title。一旦离开这个公司,失去了光鲜的“外表”和对应的title后,这些管理者就会瞬间失去所有光环,“裸露”在水面,大潮退去,就能知道到底谁在裸泳。 管理者一般相对“务虚”,所依赖的、擅长的往往是大家津津乐道的“软实力”,如沟通、协调、推动、PPT工程师、画图工程师......这些能力里面其实本身都没有问题,问题就在于这个“软”字,因为它不好展现,尤其你在面试的时候,所以很难结果导向的评估面试结果,很是吃亏。 当然喽,你可能会说,管理层咋可能还现场这样面试?都是走的人脉圈子,那另当别论 另外,对于管理岗来讲,招聘的需求一般是较少的,运气好的话还行可以很快遇到伯乐扶一把。若运气不佳,中层管理者一旦离开当前公司,市场价值很可能会出现断崖式下跌。反观技术人员,只要社会还在发展,企业对生产力还有需求,那么专业技术人员就不会彻底失业,再就业的概率也是很大、很快的。 在失去工作岗位后管理层和普通员工的区别也是蛮大的:管理层往往比普通员工更加悲惨,因为在大环境向下的时候,脱了毛的凤凰还不如鸡。况且由俭入奢易,由奢入俭难的思维必然亦会阻碍领导层再就业时的选择。 更深刻行业认知 能步入管理层,肯定是你身上那些优秀品质得以发挥效用。对于管理层而言,深刻的行业认知必不可少,最好能一针见血。下面举几个例子 1、没有技术驱动型公司 世界上没有技术驱动型公司。 哪怕强如google,微软,亦或是国内的阿里、腾讯,都不是技术驱动型公司。道理很简单:技术不是源头,需求才是。而技术只是为了更好的去服务需求的一种工具而已~ 一切技术问题,都要服从产品交付和市场反馈,所以客观的说,任何公司都不可能以技术去驱动自身。人可以用技术驱动自身,但公司不行。 不过,在市场经济下,以技术为导向的组织还有一个神秘的地方:研究院。它的特点是:不需要自己挣钱,所以没有生存压力,国家用纳税人的钱养着就成。 2、最重要的是经验,而不是聪明 作为技术从业者,有两个概念需要加以区分:IT领域 vs 科研领域。本文所讨论的范畴,那必然是IT领域。 IT领域的平均薪资经常性霸榜,再加上计算机经常被辅以“神秘”、“高大上”的标签,所以有种表面光鲜感觉。你大概率也许可能被老家的大叔大妈这样夸过:做IT做计算机的人都好聪明,脑子好使,巴拉巴拉...... 这个时候你可千万记住了,保持清醒,别被这种“幻觉”带跑偏,迷之自信了,否则这可能对你就是一个捧杀。 之前看到过一句非常经典的话在这分享给你:你做得好不好,不取决于你是否聪明,而取决于你是否愿意不断读书不断学习和不断积累。经常给自己泼泼冷水,能让自己更加清醒。 真正聪明的人大都去搞科研去了,做IT的自嘲其实就是民工一枚嘛 3、技术栈一旦确定,就改不了 IT是工科,不是理科,和IT行业相似度最高的行业是盖楼房,相似度惊人。写代码和盖楼是一样的,一旦地基框架都打好了,只有可能重新建一栋而不可能去重构它。 此小标题本来有几乎二字(就几乎改不了),但我决定话说狠一点就给拿掉了,任性 4、学会一门语言,其它语言都差不多 这是错误认知。这个观点其实是一个看起来正确,实则错误的一个认知。 我觉得,产生这种认知的大都出现在工作还只有几年的年轻从业者身上,这种选手的特点一般是自负、迷之自信,也就是对自我认知还不太够,并不太能深刻了解“你知道的越多,不知道的更多”这句话的深层含义。 也许这是一个技术广度 vs 技术深度的较量。其实每一个技术栈的size都太深了,你几乎不太可能去精通多个。正所谓基础不牢,地动山摇,所以选型确定根基很重要。业界有个极端的二八原则:花80%的时间做代码设计、画UML图、画时序图,20%的时间写code和debug~ 别妄想着去追新一会搞这一会搞那,对于大多数人来说能把一门语言搞得比较通透就已经很难了,毕竟我们并不“聪明”。 活着 我是湖北人,我喜欢科比,我喜欢梅西(link:马拉多纳)...... 湖北通山县,援鄂医疗队是云南省第二人民医院和大理大学第一附属医院的白衣天使们,特别特别感谢援助 2020年,对这两个字有感,没有哪一年比今年更对“活着”有着深刻理解。除了生死,都是小事;过往纠葛,都是故事。金钱,在健康面前,不值一提,名利,在平安面前,啥都不是。我们不知道,明天和意外究竟谁先来,好好珍惜眼前,善待身边挚爱,余生不长,活着最重要。 ✍总结 今年是近十年最差的一年,却是将来十年最好的一年。与其忧心忡忡,惴惴不安,倒不如多多思考、多多学习。 最后想说一句:滚蛋吧,2020。虽然这句话说得毫不客气,但who care?毕竟2020对也没对咱客气过,不是麽!
分享、成长,拒绝浅藏辄止。关注公号【BAT的乌托邦】,回复专栏获取原创专栏:重学Spring、重学MyBatis、中间件、云计算...本文已被 https://www.yourbatman.cn 收录。 ✍前言 你好,我是A哥(YourBatman)。本文所属专栏:Spring类型转换,公号后台回复专栏名即可获取全部内容。 在日常开发中,我们经常会有格式化的需求,如日期格式化、数字格式化、钱币格式化等等。 格式化器的作用似乎跟转换器的作用类似,但是它们的关注点却不一样: 转换器:将类型S转换为类型T,关注的是类型而非格式 格式化器: String <-> Java类型。这么一看它似乎和PropertyEditor类似,但是它的关注点是字符串的格式 Spring有自己的格式化器抽象org.springframework.format.Formatter,但是谈到格式化器,必然就会联想起来JDK自己的java.text.Format体系。为后文做好铺垫,本文就先介绍下JDK为我们提供了哪些格式化能力。 版本约定 JDK:8 ✍正文 Java里从来都缺少不了字符串拼接的活,JDK也提供了多种“工具”供我们使用,如:StringBuffer、StringBuilder以及最直接的+号,相信这些大家都有用过。但这都不是本文的内容,本文将讲解格式化器,给你提供一个新的思路来拼接字符串,并且是推荐方案。 JDK内置有格式化器,便是java.text.Format体系。它是个抽象类,提供了两个抽象方法: public abstract class Format implements Serializable, Cloneable { public abstract StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos); public abstract Object parseObject (String source, ParsePosition pos); } format:将Object格式化为String,并将此String放到toAppendTo里面 parseObject:讲String转换为Object,是format方法的逆向操作 Java SE针对于Format抽象类对于常见的应用场景分别提供了三个子类实现: DateFormat:日期时间格式化 抽象类。用于用于格式化日期/时间类型java.util.Date。虽然是抽象类,但它提供了几个静态方法用于获取它的实例: // 格式化日期 + 时间 public final static DateFormat getInstance() { return getDateTimeInstance(SHORT, SHORT); } public final static DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale){ return get(timeStyle, dateStyle, 3, aLocale); } // 格式化日期 public final static DateFormat getDateInstance(int style, Locale aLocale) { return get(0, style, 2, aLocale); } // 格式化时间 public final static DateFormat getTimeInstance(int style, Locale aLocale){ return get(style, 0, 1, aLocale); } 有了这些静态方法,你可在不必关心具体实现的情况下直接使用: /** * {@link DateFormat} */ @Test public void test1() { Date curr = new Date(); // 格式化日期 + 时间 System.out.println(DateFormat.getInstance().getClass() + "-->" + DateFormat.getInstance().format(curr)); System.out.println(DateFormat.getDateTimeInstance().getClass() + "-->" + DateFormat.getDateTimeInstance().format(curr)); // 格式化日期 System.out.println(DateFormat.getDateInstance().getClass() + "-->" + DateFormat.getDateInstance().format(curr)); // 格式化时间 System.out.println(DateFormat.getTimeInstance().getClass() + "-->" + DateFormat.getTimeInstance().format(curr)); } 运行程序,输出: class java.text.SimpleDateFormat-->20-12-25 上午7:19 class java.text.SimpleDateFormat-->2020-12-25 7:19:30 class java.text.SimpleDateFormat-->2020-12-25 class java.text.SimpleDateFormat-->7:19:30 嗯,可以看到底层实现其实是咱们熟悉的SimpleDateFormat。实话说,这种做法不常用,狠一点:基本不会用(框架开发者可能会用做兜底实现)。 SimpleDateFormat 一般来说,我们会直接使用SimpleDateFormat来对Date进行格式化,它可以自己指定Pattern,个性化十足。如: @Test public void test2() { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); // yyyy-MM-dd HH:mm:ss System.out.println(dateFormat.format(new Date())); } 运行程序,输出: 2020-12-25 关于SimpleDateFormat的使用方式不再啰嗦,不会的就可走自行劝退手续了。此处只提醒一点:SimpleDateFormat线程不安全。 说明:JDK 8以后不再建议使用Date类型,也就不会再使用到DateFormat。同时我个人建议:在项目中可强制严令禁用 NumberFormat:数字格式化 抽象类。用于格式化数字,它可以对数字进行任意格式化,如小数、百分数、十进制数等等。它有两个实现类: 类结构和DateFormat类似,也提供了getXXXInstance静态方法给你直接使用,无需关心底层实现: @Test public void test41() { double myNum = 1220.0455; System.out.println(NumberFormat.getInstance().getClass() + "-->" + NumberFormat.getInstance().format(myNum)); System.out.println(NumberFormat.getCurrencyInstance().getClass() + "-->" + NumberFormat.getCurrencyInstance().format(myNum)); System.out.println(NumberFormat.getIntegerInstance().getClass() + "-->" + NumberFormat.getIntegerInstance().format(myNum)); System.out.println(NumberFormat.getNumberInstance().getClass() + "-->" + NumberFormat.getNumberInstance().format(myNum)); System.out.println(NumberFormat.getPercentInstance().getClass() + "-->" + NumberFormat.getPercentInstance().format(myNum)); } 运行程序,输出: class java.text.DecimalFormat-->1,220.045 class java.text.DecimalFormat-->¥1,220.05 class java.text.DecimalFormat-->1,220 class java.text.DecimalFormat-->1,220.045 class java.text.DecimalFormat-->122,005% 这一看就知道DecimalFormat是NumberFormat的主力了。 DecimalFormat Decimal:小数,小数的,十进位的。 用于格式化十进制数字。它具有各种特性,可以解析和格式化数字,包括:西方数字、阿拉伯数字和印度数字。它还支持不同种类的数字,包括:整数(123)、小数(123.4)、科学记数法(1.23E4)、百分数(12%)和货币金额($123)。所有这些都可以进行本地化。 下面是它的构造器: 其中最为重要的就是这个pattern(不带参数的构造器一般不会用),它表示格式化的模式/模版。一般来说我们对DateFormat的pattern比较熟悉,但对数字格式化的模版符号了解甚少。这里我就帮你整理出这个表格(信息源自JDK官网),记得搜藏哦: 符号 Localtion 是否本地化 释义 0 Number 是 Digit # Number 是 Digit。若是0就显示为空 . Number 是 小数/货币分隔符 - Number 是 就代表减号 , Number 是 分组分隔符 E Number 是 科学计数法分隔符(位数和指数) % 前/后缀 是 乘以100并显示为百分数 ¤ 前/后缀 否 货币记号。若连续出现两次就用国际货币符号代替 ' 前后缀 否 用于引用特殊字符。作用类似于转义字符 说明:Number和Digit的区别: Number是个抽象概念,其表达形式可以是数字、手势、声音等等。如1024就是个number Digit是用来表达的单独符号。如0-9这是个digit就可以用来表示number,如1024就是由1、0、2、4这四个digit组成的 看了这个表格的符号规则,估计很多同学还是一脸懵逼。不啰嗦了,上干货 一、0和#的使用(最常见使用场景) 这是最经典、最常见的使用场景,甚至来说你有可能职业生涯只会用到此场景。 /** * {@link DecimalFormat} */ @Test public void test4() { double myNum = 1220.0455; System.out.println("===============0的使用==============="); System.out.println("只保留整数部分:" + new DecimalFormat("0").format(myNum)); System.out.println("保留3位小数:" + new DecimalFormat("0.000").format(myNum)); System.out.println("整数部分、小数部分都5位。不够的都用0补位(整数高位部,小数低位补):" + new DecimalFormat("00000.00000").format(myNum)); System.out.println("===============#的使用==============="); System.out.println("只保留整数部分:" + new DecimalFormat("#").format(myNum)); System.out.println("保留2为小数并以百分比输出:" + new DecimalFormat("#.##%").format(myNum)); // 非标准数字(不建议这么用) System.out.println("===============非标准数字的使用==============="); System.out.println(new DecimalFormat("666").format(myNum)); System.out.println(new DecimalFormat(".6666").format(myNum)); } 运行程序,输出: ===============0的使用=============== 只保留整数部分:1220 保留3位小数:1220.045 整数部分、小数部分都5位。不够的都用0补位(整数高位部,小数低位补):01220.04550 ===============#的使用=============== 只保留整数部分:1220 保留2为小数并以百分比输出:122004.55% ===============非标准数字的使用=============== 661220 1220.666 通过此案例,大致可得出如下结论: 整数部分: 0和#都可用于取出全部整数部分 0的个数决定整数部分长度,不够高位补0;#则无此约束,N多个#是一样的效果 小数部分: 可保留小数点后N位(0和#效果一样) 若小数点后位数不够,若使用的0那就低位补0,若使用#就不补(该是几位就是几位) 数字(1-9):并不建议模版里直接写1-9这样的数字,了解下即可 二、科学计数法E 如果你不是在证券/银行行业,这个大概率是用不着的(即使在,你估计也不会用它)。来几个例子感受一把就成: @Test public void test5() { double myNum = 1220.0455; System.out.println(new DecimalFormat("0E0").format(myNum)); System.out.println(new DecimalFormat("0E00").format(myNum)); System.out.println(new DecimalFormat("00000E00000").format(myNum)); System.out.println(new DecimalFormat("#E0").format(myNum)); System.out.println(new DecimalFormat("#E00").format(myNum)); System.out.println(new DecimalFormat("#####E00000").format(myNum)); } 运行程序,输出: 1E3 1E03 12200E-00001 .1E4 .1E04 1220E00000 三、分组分隔符, 分组分隔符比较常用,它就是我们常看到的逗号, @Test public void test6() { double myNum = 1220.0455; System.out.println(new DecimalFormat(",###").format(myNum)); System.out.println(new DecimalFormat(",##").format(myNum)); System.out.println(new DecimalFormat(",##").format(123456789)); // 分隔符,左边是无效的 System.out.println(new DecimalFormat("###,##").format(myNum)); } 运行程序,输出: 1,220 12,20 1,23,45,67,89 12,20 四、百分号% 在展示层面也比较常用,用于把一个数字用%形式表示出来。 @Test public void test42() { double myNum = 1220.0455; System.out.println("百分位表示:" + new DecimalFormat("#.##%").format(myNum)); System.out.println("千分位表示:" + new DecimalFormat("#.##\u2030").format(myNum)); } 运行程序,输出: 百分位表示:122004.55% 千分位表示:1220045.5‰ 五、本地货币符号¤ 嗯,这个符号¤,键盘竟无法直接输出,得使用软键盘(建议使用copy大法)。 @Test public void test7() { double myNum = 1220.0455; System.out.println(new DecimalFormat(",000.00¤").format(myNum)); System.out.println(new DecimalFormat(",000.¤00").format(myNum)); System.out.println(new DecimalFormat("¤,000.00").format(myNum)); System.out.println(new DecimalFormat("¤,000.¤00").format(myNum)); // 世界货币表达形式 System.out.println(new DecimalFormat(",000.00¤¤").format(myNum)); } 运行程序,输出: 1,220.05¥ 1,220.05¥ ¥1,220.05 1,220.05¥¥ ¥1,220.05¥ 1,220.05CNY 注意最后一条结果:如果连续出现两次,代表货币符号的国际代号。 说明:结果默认都做了Locale本地化处理的,若你在其它国家就不会再是¥人名币符号喽 DecimalFormat就先介绍到这了,其实掌握了它就基本等于掌握了NumberFormat。接下来再简要看看它另外一个“儿子”:ChoiceFormat。 ChoiceFormat Choice:精选的,仔细推敲的。 这个格式化器非常有意思:相当于以数字为键,字符串为值的键值对。使用一组double类型的数组作为键,一组String类型的数组作为值,两数组相同(不一定必须是相同,见示例)索引值的元素作为一对。 @Test public void test8() { double[] limits = {1, 2, 3, 4, 5, 6, 7}; String[] formats = {"周一", "周二", "周三", "周四", "周五", "周六", "周天"}; NumberFormat numberFormat = new ChoiceFormat(limits, formats); System.out.println(numberFormat.format(1)); System.out.println(numberFormat.format(4.3)); System.out.println(numberFormat.format(5.8)); System.out.println(numberFormat.format(9.1)); System.out.println(numberFormat.format(11)); } 运行程序,输出: 周一 周四 周五 周天 周天 结果解释: 4.3位于4和5之间,取值4;5.8位于5和6之间,取值5 9.1和11均超过了数组最大值(或者说找不到匹配的),则取值最后一对键值对。 可能你会想这有什么使用场景???是的,不得不承认它的使用场景较少,本文下面会介绍下它和MessageFormat的一个使用场景。 如果说DateFormat和NumberFormat都用没什么花样,主要记住它的pattern语法格式就成,那么就下来这个格式化器就是本文的主菜了,使用场景非常的广泛,它就是MessageFormat。 MessageFormat:字符串格式化 MessageFormat提供了一种与语言无关(不管你在中国还是其它国家,效果一样)的方式生成拼接消息/拼接字符串的方法。使用它来构造显示给最终用户的消息。MessageFormat接受一组对象,对它们进行格式化,然后在模式的适当位置插入格式化的字符串。 先来个最简单的使用示例体验一把: /** * {@link MessageFormat} */ @Test public void test9() { String sourceStrPattern = "Hello {0},my name is {1}"; Object[] args = new Object[]{"girl", "YourBatman"}; String formatedStr = MessageFormat.format(sourceStrPattern, args); System.out.println(formatedStr); } 运行程序,输出: Hello girl,my name is YourBatman 有没有中似曾相似的感觉,是不是和String.format()的作用特别像?是的,它俩的用法区别,到底使用税文下也会讨论。 要熟悉MessageFormat的使用,主要是要熟悉它的参数模式(你也可以理解为pattern)。 参数模式 MessageFormat采用{}来标记需要被替换/插入的部分,其中{}里面的参数结构具有一定模式: ArgumentIndex[,FormatType[,FormatStyle]] ArgumentIndex:非必须。从0开始的索引值 FormatType:非必须。使用不同的java.text.Format实现类对入参进行格式化处理。它能有如下值: number:调用NumberFormat进行格式化 date:调用DateFormat进行格式化 time:调用DateFormat进行格式化 choice:调用ChoiceFormat进行格式化 FormatStyle:非必须。设置FormatType使用的样式。它能有如下值: short、medium、long、full、integer、currency、percent、SubformPattern(如日期格式、数字格式#.##等) 说明:FormatType和FormatStyle只有在传入值为日期时间、数字、百分比等类型时才有可能需要设置,使用得并不多。毕竟:我在外部格式化好后再放进去不香吗? @Test public void test10() { MessageFormat messageFormat = new MessageFormat("Hello, my name is {0}. I’am {1,number,#.##} years old. Today is {2,date,yyyy-MM-dd HH:mm:ss}"); // 亦可通过编程式 显示指定某个位置要使用的格式化器 // messageFormat.setFormatByArgumentIndex(1, new DecimalFormat("#.###")); System.out.println(messageFormat.format(new Object[]{"YourBatman", 24.123456, new Date()})); } 运行程序,输出: Hello, my name is YourBatman. I’am 24.12 years old. Today is 2020-12-26 15:24:28 它既可以直接在模版里指定格式化模式类型,也可以通过API方法set指定格式化器,当然你也可以再外部格式化好后再放进去,三种方式均可,任君选择。 注意事项 下面基于此示例,对MessageFormat的使用注意事项作出几点强调。 @Test public void test11() { System.out.println(MessageFormat.format("{1} - {1}", new Object[]{1})); // {1} - {1} System.out.println(MessageFormat.format("{0} - {1}", new Object[]{1})); // 输出:1 - {1} System.out.println(MessageFormat.format("{0} - {1}", new Object[]{1, 2, 3})); // 输出:1 - 2 System.out.println("---------------------------------"); System.out.println(MessageFormat.format("'{0} - {1}", new Object[]{1, 2})); // 输出:{0} - {1} System.out.println(MessageFormat.format("''{0} - {1}", new Object[]{1, 2})); // 输出:'1 - 2 System.out.println(MessageFormat.format("'{0}' - {1}", new Object[]{1, 2})); // {0} - 2 // 若你数据库值两边都需要''包起来,请你这么写 System.out.println(MessageFormat.format("''{0}'' - {1}", new Object[]{1, 2})); // '1' - 2 System.out.println("---------------------------------"); System.out.println(MessageFormat.format("0} - {1}", new Object[]{1, 2})); // 0} - 2 System.out.println(MessageFormat.format("{0 - {1}", new Object[]{1, 2})); // java.lang.IllegalArgumentException: Unmatched braces in the pattern. } 参数模式的索引值必须从0开始,否则所有索引值无效 实际传入的参数个数可以和索引个数不匹配,不报错(能匹配上几个算几个) 两个单引号''才算作一个',若只写一个将被忽略甚至影响整个表达式 谨慎使用单引号' 关注'的匹配关系 {}只写左边报错,只写右边正常输出(注意参数的对应关系) static方法的性能问题 我们知道MessageFormat提供有一个static静态方法,非常方便的的使用: public static String format(String pattern, Object ... arguments) { MessageFormat temp = new MessageFormat(pattern); return temp.format(arguments); } 可以清晰看到,该静态方法本质上还是构造了一个MessageFormat实例去做格式化的。因此:若你要多次(如高并发场景)格式化同一个模版(参数可不一样)的话,那么提前创建好一个全局的(非static) MessageFormat实例再执行格式化是最好的,而非一直调用其静态方法。 说明:若你的系统非高并发场景,此性能损耗基本无需考虑哈,怎么方便怎么来。毕竟朝生夕死的对象对JVM来说没啥压力 和String.format选谁? 二者都能用于字符串拼接(格式化)上,撇开MessageFormat支持各种模式不说,我们只需要考虑它俩的性能上差异。 MeesageFormat:先分析(模版可提前分析,且可以只分析一次),再在指定位置上插入相应的值 分析:遍历字符串,维护一个{}数组并记录位置 填值 String.format:该静态方法是采用运行时用正则表达式 匹配到占位符,然后执行替换的 正则表达式为"%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])" 根据正则匹配到占位符列表和位置,然后填值 一说到正则表达式,我心里就发触,因为它对性能是不友好的,所以孰优孰劣,高下立判。 说明:还是那句话,没有绝对的谁好谁坏,如果你的系统对性能不敏感,那就是方便第一 经典使用场景 这个就很多啦,最常见的有:HTML拼接、SQL拼接、异常信息拼接等等。 比如下面这个SQL拼接: StringBuilder sb =new StringBuilder(); sb.append("insert into user ("); sb.append(" name,"); sb.append(" accountId,"); sb.append(" zhName,"); sb.append(" enname,"); sb.append(" status"); sb.append(") values ("); sb.append(" ''{0}'',"); sb.append(" {1},"); sb.append(" ''{2}'',"); sb.append(" ''{3}'',"); sb.append(" {4},"); sb.append(")"); Object[] args = {name, accountId, zhName, enname, status}; // 最终SQL String sql = MessageFormat.format(sb.toString(), arr); 你看,多工整。 说明:如果值是字符串需要'包起来,那么请使用两边各两个包起来 ✍总结 本文内容介绍了JDK原生的格式化器知识点,主要作用在这三个方面: DateFormat:日期时间格式化 NumberFormat:数字格式化 MessageFormat:字符串格式化 Spring是直接面向使用者的框架产品,很显然这些是不够用的,并且JDK的格式化器在设计上存在一些弊端。比如经常被吐槽的:日期/时间类型格式化器SimpleDateFormat为毛在java.text包里,而它格式化的类型Date却在java.util包内,这实为不合适。 有了JDK格式化器作为基础,下篇我们就可以浩浩荡荡的走进Spring格式化器的大门了,看看它是如何优于JDK进行设计和抽象的。 ✔✔✔推荐阅读✔✔✔ 【Spring类型转换】系列: 1. 揭秘Spring类型转换 - 框架设计的基石 2. Spring早期类型转换,基于PropertyEditor实现 3. 搞定收工,PropertyEditor就到这 4. 上新了Spring,全新一代类型转换机制 5. 穿过拥挤的人潮,Spring已为你制作好高级赛道 6. 抹平差异,统一类型转换服务ConversionService 【Jackson】系列: 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑 7. Jackson用树模型处理JSON是必备技能,不信你看 【数据校验Bean Validation】系列: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸 4. Validator校验器的五大核心组件,一个都不能少 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类 6. 自定义容器类型元素验证,类级别验证(多字段联合验证) 【新特性】系列: Spring Cloud 2020.0.0正式发布,再见了Netflix IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德 IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 [IntelliJ IDEA 2020.1正式发布,你要的Almost都在这!]() Spring Framework 5.3.0正式发布,在云原生路上继续发力 Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容) Spring改变版本号命名规则:此举对非英语国家很友好 JDK15正式发布,划时代的ZGC同时宣布转正 【程序人生】系列: 蚂蚁金服上市了,我不想努力了 如果程序员和产品经理都用凡尔赛文学对话...... 程序人生 | 春风得意马蹄疾,一日看尽长安花 还有诸如【Spring配置类】【Spring-static关键字】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,也可加我fsx1056342982,交个朋友。 有些已完结,有些连载中。我是A哥(YourBatman),咱们下期见
分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字专栏有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 https://www.yourbatman.cn 收录。 ✍前言 你好,我是YourBatman。 通过前两篇文章的介绍已经非常熟悉Spirng 3.0全新一代的类型转换机制了,它提供的三种类型转换器(Converter、ConverterFactory、GenericConverter),分别可处理1:1、1:N、N:N的类型转换。按照Spring的设计习惯,必有一个注册中心来统一管理,负责它们的注册、删除等,它就是ConverterRegistry。 对于ConverterRegistry在文首多说一句:我翻阅了很多博客文章介绍它时几乎无一例外的提到有查找的功能,但实际上是没有的。Spring设计此API接口并没有暴露其查找功能,选择把最为复杂的查找匹配逻辑私有化,目的是让开发者使可无需关心,细节之处充分体现了Spring团队API设计的卓越能力。 另外,内建的绝大多数转换器访问权限都是default/private,那么如何使用它们,以及屏蔽各种转换器的差异化呢?为此,Spring提供了一个统一类型转换服务,它就是ConversionService。 版本约定 Spring Framework:5.3.1 Spring Boot:2.4.0 ✍正文 ConverterRegistry和ConversionService的关系密不可分,前者为后者提供转换器管理支撑,后者面向使用者提供服务。本文涉及到的接口/类有: ConverterRegistry:转换器注册中心。负责转换器的注册、删除 ConversionService:统一的类型转换服务。属于面向开发者使用的门面接口 ConfigurableConversionService:上两个接口的组合接口 GenericConversionService:上个接口的实现,实现了注册管理、转换服务的几乎所有功能,是个实现类而非抽象类 DefaultConversionService:继承自GenericConversionService,在其基础上注册了一批默认转换器(Spring内建),从而具备基础转换能力,能解决日常绝大部分场景 ConverterRegistry Spring 3.0引入的转换器注册中心,用于管理新一套的转换器们。 public interface ConverterRegistry { void addConverter(Converter<?, ?> converter); <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter); void addConverter(GenericConverter converter); void addConverterFactory(ConverterFactory<?, ?> factory); // 唯一移除方法:按照转换pair对来移除 void removeConvertible(Class<?> sourceType, Class<?> targetType); } 它的继承树如下: ConverterRegistry有子接口FormatterRegistry,它属于格式化器的范畴,故不放在本文讨论。但仍旧属于本系列专题内容,会在接下来的几篇内容里介入,敬请关注。 ConversionService 面向使用者的统一类型转换服务。换句话说:站在使用层面,你只需要知道ConversionService接口API的使用方式即可,并不需要关心其内部实现机制,可谓对使用者非常友好。 public interface ConversionService { boolean canConvert(Class<?> sourceType, Class<?> targetType); boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); <T> T convert(Object source, Class<T> targetType); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } 它的继承树如下: 可以看到ConversionService和ConverterRegistry的继承树殊途同归,都直接指向了ConfigurableConversionService这个分支,下面就对它进行介绍。 ConfigurableConversionService ConversionService和ConverterRegistry的组合接口,自己并未新增任何接口方法。 public interface ConfigurableConversionService extends ConversionService, ConverterRegistry { } 它的继承树可参考上图。接下来就来到此接口的直接实现类GenericConversionService。 GenericConversionService 对ConfigurableConversionService接口提供了完整实现的实现类。换句话说:ConversionService和ConverterRegistry接口的功能均通过此类得到了实现,所以它是本文重点。 该类很有些值得学习的地方,可以细品,在我们自己设计程序时加以借鉴。 public class GenericConversionService implements ConfigurableConversionService { private final Converters converters = new Converters(); private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<ConverterCacheKey, GenericConverter>(64); } 它用两个成员变量来管理转换器们,其中converterCache是缓存用于加速查找,因此更为重要的便是Converters喽。 Converters是GenericConversionService的内部类,用于管理(添加、删除、查找)转换器们。也就说对ConverterRegistry接口的实现最终是委托给它去完成的,它是整个转换服务正常work的内核,下面我们对它展开详细叙述。 1、内部类Converters 它管理所有转换器,包括添加、删除、查找。 GenericConversionService: // 内部类 private static class Converters { private final Set<GenericConverter> globalConverters = new LinkedHashSet<GenericConverter>(); private final Map<ConvertiblePair, ConvertersForPair> converters = new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36); } 说明:这里使用的集合/Map均为LinkedHashXXX,都是有序的(存入顺序和遍历取出顺序保持一致) 用这两个集合/Map存储着注册进来的转换器们,他们的作用分别是: globalConverters:存取通用的转换器,并不限定转换类型,一般用于兜底 converters:指定了类型对,对应的转换器们的映射关系。 ConvertiblePair:表示一对,包含sourceType和targetType ConvertersForPair:这一对对应的转换器们(因为能处理一对的可能存在多个转换器),内部使用一个双端队列Deque来存储,保证顺序 小细节:Spring 5之前使用LinkedList,之后使用Deque(实际为ArrayDeque)存储 final class ConvertiblePair { private final Class<?> sourceType; private final Class<?> targetType; } private static class ConvertersForPair { private final Deque<GenericConverter> converters = new ArrayDeque<>(1); } 添加add public void add(GenericConverter converter) { Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes(); if (convertibleTypes == null) { ... // 放进globalConverters里 } else { ... // 放进converters里(若支持多组pair就放多个key) } } 在此之前需要了解个前提:对于三种转换器Converter、ConverterFactory、GenericConverter在添加到Converters之前都统一被适配为了GenericConverter,这样做的目的是方便统一管理。对应的两个适配器是ConverterAdapter和ConverterFactoryAdapter,它俩都是ConditionalGenericConverter的内部类。 添加的逻辑被我用伪代码简化后其实非常简单,无非就是一个非此即彼的关系而已: 若转换器没有指定处理的类型对,就放进全局转换器列表里,用于兜底 若转换器有指定处理的类型对(可能还是多个),就放进converters里,后面查找时使用 删除remove public void remove(Class<?> sourceType, Class<?> targetType) { this.converters.remove(new ConvertiblePair(sourceType, targetType)); } 移除逻辑非常非常的简单,这得益于添加时候做了统一适配的抽象。 查找find @Nullable public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) { // 找到该类型的类层次接口(父类 + 接口),注意:结果是有序列表 List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType()); List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType()); // 双重遍历 for (Class<?> sourceCandidate : sourceCandidates) { for (Class<?> targetCandidate : targetCandidates) { ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate); ... // 从converters、globalConverters里匹配到一个合适转换器后立马返回 } } return null; } 查找逻辑也并不复杂,有两个关键点需要关注: getClassHierarchy(class):获取该类型的类层次(父类 + 接口),注意:结果List是有序的List 也就是说转换器支持的类型若是父类/接口,那么也能够处理器子类 根据convertiblePair匹配转换器:优先匹配专用的converters,然后才是globalConverters。若都没匹配上返回null 2、管理转换器(ConverterRegistry) 了解了Converters之后再来看GenericConversionService是如何管理转换器,就如鱼得水,一目了然了。 添加 为了方便使用者调用,ConverterRegistry接口提供了三个添加方法,这里一一给与实现。 说明:暴露给调用者使用的API接口使用起来应尽量的方便,重载多个是个有效途径。内部做适配、归口即可,用户至上 @Override public void addConverter(Converter<?, ?> converter) { // 获取泛型类型 -> 转为ConvertiblePair ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class); ... // converter适配为GenericConverter添加 addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1])); } @Override public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) { addConverter(new ConverterAdapter(converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType))); } @Override public void addConverter(GenericConverter converter) { this.converters.add(converter); invalidateCache(); } 前两个方法都会调用到第三个方法上,每调用一次addConverter()方法都会清空缓存,也就是converterCache.clear()。所以动态添加转换器对性能是有损的,因此使用时候需稍加注意一些。 查找 ConverterRegistry接口并未直接提供查找方法,而只是在实现类内部做了实现。提供一个钩子方法用于查找给定sourceType/targetType对的转换器。 @Nullable protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); // 1、查缓存 GenericConverter converter = this.converterCache.get(key); if (converter != null) { ... // 返回结果 } // 2、去converters里查找 converter = this.converters.find(sourceType, targetType); if (converter == null) { // 若还没有匹配的,就返回默认结果 // 默认结果是NoOpConverter -> 什么都不做 converter = getDefaultConverter(sourceType, targetType); } ... // 把结果装进缓存converterCache里 return null; } 有了对Converters查找逻辑的分析,这个步骤就很简单了。绘制成图如下: 3、转换功能(ConversionService) 上半部分介绍完GenericConversionService对转换器管理部分的实现(对ConverterRegistry接口的实现),接下来就看看它是如何实现转换功能的(对ConversionService接口的实现)。 判断 @Override public boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType) { return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null), TypeDescriptor.valueOf(targetType)); } @Override public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { if (sourceType == null) { return true; } // 查找/匹配对应的转换器 GenericConverter converter = getConverter(sourceType, targetType); return (converter != null); } 能否执行转换判断的唯一标准:能否匹配到可用于转换的转换器。而这个查找匹配逻辑,稍稍抬头往上就能看到。 转换 @Override @SuppressWarnings("unchecked") @Nullable public <T> T convert(@Nullable Object source, Class<T> targetType) { return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); } @Override @Nullable public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { if (sourceType == null) { return handleResult(null, targetType, convertNullSource(null, targetType)); } // 校验:source必须是sourceType的实例 if (source != null && !sourceType.getObjectType().isInstance(source)) { throw new IllegalArgumentException("Source to convert from must be an instance of [" + sourceType + "]; instead it was a [" + source.getClass().getName() + "]"); } // ============拿到转换器,执行转换============ GenericConverter converter = getConverter(sourceType, targetType); if (converter != null) { Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); return handleResult(sourceType, targetType, result); } // 若没进行canConvert的判断直接调动,可能出现此种状况:一般抛出ConverterNotFoundException异常 return handleConverterNotFound(source, sourceType, targetType); } 同样的,执行转换的逻辑很简单,非常好理解的两个步骤: 查找匹配到一个合适的转换器(查找匹配的逻辑同上) 拿到此转换器执行转换converter.convert(...) 说明:其余代码均为一些判断、校验、容错,并非核心,本文给与适当忽略。 GenericConversionService实现了转换器管理、转换服务的所有功能,是可以直接面向开发者使用的。但是开发者使用时可能并不知道需要注册哪些转换器来保证程序正常运转,Spring并不能要求开发者知晓其内建实现。基于此,Spring在3.1又提供了一个默认实现DefaultConversionService,它对使用者更友好。 DefaultConversionService Spirng容器默认使用的转换服务实现,继承自GenericConversionService,在其基础行只做了一件事:构造时添加内建的默认转换器们。从而天然具备有了基本的类型转换能力,适用于不同的环境。如:xml解析、@Value解析、http协议参数自动转换等等。 小细节:它并非Spring 3.0就有,而是Spring 3.1新推出的API // @since 3.1 public class DefaultConversionService extends GenericConversionService { // 唯一构造器 public DefaultConversionService() { addDefaultConverters(this); } } 本类核心代码就这一个构造器,构造器内就这一句代码:addDefaultConverters(this)。接下来需要关注Spring默认情况下给我们“安装”了哪些转换器呢?也就是了解下addDefaultConverters(this)这个静态方法 默认注册的转换器们 // public的静态方法,注意是public的访问权限 public static void addDefaultConverters(ConverterRegistry converterRegistry) { addScalarConverters(converterRegistry); addCollectionConverters(converterRegistry); converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new StringToTimeZoneConverter()); converterRegistry.addConverter(new ZoneIdToTimeZoneConverter()); converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); converterRegistry.addConverter(new ObjectToObjectConverter()); converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new FallbackObjectToStringConverter()); converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); } 该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。为了方便记忆这个注册流程,我把它绘制成图供以你保存: 特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁,first win)。 针对这幅图,你可能还会有如下疑问: JSR310转换器只看到TimeZone、ZoneId等转换,怎么没看见更为常用的LocalDate、LocalDateTime等这些类型转换呢?难道Spring默认是不支持的? 答:当然不是。 这么常见的场景Spring怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以放在该系列后几篇关于格式化章节中再做讲述 一般的Converter都见名之意,但StreamConverter有何作用呢?什么场景下会生效 答:上文已讲述 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何 答:上文已讲述 最后,需要特别强调的是:它是一个静态方法,并且还是public的访问权限,且不仅仅只有本类调用。实际上,DefaultConversionService仅仅只做了这一件事,所以任何地方只要调用了该静态方法都能达到前者相同的效果,使用上可谓给与了较大的灵活性。比如Spring Boot环境下不是使用DefaultConversionService而是ApplicationConversionService,后者是对FormattingConversionService扩展,这个话题放在后面详解。 Spring Boot在web环境默认向容易注册了一个WebConversionService,因此你有需要可直接@Autowired使用 ConversionServiceFactoryBean 顾名思义,它是用于产生ConversionService类型转换服务的工厂Bean,为了方便和Spring容器整合而使用。 public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean { @Nullable private Set<?> converters; @Nullable private GenericConversionService conversionService; public void setConverters(Set<?> converters) { this.converters = converters; } @Override public void afterPropertiesSet() { // 使用的是默认实现哦 this.conversionService = new DefaultConversionService(); ConversionServiceFactory.registerConverters(this.converters, this.conversionService); } @Override @Nullable public ConversionService getObject() { return this.conversionService; } ... } 这里只有两个信息量需要关注: 使用的是DefaultConversionService,因此那一大串的内建转换器们都会被添加进来的 自定义转换器可以通过setConverters()方法添加进来 值得注意的是方法入参是Set<?>并没有明确泛型类型,因此那三种转换器(1:1/1:N/N:N)你是都可以添加. ✍总结 通读本文过后,相信能够给与你这个感觉:曾经望而却步的Spring类型转换服务ConversionService,其实也不过如此嘛。通篇我用了多个简单字眼来说明,因为拆开之后,无一高复杂度知识点。 迎难而上是积攒涨薪底气和勇气的途径,况且某些知识点其实并不难,所以我觉得从性价比角度来看这类内容是非常划算的,你pick到了麽? 正所谓类型转换和格式化属于两组近义词,在Spring体系中也经常交织在一起使用,有种傻傻分不清楚之感。从下篇文章起进入到本系列关于Formatter格式化器知识的梳理,什么日期格式化、@DateTimeFormat、@NumberFormat都将帮你捋清楚喽,有兴趣者可保持持续关注。 ✔✔✔推荐阅读✔✔✔ 【Spring类型转换】系列: 1. 揭秘Spring类型转换 - 框架设计的基石 2. Spring早期类型转换,基于PropertyEditor实现 3. 搞定收工,PropertyEditor就到这 4. 上新了Spring,全新一代类型转换机制 5. 穿过拥挤的人潮,Spring已为你制作好高级赛道 【Jackson】系列: 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑 7. Jackson用树模型处理JSON是必备技能,不信你看 【数据校验Bean Validation】系列: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸 4. Validator校验器的五大核心组件,一个都不能少 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类 6. 自定义容器类型元素验证,类级别验证(多字段联合验证) 【新特性】系列: IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德 IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 [IntelliJ IDEA 2020.1正式发布,你要的Almost都在这!]() Spring Framework 5.3.0正式发布,在云原生路上继续发力 Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容) Spring改变版本号命名规则:此举对非英语国家很友好 JDK15正式发布,划时代的ZGC同时宣布转正 【程序人生】系列: 蚂蚁金服上市了,我不想努力了 如果程序员和产品经理都用凡尔赛文学对话...... 程序人生 | 春风得意马蹄疾,一日看尽长安花 还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,也可加我fsx1056342982,交个朋友。 有些已完结,有些连载中。我是A哥(YourBatman),咱们下期见
分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字专栏有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 https://www.yourbatman.cn 收录。 ✍前言 你好,我是YourBatman。 上篇文章 大篇幅把Spring全新一代类型转换器介绍完了,已经至少能够考个及格分。在介绍Spring众多内建的转换器里,我故意留下一个尾巴,放在本文专门撰文讲解。 为了让自己能在“拥挤的人潮中”显得不(更)一(突)样(出),A哥特意准备了这几个特殊的转换器助你破局,穿越拥挤的人潮,踏上Spring已为你制作好的高级赛道。 版本约定 Spring Framework:5.3.1 Spring Boot:2.4.0 ✍正文 本文的焦点将集中在上文留下的4个类型转换器上。 StreamConverter:将Stream流与集合/数组之间的转换,必要时转换元素类型 这三个比较特殊,属于“最后的”“兜底类”类型转换器: ObjectToObjectConverter:通用的将原对象转换为目标对象(通过工厂方法or构造器) IdToEntityConverter:本文重点。给个ID自动帮你兑换成一个Entity对象 FallbackObjectToStringConverter:将任何对象调用toString()转化为String类型。当匹配不到任何转换器时,它用于兜底 默认转换器注册情况 Spring新一代类型转换内建了非常多的实现,这些在初始化阶段大都被默认注册进去。注册点在DefaultConversionService提供的一个static静态工具方法里: static静态方法具有与实例无关性,我个人觉得把该static方法放在一个xxxUtils里统一管理会更好,放在具体某个组件类里反倒容易产生语义上的误导性 DefaultConversionService: public static void addDefaultConverters(ConverterRegistry converterRegistry) { // 1、添加标量转换器(和数字相关) addScalarConverters(converterRegistry); // 2、添加处理集合的转换器 addCollectionConverters(converterRegistry); // 3、添加对JSR310时间类型支持的转换器 converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new StringToTimeZoneConverter()); converterRegistry.addConverter(new ZoneIdToTimeZoneConverter()); converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); // 4、添加兜底转换器(上面处理不了的全交给这几个哥们处理) converterRegistry.addConverter(new ObjectToObjectConverter()); converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new FallbackObjectToStringConverter()); converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); } } 该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。为了方便记忆这个注册流程,我把它绘制成图供以你保存: 特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁)。 针对这幅图,你可能还会有疑问: JSR310转换器只看到TimeZone、ZoneId等转换,怎么没看见更为常用的LocalDate、LocalDateTime等这些类型转换呢?难道Spring默认是不支持的? 答:当然不是。 这么常见的场景Spring怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以会在后3篇文章格式化章节在作为重中之重讲述 一般的Converter都见名之意,但StreamConverter有何作用呢?什么场景下会生效 答:本文讲述 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何 答:本文讲述 StreamConverter 用于实现集合/数组类型到Stream类型的互转,这从它支持的Set<ConvertiblePair> 集合也能看出来: @Override public Set<ConvertiblePair> getConvertibleTypes() { Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>(); convertiblePairs.add(new ConvertiblePair(Stream.class, Collection.class)); convertiblePairs.add(new ConvertiblePair(Stream.class, Object[].class)); convertiblePairs.add(new ConvertiblePair(Collection.class, Stream.class)); convertiblePairs.add(new ConvertiblePair(Object[].class, Stream.class)); return convertiblePairs; } 它支持的是双向的匹配规则: 代码示例 /** * {@link StreamConverter} */ @Test public void test2() { System.out.println("----------------StreamConverter---------------"); ConditionalGenericConverter converter = new StreamConverter(new DefaultConversionService()); TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(Set.class); TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Stream.class); boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp); System.out.println("是否能够转换:" + matches); // 执行转换 Object convert = converter.convert(Collections.singleton(1), sourceTypeDesp, targetTypeDesp); System.out.println(convert); System.out.println(Stream.class.isAssignableFrom(convert.getClass())); } 运行程序,输出: ----------------StreamConverter--------------- 是否能够转换:true java.util.stream.ReferencePipeline$Head@5a01ccaa true 关注点:底层依旧依赖DefaultConversionService完成元素与元素之间的转换。譬如本例Set -> Stream的实际步骤为: 也就是说任何集合/数组类型是先转换为中间状态的List,最终调用list.stream()转换为Stream流的;若是逆向转换先调用source.collect(Collectors.<Object>toList())把Stream转为List后,再转为具体的集合or数组类型。 说明:若source是数组类型,那底层实际使用的就是ArrayToCollectionConverter,注意举一反三 使用场景 StreamConverter它的访问权限是default,我们并不能直接使用到它。通过上面介绍可知Spring默认把它注册进了注册中心里,因此面向使用者我们直接使用转换服务接口ConversionService便可。 @Test public void test3() { System.out.println("----------------StreamConverter使用场景---------------"); ConversionService conversionService = new DefaultConversionService(); Stream<Integer> result = conversionService.convert(Collections.singleton(1), Stream.class); // 消费 result.forEach(System.out::println); // result.forEach(System.out::println); //stream has already been operated upon or closed } 运行程序,输出: ----------------StreamConverter使用场景--------------- 1 再次特别强调:流只能被读(消费)一次。 因为有了ConversionService提供的强大能力,我们就可以在基于Spring/Spring Boot做二次开发时使用它,提高系统的通用性和容错性。如:当方法入参是Stream类型时,你既可以传入Stream类型,也可以是Collection类型、数组类型,是不是瞬间逼格高了起来。 兜底转换器 按照添加转换器的顺序,Spring在最后添加了4个通用的转换器用于兜底,你可能平时并不关注它,但它实时就在发挥着它的作用。 ObjectToObjectConverter 将源对象转换为目标类型,非常的通用:Object -> Object: @Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); } 虽然它支持的是Object -> Object,看似没有限制但其实是有约定条件的: @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return (sourceType.getType() != targetType.getType() && hasConversionMethodOrConstructor(targetType.getType(), sourceType.getType())); } 是否能够处理的判断逻辑在于hasConversionMethodOrConstructor方法,直译为:是否有转换方法或者构造器。代码详细处理逻辑如下截图: 此部分逻辑可分为两个part来看: part1:从缓存中拿到Member,直接判断Member的可用性,可用的话迅速返回 part2:若part1没有返回,就执行三部曲,尝试找到一个合适的Member,然后放进缓存内(若没有就返回null) part1:快速返回流程 当不是首次进入处理时,会走快速返回流程。也就是第0步isApplicable判断逻辑,有这几个关注点: Member包括Method或者Constructor Method:若是static静态方法,要求方法的第1个入参类型必须是源类型sourceType;若不是static方法,则要求源类型sourceType必须是method.getDeclaringClass()的子类型/相同类型 Constructor:要求构造器的第1个入参类型必须是源类型sourceType 创建目标对象的实例,此转换器支持两种方式: 通过工厂方法/实例方法创建实例(method.invoke(source)) 通过构造器创建实例(ctor.newInstance(source)) 以上case,在下面均会给出代码示例。 part2:三部曲流程 对于首次处理的转换,就会进入到详细的三部曲逻辑:通过反射尝试找到合适的Member用于创建目标实例,也就是上图的1、2、3步。 step1:determineToMethod,从sourceClass里找实例方法,对方法有如下要求: 方法名必须叫 "to" + targetClass.getSimpleName(),如toPerson() 方法的访问权限必须是public 该方法的返回值必须是目标类型或其子类型 step2:determineFactoryMethod,找静态工厂方法,对方法有如下要求: 方法名必须为valueOf(sourceClass) 或者 of(sourceClass) 或者from(sourceClass) 方法的访问权限必须是public step3:determineFactoryConstructor,找构造器,对构造器有如下要求: 存在一个参数,且参数类型是sourceClass类型的构造器 构造器的访问权限必须是public 特别值得注意的是:此转换器不支持Object.toString()方法将sourceType转换为java.lang.String。对于toString()支持,请使用下面介绍的更为兜底的FallbackObjectToStringConverter。 代码示例 实例方法 // sourceClass @Data public class Customer { private Long id; private String address; public Person toPerson() { Person person = new Person(); person.setId(getId()); person.setName("YourBatman-".concat(getAddress())); return person; } } // tartgetClass @Data public class Person { private Long id; private String name; } 书写测试用例: @Test public void test4() { System.out.println("----------------ObjectToObjectConverter---------------"); ConditionalGenericConverter converter = new ObjectToObjectConverter(); Customer customer = new Customer(); customer.setId(1L); customer.setAddress("Peking"); Object convert = converter.convert(customer, TypeDescriptor.forObject(customer), TypeDescriptor.valueOf(Person.class)); System.out.println(convert); // ConversionService方式(实际使用方式) ConversionService conversionService = new DefaultConversionService(); Person person = conversionService.convert(customer, Person.class); System.out.println(person); } 运行程序,输出: ----------------ObjectToObjectConverter--------------- Person(id=1, name=YourBatman-Peking) Person(id=1, name=YourBatman-Peking) 静态工厂方法 // sourceClass @Data public class Customer { private Long id; private String address; } // targetClass @Data public class Person { private Long id; private String name; /** * 方法名称可以是:valueOf、of、from */ public static Person valueOf(Customer customer) { Person person = new Person(); person.setId(customer.getId()); person.setName("YourBatman-".concat(customer.getAddress())); return person; } } 测试用例完全同上,再次运行输出: ----------------ObjectToObjectConverter--------------- Person(id=1, name=YourBatman-Peking) Person(id=1, name=YourBatman-Peking) 方法名可以为valueOf、of、from任意一种,这种命名方式几乎是业界不成文的规矩,所以遵守起来也会比较容易。但是:建议还是注释写好,防止别人重命名而导致转换生效。 构造器 基本同静态工厂方法示例,略 使用场景 基于本转换器可以完成任意对象 -> 任意对象的转换,只需要遵循方法名/构造器默认的一切约定即可,在我们平时开发书写转换层时是非常有帮助的,借助ConversionService可以解决这一类问题。 对于Object -> Object的转换,另外一种方式是自定义Converter<S,T>,然后注册到注册中心。至于到底选哪种合适,这就看具体应用场景喽,本文只是多给你一种选择 IdToEntityConverter Id(S) --> Entity(T)。通过调用静态查找方法将实体ID兑换为实体对象。Entity里的该查找方法需要满足如下条件find[EntityName]([IdType]): 必须是static静态方法 方法名必须为find + entityName。如Person类的话,那么方法名叫findPerson 方法参数列表必须为1个 返回值类型必须是Entity类型 说明:此方法可以不必是public,但建议用public。这样即使JVM的Security安全级别开启也能够正常访问 支持的转换Pair如下:ID和Entity都可以是任意类型,能转换就成 @Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); } 判断是否能执行准换的条件是:存在符合条件的find方法,且source可以转换为ID类型(注意source能转换成id类型就成,并非目标类型哦) @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { Method finder = getFinder(targetType.getType()); return (finder != null && this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0]))); } 根据ID定位到Entity实体对象简直太太太常用了,运用好此转换器的提供的能力,或许能让你事半功倍,大大减少重复代码,写出更优雅、更简洁、更易于维护的代码。 代码示例 Entity实体:准备好符合条件的findXXX方法 @Data public class Person { private Long id; private String name; /** * 根据ID定位一个Person实例 */ public static Person findPerson(Long id) { // 一般根据id从数据库查,本处通过new来模拟 Person person = new Person(); person.setId(id); person.setName("YourBatman-byFindPerson"); return person; } } 应用IdToEntityConverter,书写示例代码: @Test public void test() { System.out.println("----------------IdToEntityConverter---------------"); ConditionalGenericConverter converter = new IdToEntityConverter(new DefaultConversionService()); TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(String.class); TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Person.class); boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp); System.out.println("是否能够转换:" + matches); // 执行转换 Object convert = converter.convert("1", sourceTypeDesp, targetTypeDesp); System.out.println(convert); } 运行程序,正常输出: ----------------IdToEntityConverter--------------- 是否能够转换:true Person(id=1, name=YourBatman-byFindPerson) 示例效果为:传入字符串类型的“1”,就能返回得到一个Person实例。可以看到,我们传入的是字符串类型的的1,而方法入参id类型实际为Long类型,但因为它们能完成String -> Long转换,因此最终还是能够得到一个Entity实例的。 使用场景 这个使用场景就比较多了,需要使用到findById()的地方都可以通过它来代替掉。如: Controller层: @GetMapping("/ids/{id}") public Object getById(@PathVariable Person id) { return id; } @GetMapping("/ids") public Object getById(@RequestParam Person id) { return id; } Tips:在Controller层这么写我并不建议,因为语义上没有对齐,势必在代码书写过程中带来一定的麻烦。 Service层: @Autowired private ConversionService conversionService; public Object findById(String id){ Person person = conversionService.convert(id, Person.class); return person; } Tips:在Service层这么写,我个人觉得还是OK的。用类型转换的领域设计思想代替了自上而下的过程编程思想。 FallbackObjectToStringConverter 通过简单的调用Object#toString()方法将任何支持的类型转换为String类型,它作为底层兜底。 @Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Object.class, String.class)); } 该转换器支持CharSequence/StringWriter等类型,以及所有ObjectToObjectConverter.hasConversionMethodOrConstructor(sourceClass, String.class)的类型。 说明:ObjectToObjectConverter不处理任何String类型的转换,原来都是交给它了 代码示例 略。 ObjectToOptionalConverter 将任意类型转换为一个Optional<T>类型,它作为最最最最最底部的兜底,稍微了解下即可。 代码示例 @Test public void test5() { System.out.println("----------------ObjectToOptionalConverter---------------"); ConversionService conversionService = new DefaultConversionService(); Optional<Integer> result = conversionService.convert(Arrays.asList(2), Optional.class); System.out.println(result); } 运行程序,输出: ----------------ObjectToOptionalConverter--------------- Optional[[2]] 使用场景 一个典型的应用场景:在Controller中可传可不传的参数中,我们不仅可以通过@RequestParam(required = false) Long id来做,还是可以这么写:@RequestParam Optional<Long> id。 ✍总结 本文是对上文介绍Spring全新一代类型转换机制的补充,因为关注得人较少,所以才有机会突破。 针对于Spring注册转换器,需要特别注意如下几点: 注册顺序很重要。先注册,先服务(若支持的话) 默认情况下,Spring会注册大量的内建转换器,从而支持String/数字类型转换、集合类型转换,这能解决协议层面的大部分转换问题。 如Controller层,输入的是JSON字符串,可用自动被封装为数字类型、集合类型等等 如@Value注入的是String类型,但也可以用数字、集合类型接收 对于复杂的对象 -> 对象类型的转换,一般需要你自定义转换器,或者参照本文的标准写法完成转换。总之:Spring提供的ConversionService专注于类型转换服务,是一个非常非常实用的API,特别是你正在做基于Spring二次开发的情况下。 当然喽,关于ConversionService这套机制还并未详细介绍,如何使用?如何运行?如何扩展?带着这三个问题,咱们下篇见。 ✔✔✔推荐阅读✔✔✔ 【Spring类型转换】系列: 1. 揭秘Spring类型转换 - 框架设计的基石 2. Spring早期类型转换,基于PropertyEditor实现 3. 搞定收工,PropertyEditor就到这 4. 上新了Spring,全新一代类型转换机制 【Jackson】系列: 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑 7. Jackson用树模型处理JSON是必备技能,不信你看 【数据校验Bean Validation】系列: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸 4. Validator校验器的五大核心组件,一个都不能少 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类 6. 自定义容器类型元素验证,类级别验证(多字段联合验证) 【新特性】系列: IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德 IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 [IntelliJ IDEA 2020.1正式发布,你要的Almost都在这!]() Spring Framework 5.3.0正式发布,在云原生路上继续发力 Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容) Spring改变版本号命名规则:此举对非英语国家很友好 JDK15正式发布,划时代的ZGC同时宣布转正 【程序人生】系列: 蚂蚁金服上市了,我不想努力了 如果程序员和产品经理都用凡尔赛文学对话...... 程序人生 | 春风得意马蹄疾,一日看尽长安花 还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,也可加我fsx1056342982,交个朋友。 有些已完结,有些连载中。我是A哥(YourBatman),咱们下期再见
分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字专栏有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 https://www.yourbatman.cn 收录。 ✍前言 你好,我是YourBatman。 上篇文章 介绍完了Spring类型转换早期使用的PropertyEditor详细介绍,关于PropertyEditor现存的资料其实还蛮少的,希望这几篇文章能弥补这块空白,贡献一份微薄之力。 如果你也吐槽过PropertyEditor不好用,那么本文将对会有帮助。Spring自3.0版本开始自建了一套全新类型转换接口,这就是本文的主要内容,接下来逐步展开。 说明:Spring自3.0后笑傲群雄,进入大一统。Java从此步入Spring的时代 版本约定 Spring Framework:5.3.1 Spring Boot:2.4.0 ✍正文 在了解新一代的转换接口之前,先思考一个问题:Spring为何要自己造一套轮子呢? 一向秉承不重复造轮子原则的Spring,不是迫不得已的话是不会去动他人奶酪的,毕竟互利共生才能长久。类型转换,作为Spring框架的基石,扮演着异常重要的角色,因此对其可扩展性、可维护性、高效性均有很高要求。 基于此,我们先来了解下PropertyEditor设计上到底有哪些缺陷/不足(不能满足现代化需求),让Spring“被迫”走上了自建道路。 PropertyEditor设计缺陷 前提说明:本文指出它的设计缺陷,只讨论把它当做类型转换器在转换场景下存在的一些缺陷。 职责不单一:该接口有非常多的方法,但只用到2个而已 类型不安全:setValue()方法入参是Object,getValue()返回值是Object,依赖于约定好的类型强转,不安全 线程不安全:依赖于setValue()后getValue(),实例是线程不安全的 语义不清晰:从语义上根本不能知道它是用于类型转换的组件 只能用于String类型:它只能进行String <-> 其它类型的转换,而非更灵活的Object <-> Object PropertyEditor存在这五宗“罪”,让Spring决定自己设计一套全新API用于专门服务于类型转换,这就是本文标题所述:新一代类型转换Converter、ConverterFactory、GenericConverter。 关于PropertyEditor在Spring中的详情介绍,请参见文章:3. 搞定收工,PropertyEditor就到这 新一代类型转换 为了解决PropertyEditor作为类型转换方式的设计缺陷,Spring 3.0版本重新设计了一套类型转换接口,有3个核心接口: Converter<S, T>:Source -> Target类型转换接口,适用于1:1转换 ConverterFactory<S, R>:Source -> R类型转换接口,适用于1:N转换 GenericConverter:更为通用的类型转换接口,适用于N:N转换 注意:就它没有泛型约束,因为是通用 另外,还有一个条件接口ConditionalConverter,可跟上面3个接口搭配组合使用,提供前置条件判断验证。 这套接口,解决了PropertyEditor做类型转换存在的所有缺陷,且具有非常高的灵活性和可扩展性。下面进入详细了解。 Converter 将源类型S转换为目标类型T。 @FunctionalInterface public interface Converter<S, T> { T convert(S source); } 它是个函数式接口,接口定义非常简单。适合1:1转换场景:可以将任意类型 转换为 任意类型。它的实现类非常多,部分截图如下: 值得注意的是:几乎所有实现类的访问权限都是default/private,只有少数几个是public公开的,下面我用代码示例来“近距离”感受一下。 代码示例 /** * Converter:1:1 */ @Test public void test() { System.out.println("----------------StringToBooleanConverter---------------"); Converter<String, Boolean> converter = new StringToBooleanConverter(); // trueValues.add("true"); // trueValues.add("on"); // trueValues.add("yes"); // trueValues.add("1"); System.out.println(converter.convert("true")); System.out.println(converter.convert("1")); // falseValues.add("false"); // falseValues.add("off"); // falseValues.add("no"); // falseValues.add("0"); System.out.println(converter.convert("FalSe")); System.out.println(converter.convert("off")); // 注意:空串返回的是null System.out.println(converter.convert("")); System.out.println("----------------StringToCharsetConverter---------------"); Converter<String, Charset> converter2 = new StringToCharsetConverter(); // 中间横杠非必须,但强烈建议写上 不区分大小写 System.out.println(converter2.convert("uTf-8")); System.out.println(converter2.convert("utF8")); } 运行程序,正常输出: ----------------StringToBooleanConverter--------------- true true false false null ----------------StringToCharsetConverter--------------- UTF-8 UTF-8 说明:StringToBooleanConverter/StringToCharsetConverter访问权限都是default,外部不可直接使用。此处为了做示例用到一个小技巧 -> 将Demo的报名调整为和转换器的一样,这样就可以直接访问。 关注点:true/on/yes/1都能被正确转换为true的,且对于英文字母来说一般都不区分大小写,增加了容错性(包括Charset的转换)。 不足 Converter用于解决1:1的任意类型转换,因此它必然存在一个不足:解决1:N转换问题需要写N遍,造成重复冗余代码。 譬如:输入是字符串,它可以转为任意数字类型,包括byte、short、int、long、double等等,如果用Converter来转换的话每个类型都得写个转换器,想想都麻烦有木有。 Spring早早就考虑到了该场景,提供了相应的接口来处理,它就是ConverterFactory<S, R>。 ConverterFactory 从名称上看它代表一个转换工厂:可以将对象S转换为R的所有子类型,从而形成1:N的关系。 该接口描述为xxxFactory是非常合适的,很好的表达了1:N的关系 public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); } 它同样也是个函数式接口。该接口的实现类并不多,Spring Framework共提供了5个内建实现(访问权限全部为default): 以StringToNumberConverterFactory为例看看实现的套路: final class StringToNumberConverterFactory implements ConverterFactory<String, Number> { @Override public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) { return new StringToNumber<T>(targetType); } // 私有内部类:实现Converter接口。用泛型边界约束一类类型 private static final class StringToNumber<T extends Number> implements Converter<String, T> { private final Class<T> targetType; public StringToNumber(Class<T> targetType) { this.targetType = targetType; } @Override public T convert(String source) { if (source.isEmpty()) { return null; } return NumberUtils.parseNumber(source, this.targetType); } } } 由点知面,ConverterFactory作为Converter的工厂,对Converter进行包装,从而达到屏蔽内部实现的目的,对使用者友好,这不正是工厂模式的优点么,符合xxxFactory的语义。但你需要清除的是,工厂内部实现其实也是通过众多if else之类的去完成的,本质上并无差异。 代码示例 /** * ConverterFactory:1:N */ @Test public void test2() { System.out.println("----------------StringToNumberConverterFactory---------------"); ConverterFactory<String, Number> converterFactory = new StringToNumberConverterFactory(); // 注意:这里不能写基本数据类型。如int.class将抛错 System.out.println(converterFactory.getConverter(Integer.class).convert("1").getClass()); System.out.println(converterFactory.getConverter(Double.class).convert("1.1").getClass()); System.out.println(converterFactory.getConverter(Byte.class).convert("0x11").getClass()); } 运行程序,正常输出: ----------------StringToNumberConverterFactory--------------- class java.lang.Integer class java.lang.Double class java.lang.Byte 关注点:数字类型的字符串,是可以被转换为任意Java中的数字类型的,String(1) -> Number(N)。这便就是ConverterFactory的功劳,它能处理这一类转换问题。 不足 既然有了1:1、1:N,自然就有N:N。比如集合转换、数组转换、Map到Map的转换等等,这些N:N的场景,就需要借助下一个接口GenericConverter来实现。 GenericConverter 它是一个通用的转换接口,用于在两个或多个类型之间进行转换。相较于前两个,这是最灵活的SPI转换器接口,但也是最复杂的。 public interface GenericConverter { Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); // 普通POJO final class ConvertiblePair { private final Class<?> sourceType; private final Class<?> targetType; } } 该接口并非函数式接口,虽然方法不多但稍显复杂。现对出现的几个类型做简单介绍: ConvertiblePair:维护sourceType和targetType的POJO getConvertibleTypes()方法返回此Pair的Set集合。由此也能看出该转换器是可以支持N:N的(大多数情况下只写一对值而已,也有写多对的) TypeDescriptor:类型描述。该类专用于Spring的类型转换场景,用于描述from or to的类型 比单独的Type类型强大,内部借助了ResolvableType来解决泛型议题 GenericConverter的内置实现也比较多,部分截图如下: ConditionalGenericConverter是GenericConverter和条件接口ConditionalConverter的组合,作用是在执行GenericConverter转换时增加一个前置条件判断方法。 转换器 描述 示例 ArrayToArrayConverter 数组转数组Object[] -> Object[] ["1","2"] -> [1,2] ArrayToCollectionConverter 数组转集合 Object[] -> Collection 同上 CollectionToCollectionConverter 数组转集合 Collection -> Collection 同上 StringToCollectionConverter 字符串转集合String -> Collection 1,2 -> [1,2] StringToArrayConverter 字符串转数组String -> Array 同上 MapToMapConverter Map -> Map(需特别注意:key和value都支持转换才行) 略 CollectionToStringConverter 集合转字符串Collection -> String [1,2] -> 1,2 ArrayToStringConverter 委托给CollectionToStringConverter完成 同上 -- -- -- StreamConverter 集合/数组 <-> Stream互转 集合/数组类型 -> Stream类型 IdToEntityConverter ID->Entity的转换 传入任意类型ID -> 一个Entity实例 ObjectToObjectConverter 很复杂的对象转换,任意对象之间 obj -> obj FallbackObjectToStringConverter 上个转换器的兜底,调用Obj.toString()转换 obj -> String 说明:分割线下面的4个转换器比较特殊,字面上不好理解其实际作用,比较“高级”。它们如果能被运用在日常工作中可以事半功弎,因此放在在下篇文章专门给你介绍 下面以CollectionToCollectionConverter为例分析此转换器的“复杂”之处: final class CollectionToCollectionConverter implements ConditionalGenericConverter { private final ConversionService conversionService; public CollectionToCollectionConverter(ConversionService conversionService) { this.conversionService = conversionService; } // 集合转集合:如String集合转为Integer集合 @Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Collection.class, Collection.class)); } } 这是唯一构造器,必须传入ConversionService:元素与元素之间的转换是依赖于conversionService转换服务去完成的,最终完成集合到集合的转换。 CollectionToCollectionConverter: @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); } 判断能否转换的依据:集合里的元素与元素之间是否能够转换,底层依赖于ConversionService#canConvert()这个API去完成判断。 接下来再看最复杂的转换方法: CollectionToCollectionConverter: @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } Collection<?> sourceCollection = (Collection<?>) source; // 判断:这些情况下,将不用执行后续转换动作了,直接返回即可 boolean copyRequired = !targetType.getType().isInstance(source); if (!copyRequired && sourceCollection.isEmpty()) { return source; } TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); if (elementDesc == null && !copyRequired) { return source; } Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), (elementDesc != null ? elementDesc.getType() : null), sourceCollection.size()); // 若目标类型没有指定泛型(没指定就是Object),不用遍历直接添加全部即可 if (elementDesc == null) { target.addAll(sourceCollection); } else { // 遍历:一个一个元素的转,时间复杂度还是蛮高的 // 元素转元素委托给conversionService去完成 for (Object sourceElement : sourceCollection) { Object targetElement = this.conversionService.convert(sourceElement, sourceType.elementTypeDescriptor(sourceElement), elementDesc); target.add(targetElement); if (sourceElement != targetElement) { copyRequired = true; } } } return (copyRequired ? target : source); } 该转换步骤稍微有点复杂,我帮你屡清楚后有这几个关键步骤: 快速返回:对于特殊情况,做快速返回处理 若目标元素类型是源元素类型的子类型(或相同),就没有转换的必要了(copyRequired = false) 若源集合为空,或者目标集合没指定泛型,也不需要做转换动作 源集合为空,还转换个啥 目标集合没指定泛型,那就是Object,因此可以接纳一切,还转换个啥 若没有触发快速返回。给目标创建一个新集合,然后把source的元素一个一个的放进新集合里去,这里又分为两种处理case 若新集合(目标集合)没有指定泛型类型(那就是Object),就直接putAll即可,并不需要做类型转换 若新集合(目标集合指定了泛型类型),就遍历源集合委托conversionService.convert()对元素一个一个的转 代码示例 以CollectionToCollectionConverter做示范:List<String> -> Set<Integer> @Test public void test3() { System.out.println("----------------CollectionToCollectionConverter---------------"); ConditionalGenericConverter conditionalGenericConverter = new CollectionToCollectionConverter(new DefaultConversionService()); // 将Collection转为Collection(注意:没有指定泛型类型哦) System.out.println(conditionalGenericConverter.getConvertibleTypes()); List<String> sourceList = Arrays.asList("1", "2", "2", "3", "4"); TypeDescriptor sourceTypeDesp = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)); TypeDescriptor targetTypeDesp = TypeDescriptor.collection(Set.class, TypeDescriptor.valueOf(Integer.class)); System.out.println(conditionalGenericConverter.matches(sourceTypeDesp, targetTypeDesp)); Object convert = conditionalGenericConverter.convert(sourceList, sourceTypeDesp, targetTypeDesp); System.out.println(convert.getClass()); System.out.println(convert); } 运行程序,正常输出: [java.util.Collection -> java.util.Collection] true class java.util.LinkedHashSet [1, 2, 3, 4] 关注点:target最终使用的是LinkedHashSet来存储,这结果和CollectionFactory#createCollection该API的实现逻辑是相关(Set类型默认创建的是LinkedHashSet实例)。 不足 如果说它的优点是功能强大,能够处理复杂类型的转换(PropertyEditor和前2个接口都只能转换单元素类型),那么缺点就是使用、自定义实现起来比较复杂。这不官方也给出了使用指导意见:在Converter/ConverterFactory接口能够满足条件的情况下,可不使用此接口就不使用。 ConditionalConverter 条件接口,@since 3.2。它可以为Converter、GenericConverter、ConverterFactory转换增加一个前置判断条件。 public interface ConditionalConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); } 该接口的实现,截图如下: 可以看到,只有通用转换器GenericConverter和它进行了合体。这也很容易理解,作为通用的转换器,加个前置判断将更加严谨和更安全。对于专用的转换器如Converter,它已明确规定了转换的类型,自然就不需要做前置判断喽。 ✍总结 本文详细介绍了Spring新一代的类型转换接口,类型转换作为Spring的基石,其重要性可见一斑。 PropertyEditor作为Spring早期使用“转换器”,因存在众多设计缺陷自Spring 3.0起被新一代转换接口所取代,主要有: Converter<S, T>:Source -> Target类型转换接口,适用于1:1转换 ConverterFactory<S, R>:Source -> R类型转换接口,适用于1:N转换 GenericConverter:更为通用的类型转换接口,适用于N:N转换 下篇文章将针对于GenericConverter的几个特殊实现撰专文为你讲解,你也知道做难事必有所得,做难事才有可能破局、破圈,欢迎保持关注。 ✔✔✔推荐阅读✔✔✔ 【Spring类型转换】系列: 1. 揭秘Spring类型转换 - 框架设计的基石 2. Spring早期类型转换,基于PropertyEditor实现 3. 搞定收工,PropertyEditor就到这 【Jackson】系列: 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑 7. Jackson用树模型处理JSON是必备技能,不信你看 【数据校验Bean Validation】系列: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸 4. Validator校验器的五大核心组件,一个都不能少 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类 6. 自定义容器类型元素验证,类级别验证(多字段联合验证) 【新特性】系列: IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德 IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 [IntelliJ IDEA 2020.1正式发布,你要的Almost都在这!]() Spring Framework 5.3.0正式发布,在云原生路上继续发力 Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容) Spring改变版本号命名规则:此举对非英语国家很友好 JDK15正式发布,划时代的ZGC同时宣布转正 【程序人生】系列: 蚂蚁金服上市了,我不想努力了 如果程序员和产品经理都用凡尔赛文学对话...... 程序人生 | 春风得意马蹄疾,一日看尽长安花 还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,分享、成长,拒绝浅藏辄止。 有些专栏已完结,有些正在连载中,期待你的关注、共同进步
青年时种下什么,老年时就收获什么。关注公众号【BAT的乌托邦】,有Spring技术栈、MyBatis、JVM、中间件等小而美的原创专栏供以免费学习。分享、成长,拒绝浅尝辄止。本文已被 https://www.yourbatman.cn 收录。 ✍前言 你好,我是YourBatman。 Spring早在1.0(2004年发布,2003年孵化中)的时候,就有了类型转换功能模块。此模块存在的必要性不必多说,相信每个同学都可理解。最初,Spring做类型转换器是基于Java标准的java.beans.PropertyEditor这个API去扩展实现的,直到Spring 3.0后才得以出现更好替代方案(Spring 3.0发布于2009 年12月)。 提示:文章末尾附有Spring主要版本的发布时间和以及主要特性,感兴趣者可文末查看 虽说Spring自3.0就提出了更为灵活、优秀的类型转换接口/服务,但是早期基于PropertyEditor实现的转换器并未废弃且还在发挥余热中,因此本文就针对其早期类型转换实现做出专文讲解。 版本约定 Spring Framework:5.3.1 Spring Boot:2.4.0 说明:版本均于2020-11发布,且版本号均不带有.RELEASE后缀,这和之前是不一样的。具体原因请参考:Spring改变版本号命名规则:此举对非英语国家很友好 ✍正文 若你用当下的眼光去看Spring基于PropertyEditor的类型转换实现,会发现这么搞是存在一些设计缺陷的。当然并不能这么去看,毕竟现在都2020年了,那会才哪跟哪呢。既然Spring里的PropertyEditor现如今依然健在,那咱就会会它呗。 PropertyEditor是什么? PropertyEditor位于java.beans包中,要知道这个包里面的类都是设计为Java GUI程序(AWT)服务的,所以你看官方javadoc对PropertyEditor的介绍也无出其右: A PropertyEditor class provides support for GUIs that want to allow users to edit a property value of a given type. 为GUI程序提供支持,允许你对给定的value进行编辑,作用类似于一个转换器:GUI上你可以输入、编辑某个属性然后经过它转换成合适的类型。 此接口提供的方法挺多的,和本文类型转换有关的最多只有4个: void setValue(Object value):设置属性值 Object getValue():获取属性值 String getAsText():输出。把属性值转换成String输出 void setAsText(String text):输入。将String转换为属性值类型输入 JDK对PropertyEditor接口提供了一个默认实现java.beans.PropertyEditorSupport,因此我们若需扩展此接口,仅需继承此类,根据需要复写getAsText/setAsText这两个方法即可,Spring无一例外都是这么做的。 PropertyEditor作为一个JDK原生接口,内置了一些基本实现来服务于GUI程序,如: BooleanEditor:将true/false字符串转换为Boolean类型 IntegerEditor:将字符串转换为Integer类型 同类别的还有LongEditor、FloatEditor... JDK内置的实现比较少(如上),功能简陋,但对于服务GUI程序来说已经够用,毕竟界面输入的只可能是字符串,并且还均是基础类型。但这对于复杂的Spring环境、以及富文本的web环境来说就不够用了,所以Spring在此基础上有所扩展,因此才有了本文来讨论。 注意:PropertyEditorSupport线程不安全 PropertyEditor实现的是双向类型转换:String和Object互转。调用setValue()方法后,需要先“缓存”起来后续才能够使用(输出)。PropertyEditorSupport为此提供了一个成员属性来做: PropertyEditorSupport: // 调用setValue()方法对此属性赋值 getValue()方法取值 private Object value; 这么一来PropertyEditorSupport就是有状态的了,因此是线程不安全的。在使用过程中需要特别注意,避免出现并发风险。 说明:Support类里还看到属性监听器PropertyChangeListener,因它属于GUI程序使用的组件,与我们无关,所以后续丝毫不会提及哦 Spring内置的所有扩展均是基于PropertyEditorSupport来实现的,因此也都是线程不安全的哦~ Spring为何基于它扩展? 官方的javadoc都说得很清楚:PropertyEditor设计是为GUI程序服务的,那么Spring为何看上它了呢? 试想一下:那会的Spring只能支持xml方式配置,而XML属于文本类型配置,因此在给某个属性设定值的时候,书写上去的100%是个字符串,但是此属性对应的类型却不一定是字符串,可能是任意类型。你思考下,这种场景是不是跟GUI程序(AWT)一毛一样:输入字符串,对应任意类型。 为了实现这种需求,在PropertyEditorSupport的基础上只需要复写setAsText和getAsText这两个方法就行,然后Spring就这么干了。我个人yy一下,当初Spring选择这么干而没自己另起炉灶的原因可能有这么几个: 本着不重复发明轮子的原则,有得用就直接用呗,况且是100%满足要求的 示好Java EE技术。毕竟那会Spring地位还并不稳,有大腿就先榜上 2003年左右,Java GUI程序还并未退出历史舞台,Spring为了通用性就选择基于它扩展喽 说明:那会的通用性可能和现在通用性含义上是不一样的,需要稍作区别 Spring内建扩展实现有哪些? Spring为了扩展自身功能,提高配置灵活性,扩展出了非常非常多的PropertyEditor实现,共计40余个,部分截图如下: PropertyEditor 功能 举例 ZoneIdEditor 转为java.time.ZoneId Asia/Shanghai URLEditor 转为URL,支持传统方式file:,http:,也支持Spring风格:classpath:,context上下文相对路径等等 http://www.baidu.com StringTrimmerEditor trim()字符串,也可删除指定字符char 任意字符串 StringArrayPropertyEditor 转为字符串数组 A,B,C PropertiesEditor 转为Properties name = YourBatman PatternEditor 转为Pattern (\D)(\d+)(.) PathEditor 转为java.nio.file.Path。支持传统URL和Spring风格的url classpath:xxx ClassEditor 转为Class 全类名 CustomBooleanEditor 转为Boolean 见示例 CharsetEditor 转为Charset 见示例 CustomDateEditor 转为java.util.Date 见示例 Spring把实现基本(大多数)都放在org.springframework.beans.propertyeditors包下,接下来我挑选几个代表性API举例说明。 标准实现示例 CustomBooleanEditor: @Test public void test1() { PropertyEditor editor = new CustomBooleanEditor(true); // 这些都是true,不区分大小写 editor.setAsText("trUe"); System.out.println(editor.getAsText()); editor.setAsText("on"); System.out.println(editor.getAsText()); editor.setAsText("yes"); System.out.println(editor.getAsText()); editor.setAsText("1"); System.out.println(editor.getAsText()); // 这些都是false(注意:null并不会输出为false,而是输出空串) editor.setAsText(null); System.out.println(editor.getAsText()); editor.setAsText("fAlse"); System.out.println(editor.getAsText()); editor.setAsText("off"); System.out.println(editor.getAsText()); editor.setAsText("no"); System.out.println(editor.getAsText()); editor.setAsText("0"); System.out.println(editor.getAsText()); // 报错 editor.setAsText("2"); System.out.println(editor.getAsText()); } 关注点:对于Spring来说,传入的true、on、yes、1等都会被“翻译”成true(字母不区分大小写),大大提高兼容性。 现在知道为啥你用Postman传个1,用Bool值也能正常封装进去了吧,就是它的功劳。此效果等同于转换器StringToBooleanConverter,将在后面进行讲述对比 CharsetEditor: @Test public void test2() { // 虽然都行,但建议你规范书写:UTF-8 PropertyEditor editor = new CharsetEditor(); editor.setAsText("UtF-8"); System.out.println(editor.getAsText()); // UTF-8 editor.setAsText("utF8"); System.out.println(editor.getAsText()); // UTF-8 } 关注点:utf-8中间的横杠可要可不要,但建议加上使用标准写法,另外字母也是不区分大小写的。 CustomDateEditor: @Test public void test3() { PropertyEditor editor = new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),true); editor.setAsText("2020-11-30 09:10:10"); System.out.println(editor.getAsText()); // 2020-11-30 09:10:10 // null输出空串 editor.setAsText(null); System.out.println(editor.getAsText()); // 报错 editor.setAsText("2020-11-30"); System.out.println(editor.getAsText()); } 关注点:这个时间/日期转换器很不好用,构造时就必须指定一个SimpleDateFormat格式化器。在实际应用中,Spring并没有使用到它,而是用后面会说到的替代方案。 特殊实现 把没有放在org.springframework.beans.propertyeditors包下的实现称作特殊实现(前者称为标准实现)。 MediaTypeEditor:位于org.springframework.http。依赖的核心API是MediaType.parseMediaType(text),可以把诸如text/html、application/json转为MediaType对象 显然它属于spring-web包,使用在web环境下 FormatterPropertyEditorAdapter:位于org.springframework.format.support。将3.0新增的Formatter接口适配为一个PropertyEditor:setAsText这种转换操作委托给formatter.parse()去完成,反向委托给formatter.print()去完成。 此适配器在DataBinder#addCustomFormatter()得到应用 PropertyValuesEditor:位于org.springframework.beans。将k-v字符串(Properties格式)转换为一个PropertyValues,从而方便放进Environment里 ResourceEditor:位于org.springframework.core.io。此转换器将String转换为Resource资源,特别实用。作为基础设施,广泛被用到Spring的很多地方 像什么标准的FileEditor、InputSourceEditor、InputStreamEditor、URLEditor等等与资源相关的转换器,均依赖它而实现 @Test public void test4() { // 支持标准URL如file:C:/myfile.txt,也支持classpath:myfile.txt // 同时还支持占位符形式 PropertyEditor editor = new ResourceEditor(new DefaultResourceLoader(), new StandardEnvironment(), true); // file:形式本处略 // editor.setAsText("file:..."); // System.out.println(editor.getAsText()); // classpath形式(注意:若文件不存在不会报错,而是输出null) editor.setAsText("classpath:app.properties"); System.out.println(editor.getAsText()); // 输出带盘符的全路径 System.setProperty("myFile.name", "app.properties"); editor.setAsText("classpath:${myFile.name}"); System.out.println(editor.getAsText()); // 结果同上 } 关注点:Spring扩展出来的Resource不仅自持常规file:资源协议,还支持平时使用最多的classpath:协议,可谓非常好用。 ConvertingPropertyEditorAdapter:位于org.springframework.core.convert.support。将3.0新增的ConversionService转换服务适配为一个PropertyEditor,内部转换动作都委托给前者去完成。 AbstractPropertyBindingResult#findEditor()为属性寻找合适PropertyEditor的时候,若ConversionService能支持就包装为ConvertingPropertyEditorAdapter供以使用,这是适配器模式的典型应用场景。 “谁”在使用ProertyEditor PropertyEditor自动发现机制 PropertyEditor存在的缺陷 考虑到阅读的舒适性,单篇文章不宜太长,因此涉及到PropertyEditor的这几个问题,放在下篇文章单独列出。这个几个问题会明显比本文更深入,欢迎保持持续关注,peace! ✍总结 本文主要介绍了三点内容: PropertyEditor是什么? Spring为何选择基于PropertyEditor? Spring内建的那些PropertyEditor都有哪些,各自什么作用? PropertyEditor虽然已经很古老,不适合当下复杂环境。但不可否认它依旧有存在的价值,Spring内部也大量的仍在使用,因此不容忽视。下篇文章将深度探讨Spring内部是如何使用PropertyEditor的,赋予了它哪些机制,以及最终为何还是决定自己另起炉灶搞一套呢?欢迎对本系列保持持续关注~ 附:Spring主要版本发布时间和特性 2002-02,开始开发孵化此项目,项目名叫:interface21,它便就是Spring的前身 2004-03,1.0版发布。进入迅速发展期 2006-10,2.0版发布。支持可扩展的xml配置功能、支持Java5、支持动态语言、支持更多扩展点 2007-11,2.5版发布。项目名从此改为Spring Source,支持Java 6,支持注解配置(部分) 2009-12,3.0版发布。这是非常非常重要的一个版本,支持了模块驱动、支持SpEL、支持Java Bean配置、支持嵌入式数据库...... 2011和2012,这两年发布了非常多的3.x系列小版本,带来了很多惊喜,同时也让Spring更加扎实 2013-12,4.0版发布。这是有一次进步,提供了对Java 8的全面支持,支持Java EE 7、支持websocket、泛型依赖注入...... 2017-09,5.0版发布。最低JDK版本要求是Java 8,同时支持Servlet 3.1。当然最为重要的是引入了全新模块:WebFlux 截止到当前,Spring Framework的最新版本是5.3.x版本,此版本是5.x的最后一个主要功能分支,下个版本将是6.x喽,咱们拭目以待。 ✔推荐阅读: 1. 揭秘Spring类型转换 - 框架设计的基石 蚂蚁金服上市了,我不想努力了 如果程序员和产品经理都用凡尔赛文学对话...... 程序人生 | 春风得意马蹄疾,一日看尽长安花
仰不愧天,俯不愧人,内不愧心。关注公众号【BAT的乌托邦】,有Spring技术栈、MyBatis、JVM、中间件等小而美的原创专栏供以免费学习。分享、成长,拒绝浅尝辄止。本文已被 https://www.yourbatman.cn 收录。 ✍前言 你好,我是YourBatman。 Spring Framework是一个现代化的框架,俨然已发展成为Java开发的基石。随着高度封装、高度智能化的Spring Boot的普及,发现团队内越来越少的人知道其深层次机制,哪怕只有一点点。这是让Spirng团队开心,但却是让使用的团队比较担忧的现象。 若运行一个完全黑箱程序无疑像抱着一个定时炸弹,总是如履薄冰、战战兢兢。团队内需要这样的同学来为它保驾护航,惊爆之时方可泰然自诺。所以,你愿意pick吗? 本系列将讨论Spring Framework里贯穿其上下文,具有举足轻重地位的一个模块:类型转换(也可叫数据转换)。 ✍正文 Java是个多类型且强类型语言,类型转换这个概念对它来说并不陌生。比如: 自动类型转换(隐式):小类型 -> 大类型。eg:int a = 10; double b = a; 强制类型转换(显式):大类型 -> 小类型。eg:double a = 10.123; int b = (int)a; 说明:强转有可能产生精度丢失 调用API类型转换:常见的是字符串和其它类型的互转。eg:parseInt(String); parseBoolean(String); JSON.toJSONString(Obj); LocalDate.parse(String) 说明:API可能来自于JDK提供、一方库、二方库、三方库提供 在企业级开发环境中,会遇到更为复杂的数据转换场景,譬如说: 输入/传入一个规格字符串(如1,2,3,4),转换为一个数组 输入/传入一个JSON串(如{"name":"YourBatman","age":18}),转换为一个Person对象 输入/传入一个URL串(如:C:/myfile.txt、classpath:myfile.txt),转换为一个org.springframework.core.io.Resource对象 虽说数据输入/传入绝大部分都会是字符串(如Http请求信息、XML配置信息),但结构可以千差万别,那么这就必然会涉及到大量的数据类型、结构转换的逻辑。倘若这都需要程序员自己手动编码做转换处理,那会让人望而生畏甚至怯步。 还好我们有Spring。从本文起,A哥就帮你解密Spring Framework它是如何帮你接管类型转换,实现“自动化”的。有了此部分知识的储备,后续再讨论自动化数据绑定、自动化数据校验、Spring Boot松散绑定等,一切都变得容易接受得多。 说明:类型转换其实每个框架都会存在,其中Java领域以Spring的实现最为经典,学会后便可举一反三 Spring类型转换 Spring的类型转换也并非一步到位。完全掌握Spring的类型转换并非易事,需要有一定的脉络按步骤进行。本文作为类型转换系列第一篇文章,将绘制目录大纲,将从以下几个方面逐步展开讨论。 早期类型转换之PropertyEditor 早期的Spirng(3.0之前)类型转换是基于Java Beans接口java.beans.PropertyEditor来实现的(全部继承自PropertyEditorSupport): public interface PropertyEditor { ... // String -> Object void setAsText(String text) throws java.lang.IllegalArgumentException; // Object -> String String getAsText(); ... } 这类实现举例有: StringArrayPropertyEditor:,分隔的字符串和String[]类型互转 PropertiesEditor:键值对字符串和Properties类型互转 IntegerEditor:字符串和Integer类型互转 ... 基于PropertyEditor的类型转换作为一种古老的、遗留下来的方式,是具有一些设计缺陷的,如:职责不单一,类型不安全,只能实现String类型的转换等。虽然自Spring 3.0起提供了现代化的类型转换接口,但是此部分机制一直得以保留,保证了向下兼容性。 说明:Spring 3.0之前在Java领域还未完全站稳脚跟,因此良好的向下兼容显得尤为重要 这块内容将在本系列后面具体篇章中得到专题详解,敬请关注。 新一代类型转换接口Converter、GenericConverter 为了解决PropertyEditor作为类型转换方式的设计缺陷,Spring 3.0版本重新设计了一套类型转换接口,其中主要包括: Converter<S, T>:Source -> Target类型转换接口,适用于1:1转换 StringToPropertiesConverter:将String类型转换为Properties StringToBooleanConverter:将String类型转换为Boolean EnumToIntegerConverter:将Enum类型转换为Integer ConverterFactory<S, R>:Source -> R类型转换接口,适用于1:N转换 StringToEnumConverterFactory:将String类型转任意Enum StringToNumberConverterFactory:将String类型转为任意数字(可以是int、long、double等等) NumberToNumberConverterFactory:数字类型转为数字类型(如int到long,long到double等等) GenericConverter:更为通用的类型转换接口,适用于N:N转换 ObjectToCollectionConverter:任意集合类型转为任意集合类型(如List<String>转为List<Integer> / Set<Integer>都使用此转换器) CollectionToArrayConverter:解释基本同上 MapToMapConverter:解释基本同上 ConditionalConverter:条件转换接口。可跟上面3个接口组合使用,提供前置条件判断验证 重新设计的这套接口,解决了PropertyEditor做类型转换存在的所有缺陷,且具有非常高的灵活性和可扩展性。但是,每个接口独立来看均具有一定的局限性,只有使用组合拳方才有最大威力。当然喽,这也造成学习曲线变得陡峭。据我了解,很少有同学搞得清楚新的这套类型转换机制,特别容易混淆。倘若你掌握了是不是自己价值又提升了呢?不信你细品? 这块内容将在本系列后面具体篇章中得到专题详解,敬请关注。 新一代转换服务接口:ConversionService 从上一小节我们知道,新的这套接口中,Converter、ConverterFactory、GenericConverter它们三都着力于完成类型转换。对于使用者而言,如果做个类型转换需要了解到这三套体系无疑成本太高,因此就有了ConversionService用于整合它们三,统一化接口操作。 此接口也是Spring 3.0新增,用于统一化 底层类型转换实现的差异,对外提供统一服务,所以它也被称作类型转换的门面接口,从接口名称xxxService也能看出来其设计思路。它主要有两大实现: GenericConversionService:提供模版实现,如转换器的注册、删除、匹配查找等,但并不内置转换器实现 DefaultConversionService:继承自GenericConversionService。在它基础上默认注册了非常多的内建的转换器实现,从而能够实现绝大部分的类型转换需求 ConversionService转换服务它贯穿于Spring上下文ApplicationContext的多项功能,包括但不限于:BeanWrapper处理Bean属性、DataBinder数据绑定、PropertySource外部化属性处理等等。因此想要进一步深入了解的话,ConversionService是你绕不过去的坎。 说明:很多小伙伴问WebConversionService是什么场景下使用?我说:它并非Spirng Framework的API,而属于Spring Boot提供的增强,且起始于2.x版本,这点需引起注意 这块内容将在本系列后面具体篇章中得到专题详解,敬请关注。 类型转换整合格式化器Formatter Spring 3.0还新增了一个Formatter<T>接口,作用为:将Object格式化为类型T。从语义上理解它也具有类型转换(数据转换的作用),相较于Converter<S,T>它强调的是格式化,因此一般用于时间/日期、数字(小数、分数、科学计数法等等)、货币等场景,举例它的实现: DurationFormatter:字符串和Duration类型的互转 CurrencyUnitFormatter:字符串和javax.money.CurrencyUnit货币类型互转 DateFormatter:字符串和java.util.Date类型互转。这个就使用得太多了,它默认支持什么格式?支持哪些输出方式,这将在后文详细描述 ...... 为了和类型转换服务ConversionService完成整合,对外只提供统一的API。Spring提供了FormattingConversionService专门用于整合Converter和Formatter,从而使得两者具有一致的编程体验,对开发者更加友好。 这块内容将在本系列后面具体篇章中得到专题详解,敬请关注。 类型转换底层接口TypeConvert 定义类型转换方法的接口,它在Spring 2.0就已经存在。在还没有ConversionService之前,它的类型转换动作均委托给已注册的PropertyEditor来完成。但自3.0之后,这个转换动作可能被PropertyEditor来做,也可能交给ConversionService处理。 它一共提供三个重载方法: // @since 2.0 public interface TypeConverter { // value:待转换的source源数据 // requiredType:目标类型targetType // methodParam:转换的目标方法参数,主要为了分析泛型类型,可能为null // field:目标的反射字段,为了泛型,可能为null <T> T convertIfNecessary(Object value, Class<T> requiredType) throws TypeMismatchException; <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParameter methodParam) throws TypeMismatchException; <T> T convertIfNecessary(Object value, Class<T> requiredType, Field field) throws TypeMismatchException; } 它是Spring内部使用类型转换的入口,最终委托给PropertyEditor或者注册到ConversionService里的转换器去完成。它的主要实现有: TypeConverterSupport:@since 3.2。继承自PropertyEditorRegistrySupport,它主要是为子类BeanWrapperImpl提供功能支撑。作用有如下两方面: 提供对默认编辑器(支持JDK内置类型的转换如:Charset、Class、Class[]、Properties、Collection等等)和自定义编辑器的管理(PropertyEditorRegistry#registerCustomEditor) 提供get/set方法,把ConversionService管理上(可选依赖,可为null) 数据绑定相关:因为数据绑定强依赖于类型转换,因此数据绑定涉及到的属性访问操作将会依赖于此组件,不管是直接访问属性的DirectFieldAccessor还是功能更强大的BeanWrapperImpl均是如此 总的来说,TypeConverter能把类型的各种实现、API收口于此,Spring把类型转换的能力都转嫁到TypeConverter这个API里面去了。虽然方便了使用,但其内部实现原理稍显复杂,同样的这块内容将在本系列后面具体篇章中得到专题详解,敬请关注。 Spring Boot使用增强 在传统Spring Framework场景下,若想使用ConversionService还得手动档去配置,这对于不太了解其运行机制的同学无疑是有使用门槛的。而在Spring Boot场景下这一切都会变得简单许多,可谓使用起来愈发方便了。 另外,Spring Boot在内建转换器的基础上额外扩展了不少实用转换器,形如: StringToFileConverter:String -> File NumberToDurationConverter: DelimitedStringToCollectionConverter: ...... ✍总结 基于配置来控制程序运行总比你修改程序代码来得更优雅、更富弹性,但这是需要依赖于数据绑定、数据校验等功能的,而它们又依赖于类型转换。 虽说几乎所有的框架都会有类型转换的功能模块,但Spring的可能是最为通用、最为经典的存在。因此本系列专题讲解Spring Framework的类型转换,旨在能够帮你你撬开通往跃升的大门,节节攀高。 ✔推荐阅读: Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容) 如果程序员和产品经理都用凡尔赛文学对话...... Spring Framework 5.3.0正式发布,在云原生路上继续发力 Spring改变版本号命名规则:此举对非英语国家很友好 JDK15正式发布,划时代的ZGC同时宣布转正 IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 Spring Boot 2.3.0正式发布:优雅停机、配置文件位置通配符新特性一览 搞事情?Spring Boot今天一口气发布三个版本
千里之行,始于足下。关注公众号【BAT的乌托邦】,有Spring技术栈、MyBatis、JVM、中间件等小而美的原创专栏供以免费学习。分享、成长,拒绝浅尝辄止。本文已被 https://www.yourbatman.cn 收录。 ✍前言 你好,我是YourBatman。 北京时间2020-11-12,Spring Boot 2.4.0正式发布。2.4.0是第一个使用新版本方案的Spring Boot发行版本。 注意:2.4.0版本号没有.RELEASE后缀,没有.RELEASE后缀,没有.RELEASE后缀。使用的是Spring最新的版本发布规则。此规则详解请参考上篇文章:Spring改变版本号命名规则:此举对非英语国家很友好 还记得Spring Boot 2.3.0.RELEASE版本发布时那会麽?前后相差将好半年:直达电梯:Spring Boot 2.3.0正式发布:优雅停机、配置文件位置通配符新特性一览 一般来说,次版本号的升级会有点料,根据之前的爆料此次升级据说是做了大量的更新和改进。那么老规矩,作为小白鼠的我先代你玩一玩,初体验吧。 也可参见官方的更新日志:Spring Boot 2.4.0 Release Notes ✍正文 除了刚发布的Spring Boot 2.4.0,Spring Boot 2.3.x/2.2.x仍旧是活跃的维护的版本。Spring Boot遵循的是Pivotal OSS支持策略,从发布日期起支持主要版本3年(注意:是主要版本)。下面是详情: 2.3.x:支持的版本。2020.05发布,是现在的活跃的主干 2.2.x:支持的版本。2019.10发布,是现在的活跃的主干 2.1.x:2018.10发布,支持到2020.10月底,建议尽快升级 EOL分支: 2.0.x:2018.3发布,2019.4.3停止维护 1.5.x:生命已终止的版本。2017.1发布,是最后一个1.x分支,2019.8.1停止维护 回忆2.3版本的新特性 可能大部分小伙伴都还没用过2.3.x分支,没想到2.4.x就已发布。因此这里先对2.3.x版本的新特性,来波简单回忆: 优雅停机。这是2.3.x主打的新特性:在关闭时,web服务器将不再允许新的请求,并将等待完成的请求给个宽限期让它完成。这个宽限期是可以设置的:可以使用spring.lifecycle.timeout-per-shutdown-phase=xxx来配置,默认值是30s。 配置文件位置支持通配符。简单的说,如果你有MySql的配置和Redis配置的话,你就可以把他们分开来放置,这个新特性也是棒棒哒。隔离性更好目录也更加清晰了(注意:此格式只支持放在classpath外部): mysql:/config/mysql/application.properties redis:/config/redis/application.properties 核心依赖升级。 Spring Data Neumann。备注:很明显这个还是旧的命名方式。在Spirng新的版本规则下,Spring Data最新版本为Spring Data 2020.0.0 Spring Session Dragonfruit(很明显这个也还是旧的命名方式) Spring Security 5.3 Spring Framework 没有升级,使用的依旧是和Spring Boot 2.2相同的5.2.x版本 说明:小版本号的升级对于新特性来说一般选择性忽略 关于Bean Validation:从此版本开始,spring-boot-starter-web不会再把validation带进来,所以若使用到,你需要自己添加这个spring-boot-starter-validation依赖 一般来说建议你手动引入,毕竟Bean Validation的使用还是很广泛,并且真的非常非常好用 做足功课后,就开始最新的Spring Boot 2.4.0之旅吧。 2.4.0主要新特性 全新的配置文件处理(properties/yaml) 这个改变最为重磅,本次改变了配置文件的加载逻辑,旨在简化和合理化外部配置的加载方式,它可能具有不向下兼容性。 Spring Boot 2.4改变了处理application.properties和application.yml文件的方式: 若你只是简单的文件application.properties/yaml,那么升级对你是无缝的,你感受不到任何变化 若你使用了比较复杂的文件,如application-profile.properties/yaml这种(或者使用了Spirng Cloud的配置中心、(带有分隔符----的)多yaml文件),那么默认是不向下兼容的,需要你显式的做出些更改 因为配置文件隶属于程序的一部分,特别是我们现在几乎都会使用到配置中心。因此下面针对于老版本升级到Spring Boot 2.4.0做个简单的迁移指导。 说明:因配置文件加载逻辑完全进行了重写,因此详细版本我放到了下文专文讲解,有兴趣可保持关注 老版本版本配置属性迁移指南 老版本:2.4.0之前的版本都叫老版本。 Spring Boot 2.4对application.poperties/yaml的处理做了更新/升级。旨在简化和合理化外部配置的加载方式。它还提供了新功能:spring.config.import支持。所以呢,对于Spring Boot 2.4.0之前的版本(老版本)若升级到2.4.0需要做些修改,指导建议如下: 方式一:恢复旧模式(不推荐) 如果你还未准备好做配置迁移的修改,Spring Boot也帮你考虑到了,提供了一键切换到旧模式的“按钮”。具体做法是:只需要在Environment里增加一个属性spring.config.use-legacy-processing = true就搞定。最简的方式就是把这个属性放在application.poperties/yaml里即可。 spring.config.use-legacy-processing = true 增加此配置后,Spring Boot对配置文件的解析恢复到原来模式:仍旧使用ConfigFileApplicationListener去解析。 ConfigFileApplicationListener属于Spring Boot非常核心的底层代码,这次做了不向下兼容的改进,可见它对进击云原生的决心 值得注意的是:此API在2.4.0已被标记为过期: // @since 1.0.0 // @deprecated since 2.4.0 in favor of {@link ConfigDataEnvironmentPostProcessor} @Deprecated public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered { ... } 按照Spring Boot的版本策略,此类将在Spring Boot 2.6.0版本被移除。因此:若不是迫不得已(时间紧急),并不建议你用兼容手法这么去做,因为这将成为技术债,迟早要还的。 说明:很多RD其实只会看到当前的方便,获得利益(比如快速上线获奖),坑交给后人。我个人认为作为程序员应该有一定自我修养,自我追求,不为一时的爽而持续给团队积累债务,毕竟积重难返。 方式二:按新规则迁移(推荐) 若你对配置文件的使用有如下情行,那么你需要做迁移: 多文档的yaml文件(带有----分隔符的文件) 在Jar外使用配置文件,或者使用形如application-{xxx}.properties/yaml这种配置 若在多文档yaml中使用到了spring.profiles配置项 ... Spring Boot 2.4.0升级对配置文件的改动是最大的,并且还不具备向下兼容性,简单的说就是从此版本开始要把Spring Boot的配置文件加载机制重学一遍(比如还增加了spring.config.import,增加了对kubernetes配置的支持等等),并且还要学会如何迁移。 为了更好的描述好这个非常非常重要的知识点,下篇文章我会用专文来全面介绍 Spring Boot这套全新的配置文件加载机制,并且辅以原理,以及和过去方式的比较,帮助你更全面、更快速、更劳的掌握它,欢迎持续关注。 说明:Spring Boot的配置文件加载机制非常非常重要,因为你也知道你平时开发中很大程度实际上是在跟它的配置项打交道。新的配置加载方式比老的更加优秀,适应发展,敬请期待 从spring-boot-starter-test中删除Vintage Engine Spring Boot 2.2.0版本开始就引入JUnit 5作为单元测试默认库,在此之前,spring-boot-starter-test包含的是JUnit 4的依赖,Spring Boot 2.2.0版本之后替换成了Junit Jupiter(Junit5)。 Vintage Engine属于Junit5的一个模块,它的作用是:允许用JUnit 5运行用JUnit 4编写的测试,从而提供了向下兼容的能力。 从2.2.0到现在经过了2个版本的迭代,到Spring Boot 2.4.0这个版本决定了把Vintage Engine从spring-boot-starter-test正式移除。因此:若你的工程仍需要对JUnit4支持,那么请手动引入依赖项(如果工程量不大,强烈建议使用JUnit5,比4好用太多): <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependency> 说明:其实在2.4.0之前,若你是从https://start.spring.io生成的项目其实也是不会带有vintage-engine的。只不过它是通过显式的在pom里通过exclusion标签来排除的 嵌入式数据库检测 改进嵌入式数据库检测机制:仅当数据库在内存中时才将其视为嵌入式数据库。所以如果使用H2、HSQL等产品,但是你是基于文件的持久性或使用的是服务器模式,那么将不会检测为内存数据库。而对于非内存数据库,你可能需要额外做如下动作: sa用户名将不会再被主动设置。所以如果你的数据库需要用户名,请增加配置项:spring.datasource.username = sa 这种数据库将不会再被自动初始化,若要使用请根据需要更改spring.datasource.initialization-mode的值 Logback配置属性 Logback一些配置项改名了,更加表名了它是logback的配置项。 新增了配置类LogbackLoggingSystemProperties用于对应,它继承自之前的LoggingSystemProperties 之前的配置项有些被废弃(此版本还未删除,后续版本肯定会删除的),对应关系如下: 老(已废弃) 新 logging.pattern.rolling-file-name logging.logback.rollingpolicy.file-name-pattern logging.file.clean-history-on-start logging.logback.rollingpolicy.clean-history-on-start logging.file.max-size logging.logback.rollingpolicy.max-file-size logging.file.total-size-cap logging.logback.rollingpolicy.total-size-cap logging.file.max-history logging.logback.rollingpolicy.max-history 一些属性是被放到system environment里面的: 老(已废弃) 新 ROLLING_FILE_NAME_PATTERN LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN LOG_FILE_CLEAN_HISTORY_ON_START LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START LOG_FILE_MAX_SIZE LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE LOG_FILE_TOTAL_SIZE_CAP LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP LOG_FILE_MAX_HISTORY LOGBACK_ROLLINGPOLICY_MAX_HISTORY 不再注册DefaultServlet 从Spring Boot 2.4开始,默认将不会再注册DefaultServlet。因为在绝大多数的应用中,Spring MVC提供的DispatcherServlet是唯一需要被注册的Servlet。从源码处感受下这次改动: AbstractServletWebServerFactory: // 2.4.0之前版本,默认值是true private boolean registerDefaultServlet = true; // 2.4.0以及之后版本,默认值是false private boolean registerDefaultServlet = false; 当然喽,若你的工程强依赖于此Servelt,那么可以通过此配置项server.servlet.register-default-servlet = true把它注册上去。 补课:什么是DefaultServlet? 它是Java EE提供的标准技术,如Tomcat、Jetty等都提供了这个类。简而言之它的作用就是兜底(拦截/),当别的servlet都没匹配上时就交给它来处理,一般用于处理静态资源如.jpg,.html,.js这类的静态文件。 DefaultServlet在传统web容器里,会被配置在tomcat目录(此处以tomcat为例)下的conf/web.xml里: <servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>listings</param-name> <param-value>false</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> 说明:tomcat下的web.xml对其加载的所有的Application都生效,并且最终和Application自己的web.xml内容合并,遇相同的话后者优先级更高 在Spring Boot 嵌入式容器里配置是这样的(完全等价于xml配置): private void addDefaultServlet(Context context) { Wrapper defaultServlet = context.createWrapper(); defaultServlet.setName("default"); defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); defaultServlet.addInitParameter("debug", "0"); defaultServlet.addInitParameter("listings", "false"); defaultServlet.setLoadOnStartup(1); // Otherwise the default location of a Spring DispatcherServlet cannot be set defaultServlet.setOverridable(true); context.addChild(defaultServlet); context.addServletMappingDecoded("/", "default"); } 值得注意的是:Spring Boot注册的DispatcherServlet的path也是/(覆盖掉了DefaultServelt)。在Spring MVC环境下倘若是静态资源,也不用DefaultServelt费心,Spring MVC专门提供了一个DefaultServletHttpRequestHandler用于处理静态资源(虽然最终还是Dispatcher给defaultServlet去搞定)。 现在的Spring Boot服务大都是REST服务,并无静态资源需要提供,因此就没有必要启用DefaultServletHttpRequestHandler和注册DefaultServlet来增加不必要的开销喽。 HTTP traces不再包含cookie头 Http traces默认将不再包含请求头Cookie以及响应头Set-Cookie。源码处感受一下: org.springframework.boot.actuate.trace.http.Include: // 2.4.0版本之前:包含COOKIE_HEADERS这个头 static { Set<Include> defaultIncludes = new LinkedHashSet<>(); defaultIncludes.add(Include.REQUEST_HEADERS); defaultIncludes.add(Include.RESPONSE_HEADERS); defaultIncludes.add(Include.COOKIE_HEADERS); defaultIncludes.add(Include.TIME_TAKEN); DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes); } // 2.4.0版本以及之后:不包含COOKIE_HEADERS这个头 static { Set<Include> defaultIncludes = new LinkedHashSet<>(); defaultIncludes.add(Include.REQUEST_HEADERS); defaultIncludes.add(Include.RESPONSE_HEADERS); defaultIncludes.add(Include.TIME_TAKEN); DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes); } 若你仍旧想保留老的习惯,那么请用配置项management.trace.http.include = cookies, errors, request-headers, response-headers自行控制。 Neo4j 这个版本对Neo4j的支持进行了重大调整。直接用源码来说明差异: Spring Boot 2.4.0之前版本: @ConfigurationProperties(prefix = "spring.data.neo4j") public class Neo4jProperties implements ApplicationContextAware { ... } // 无Neo4jDataProperties配置类 Spring Boot 2.4.0以及之后版本: @ConfigurationProperties(prefix = "spring.neo4j") public class Neo4jProperties { ... } @ConfigurationProperties(prefix = "spring.data.neo4j") public class Neo4jDataProperties { ... } 其它升级关注点 Spring Framework 5.3:Spring Boot 2.4.0使用的是5.3.0主线分支(之前使用的5.2.x或更低) Spring Framework 5.3的新特性应该重点关注,请移步我上篇文章:Spring Framework 5.3.0正式发布,在云原生路上继续发力 Spring Data 2020.0:Spring Boot 2.4.0使用的是最新发布的Spring Data 2020.0 此版本的命名方式不同于之前,是因为使用了Spirng最新的release train命名方式。Spring在2020年4月份发布了最新的版本命名方式,可参考前面这篇文章:Spring改变版本号命名规则:此举对非英语国家很友好 支持Java 15:此版本的Spring Boot完全支持Java 15,最小支持依旧是Java 8 自定义属性名支持:当使用构造函数绑定时,属性的名称需要和参数名称保持一样。如果您想使用Java保留关键字,这可能是一个问题。如下例子: @ConfigurationProperties(prefix = "sample") public class SampleConfigurationProperties { private final String importValue; // import是Java关键字 public SampleConfigurationProperties(@Name("import") String importValue) { this.importValue = importValue; } } @Name注解是Spring Boot 2.4.0新增的注解,能标注在ElementType.PARAMETER上 支持导入无扩展名的配置文件:如果您有这样的需求,现在就可以通过向Spring Boot引导提供关于内容类型的提示来导入这些文件 此版本对Spring Boot的配置文件加载进行了完全重新改造,并且不向下兼容,具体参见下篇文章 新增StartupEndpoint:显示有关应用程序启动的信息。此端点可以帮助您识别启动时间超过预期的bean 此端点依赖于Spring Framework 5.3.0新提供的应用启动追踪新特性。具体可参考ApplicationStartup和StartupStep这个两个API是如何做追踪的 新增RedisCacheMetrics:用于监控使用redis时的puts、gets、deletes以及缓存命中率等信息 此指标信息默认不开启,需你增加配置spring.cache.redis.enable-statistics = true 新增些Web配置项:spring.web.locale、spring.web.locale-resolver、spring.web.resources.*、management.server.base-path,这些属性既支持Servlet也支持WebFlux 对应的只能用于 Spring MVC或servelt下配置项spring.mvc.locale/spring.mvc.locale-resolver/spring.resources.*/management.server.servlet.context-path均以标注为过期 支持Flyway 7:这个版本升级到Flyway 7,带来了一些额外的属性。如:spring.flyway.url/user/password(开源版本);spring.flyway.cherry-pick/jdbc-properties...(团队版本) H2数据库控制台支持配置密码:可通过spring.h2.console.settings.web-admin-password属性配置通过密码访问H2控制台 增强的错误分析器FailureAnalizers:现在即使你还没有创建ApplicationContext,FailureAnalizers都会生效来帮你定位错误位置 处理/标注Spring Boot 2.2和2.3中过期项:按照Spring Boot的版本兼容性政策,在2.2版本已被标记为@Deprecated的在2.4.0版本会被删除,在2.3版本中被标记为@Deprecated的计划在2.5.0版本中将其移除 ✍总结 这是A哥奉给大家的,对Spring Boot2.4.0版本新特性的介绍,希望对你有些帮助。 Spring Boot 2.4.0版本的升级目标,基本和Spring Framework 5.3.0保持一致:为云原生做努力。表现在除了删除些无用类,禁止不需要的类的加载外,重点还会体现在它对配置文件加载机制的重构上,这将是下文的内容,也是本次升级的重头戏,敬请关注。 Spring Boot重写了对配置文件的加载机制,并且新引入了近40个类来处理(老方式仅有区区几个类),可见其重视、重要程度。因此,为了适应未来的发展,你一定要掌握,并且越早越好,下篇将为你揭晓。 ✔推荐阅读: 如果程序员和产品经理都用凡尔赛文学对话...... Spring Framework 5.3.0正式发布,在云原生路上继续发力 Spring改变版本号命名规则:此举对非英语国家很友好 JDK15正式发布,划时代的ZGC同时宣布转正 IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 Spring Boot 2.3.0正式发布:优雅停机、配置文件位置通配符新特性一览 搞事情?Spring Boot今天一口气发布三个版本
今天搬砖不狠,明天地位不稳。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 本文是上篇文章的续篇,个人建议可先花3分钟移步上篇文章浏览一下:5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类 很多人说Bean Validation只能验证单属性(单字段),但我却说它能完成99.99%的Bean验证,不信你可继续阅读本文,能否解你疑惑。 版本约定 Bean Validation版本:2.0.2 Hibernate Validator版本:6.1.5.Final ✍正文 本文接上文叙述,继续介绍Bean Validation声明式验证四大级别中的:容器元素验证(自定义容器类型)以及类级别验证(也叫多字段联合验证)。 据我了解,很多小伙伴对这部分内容并不熟悉,遇到类似场景往往被迫只能是一半BV验证 + 一半事务脚本验证的方式,显得洋不洋俗不俗。 本文将给出具体案例场景,然后统一使用BV来解决数据验证问题,希望可以帮助到你,给予参考之作用。 自定义容器类型元素验证 通过上文我们已经知道了Bean Validation是可以对形如List、Set、Map这样的容器类型里面的元素进行验证的,内置支持的容器虽然能cover大部分的使用场景,但不免有的场景依旧不能覆盖,而且这个可能还非常常用。 譬如我们都不陌生的方法返回值容器Result<T>,结构形如这样(最简形式,仅供参考): @Data public final class Result<T> implements Serializable { private boolean success = true; private T data = null; private String errCode; private String errMsg; } Controller层用它包装(装载)数据data,形如这样: @GetMapping("/room") Result<Room> room() { ... } public class Room { @NotNull public String name; @AssertTrue public boolean finished; } 这个时候希望对Result<Room>里面的Room进行合法性验证:借助BV进行声明式验证而非硬编码。希望这么写就可以了:Result<@Notnull @Valid LoggedAccountResp>。显然,缺省情况下即使这样声明了约束注解也是无效的,毕竟Bean Validation根本就“不认识”Result这个“容器”,更别提验证其元素了。 好在Bean Validation对此提供了扩展点。下面我将一步一步的来对此提供实现,让验证优雅再次起来。 自定义一个可以从Result<T>里提取出T值的ValueExtractor值提取器 Bean Validation允许我们对自定义容器元素类型进行支持。通过前面这篇文章:4. Validator校验器的五大核心组件,一个都不能少 知道要想支持自定义的容器类型,需要注册一个自定义的ValueExtractor用于值的提取。 /** * 在此处添加备注信息 * * @author yourbatman * @site https://www.yourbatman.cn * @date 2020/10/25 10:01 * @see Result */ public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> { @Override public void extractValues(Result<?> originalValue, ValueReceiver receiver) { receiver.value(null, originalValue.getData()); } } 将此自定义的值提取器注册进验证器Validator里,并提供测试代码: 把Result作为一个Filed字段装进Java Bean里: public class ResultDemo { public Result<@Valid Room> roomResult; } 测试代码: public static void main(String[] args) { Room room = new Room(); room.name = "YourBatman"; Result<Room> result = new Result<>(); result.setData(room); // 把Result作为属性放进去 ResultDemo resultDemo = new ResultDemo(); resultDemo.roomResult = result; // 注册自定义的值提取器 Validator validator = ValidatorUtil.obtainValidatorFactory() .usingContext() .addValueExtractor(new ResultValueExtractor()) .getValidator(); ValidatorUtil.printViolations(validator.validate(resultDemo)); } 运行测试程序,输出: roomResult.finished只能为true,但你的值是: false 完美的实现了对Result“容器”里的元素进行了验证。 小贴士:本例是把Result作为Java Bean的属性进行试验的。实际上大多数情况下是把它作为方法返回值进行校验。方式类似,有兴趣的同学可自行举一反三哈 在此弱弱补一句,若在Spring Boot场景下你想像这样对Result<T>提供支持,那么你需要自行提供一个验证器来覆盖掉自动装配进去的,可参考ValidationAutoConfiguration。 类级别验证(多字段联合验证) 约束也可以放在类级别上(也就说注解标注在类上)。在这种情况下,验证的主体不是单个属性,而是整个对象。如果验证依赖于对象的几个属性之间的相关性,那么类级别约束就能搞定这一切。 这个需求场景在平时开发中也非常常见,比如此处我举个场景案例:Room表示一个教室,maxStuNum表示该教室允许的最大学生数,studentNames表示教室里面的学生们。很明显这里存在这么样一个规则:学生总数不能大于教室允许的最大值,即studentNames.size() <= maxStuNum。如果用事务脚本来实现这个验证规则,那么你的代码里肯定穿插着类似这样的代码: if (room.getStudentNames().size() > room.getMaxStuNum()) { throw new RuntimeException("..."); } 虽然这么做也能达到校验的效果,但很明显这不够优雅。期望这种case依旧能借助Bean Validation来优雅实现,下面我来走一把。 相较于前面但字段/属性验证的使用case,这个需要验证的是整个对象(多个字段)。下面呀,我给出两种实现方式,供以参考。 方式一:基于内置的@ScriptAssert实现 虽说Bean Validation没有内置任何类级别的注解,但Hibernate-Validator却对此提供了增强,弥补了其不足。@ScriptAssert就是HV内置的一个非常强大的、可以用于类级别验证注解,它可以很容易的处理这种case: @ScriptAssert(lang = "javascript", alias = "_", script = "_.maxStuNum >= _.studentNames.length") @Data public class Room { @Positive private int maxStuNum; @NotNull private List<String> studentNames; } @ScriptAssert支持写脚本来完成验证逻辑,这里使用的是javascript(缺省情况下的唯一选择,也是默认选择) 测试用例: public static void main(String[] args) { Room room = new Room(); ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(room)); } 运行程序,抛错: Caused by: <eval>:1 TypeError: Cannot get property "length" of null at jdk.nashorn.internal.runtime.ECMAErrors.error(ECMAErrors.java:57) at jdk.nashorn.internal.runtime.ECMAErrors.typeError(ECMAErrors.java:213) ... 这个报错意思是_.studentNames值为null,也就是room.studentNames字段的值为null。 what?它头上不明明标了@NotNull注解吗,怎么可能为null呢?这其实涉及到前面所讲到的一个小知识点,这里提一嘴:所有的约束注解都会执行,不存在短路效果(除非校验程序抛异常),只要你敢标,我就敢执行,所以这里为嘛报错你懂了吧。 小贴士:@ScriptAssert对null值并不免疫,不管咋样它都会执行的,因此书写脚本时注意判空哦 当然喽,多个约束之间的执行也是可以排序(有序的),这就涉及到多个约束的执行顺序(序列)问题,本文暂且绕过。例子种先给填上一个值,后续再专文详解多个约束注解执行序列问题和案例剖析。 修改测试脚本(增加一个学生,让其不为null): public static void main(String[] args) { Room room = new Room(); room.setStudentNames(Collections.singletonList("YourBatman")); ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(room)); } 再次运行,输出: 执行脚本表达式"_.maxStuNum >= _.studentNames.length"没有返回期望结果,但你的值是: Room(maxStuNum=0, studentNames=[YourBatman]) maxStuNum必须是正数,但你的值是: 0 验证结果符合预期:0(maxStuNum) < 1(studentNames.length)。 小贴士:若测试脚本中增加一句room.setMaxStuNum(1);,那么请问结果又如何呢? 方式二:自定义注解方式实现 虽说BV自定义注解前文还暂没提到,但这并不难,因此这里先混个脸熟,也可在阅读到后面文章后再杀个回马枪回来。 自定义一个约束注解,并且提供约束逻辑的实现 @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = {ValidStudentCountConstraintValidator.class}) public @interface ValidStudentCount { String message() default "学生人数超过最大限额"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } public class ValidStudentCountConstraintValidator implements ConstraintValidator<ValidStudentCount, Room> { @Override public void initialize(ValidStudentCount constraintAnnotation) { } @Override public boolean isValid(Room room, ConstraintValidatorContext context) { if (room == null) { return true; } boolean isValid = false; if (room.getStudentNames().size() <= room.getMaxStuNum()) { isValid = true; } // 自定义提示语(当然你也可以不自定义,那就使用注解里的message字段的值) if (!isValid) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("校验失败xxx") .addPropertyNode("studentNames") .addConstraintViolation(); } return isValid; } } 书写测试脚本 public static void main(String[] args) { Room room = new Room(); room.setStudentNames(Collections.singletonList("YourBatman")); ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(room)); } 运行程序,输出: maxStuNum必须是正数,但你的值是: 0 studentNames校验失败xxx,但你的值是: Room(maxStuNum=0, studentNames=[YourBatman]) 完美,完全符合预期。 这两种方式都可以实现类级别的验证,它俩可以说各有优劣,主要体现在如下方面: @ScriptAssert是内置就提供的,因此使用起来非常的方便和通用。但缺点也是因为过于通用,因此语义上不够明显,需要阅读脚本才知。推荐少量(非重复使用)、逻辑较为简单时使用 自定义注解方式。缺点当然是“开箱使用”起来稍显麻烦,但它的优点就是语义明确,灵活且不易出错,即使是复杂的验证逻辑也能轻松搞定 总之,若你的验证逻辑只用一次(只一个地方使用)且简单(比如只是简单判断而已),推荐使用@ScriptAssert更为轻巧。否则,你懂的~ ✍总结 如果说能熟练使用Bean Validation进行字段、属性、容器元素级别的验证是及格60分的话,那么能够使用BV解决本文中几个场景问题的话就应该达到优秀级80分了。 本文举例的两个场景:Result<T>和多字段联合验证均属于平时开发中比较常见的场景,如果能让Bean Validation介入帮解决此类问题,相信对提效是很有帮助的,说不定你还能成为团队中最靓的仔呢。 ✔推荐阅读: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸 4. Validator校验器的五大核心组件,一个都不能少 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类
要想改变命运,首先改变自己。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 还记得在今年5月份样子看到了一篇来自Pivotal的邮件,大致内容是说Spring改变了版本号的命名规则,当时本着先收藏一下准备晚上再看,然后,就没有然后了。 直到前些天突然看到了篇标题为:Spring Data 2020.0.0正式发布的文章,这才让我把此事联想了起来,因此才决定写此文记录一下,顺带分享给你。 若你已苦于Spring Cloud的版本号命名方式,那么本文给你带来了曙光 ✍正文 天下苦Spring Cloud版本命名久矣。在正式开始之前,管生管养的A哥有意对这其中的相关名词进行解释,方便理解本文。 Release Train Release Train直译过来意思为:发版火车/火车发版。火车大家不陌生,它有一个显著的特点:定时定点发车。这里的发车在软件领域就等同于软件的发版。 为何需要Release Train发版模式? 在公司还很小很小的时候,整个公司可能只有一个软件,版本发布非常的简单,没什么需要协调的,发就完了。但是,一旦公司快速发展变得比较大后,核心产品功能数以十、百计,各功能模块由不同的团队负责,沟通成本明显升高,单单在版本上稍不注意就会产生各种问题,很容易给人一种“乱如麻”的感觉。 使用Release Train的发版模式就能很大程度上避免这些问题,可以这样做:规定每个月的最后一天(精确的发版日期)需要发一版(类比于火车发车),那么就可以以这个时间点为deadline,参与的的各方包括产品经理、RD、QA等等都提前沟通好需求内容,并做好计划,充分做好统一发车的准备。在这期间,如果中间某一团队出现问题跟不上节奏了,那么请及时下车(前提是控制好下车的影响面),不要影响整体发车时间点。 总的来讲:火车是按点准时出发的,各方应按点上车,倘若本次赶不上车的那么就请等下一趟车。通过这种方式可以确保软件产品的持续迭代,保证产品的稳定性,这就是Release Train发版模式。 在实际的软件产品中,可以认为稍微大一点的软件都是按照此模式来持续迭代的,比如IOS、maxOS、MIUI、Spring Cloud等等。这些软件版本在命名方式上不同但均遵循一定规律: IOS 14、IOS 14.1、IOS14.1.1 macOS Mojave、macOS Sierra Spring Cloud Greenwich、Spring Cloud Hoxton Project Module 如果说按照Release Train发版模式发出的一个版本代表着一个大的产品版本号,那么Project Module就代表其内部的模块。一般一个软件产品由N多个模块组成,以最新的Spring Data 2020.0.0版本为例,内含有多个Project Module模块: Spring Data Commons 2.4 Spring Data JDBC 2.1 Spring Data JPA 2.4 Spring Data MongoDB 3.1 Spring Data KeyValue 2.4 Spring Data LDAP 2.4 Spring Data Elasticsearch 4.1 ... Semantic Versioning 语义化版本号,有被称作语义化版本控制规范,简称“SemVer”。它是一套版本号规则的标准/规范,用于改善软件版本号格式混乱问题,顺便统一一下版本号所表达的含义。官方主页是:https://semver.org 版本号组成 SemVer版本号主要由三个部分组成,每个部分是一个非负整数,部分和部分之间用.分隔:主版本号.次版本号.修订号(简写为x.y.z)。下面对这三部分做出解释(约定): 主版本号:只有进行非向下兼容的修改或者颠覆性的更新时,主版本号加1 话外音:改变很大,暴力式更改 次版本号:进行向下兼容的修改或者添加兼容性的新功能时,次版本号加1 话外音:改变不很大,一般是向下兼容的。值得注意的是:这里指的是一般,有些情况也存在不兼容情况也是允许的,当然不能是主要功能不兼容 补丁号:没有新功能加入,一般修复bug、优化代码等,补丁号加1 话外音:此版本号可放心无缝升级 关于这三部分还有两点值得注意: 版本号均从0开始(包括主版本号) 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版 1.0.0 的版本号用于界定公共 API 的形成。也就说从 当主版本号更新时,次版本号和补丁号都要归零;次版本号更新时补丁号归零 版本号比较 这种三段式的版本号是可以比较大小的。比较的顺序是:主版本号、次版本号、补丁号。举例:4.3.0 < 5.0.0 < 5.0.3 < 5.1.0 说明:使用.分隔开的话,正常比较(当字符串比较)是不会出现形如.2. > .10.的问题的 值得注意的是,Semantic Versioning只是一个标准,它并没有提供实现(比如版本号比较),虽然按照此规则自己实现一个并不复杂,但我建议各位不要自己实现,毕竟这种轮子社区里大把的,各种语言的都有哦,何必重复造呢。 Calendar Versioning 日历化版本,简称CalVer。CalVer不是基于任意数字,而是基于项目发布日期的版本控制约定。相较于语义化版本号,日历化版本号更接地气,显得活力更强些。因为日期是单向向前的,因此版本随着时间的推移会变得更好。 方案类别 有多种日历化版本方案,长期被各种大小项目使用。对于CalVer来说,它的规范非常抽象,毕竟发布日期本就是一个很抽象的概念嘛。 CalVer 并未像 “语义化版本” 那样选择单一方案, 而是引入了开发人员的 标准术语: YYYY:年份全称。如:2020 YY:年费缩写。如:20 MM:月份缩写。如:1、2、3 DD:日缩写。如:1、2、3 ... 和日期格式化类似有木有。是的,日期你可以随意,甚至可以是任意递增格式,但建议使用标准格式而已。 Spring改变版本号命名规则 Spring团队在其官网博客里于2020-04-30对外宣布要改变版本号命名规则,共包含两部分的内容: Release Train版本规则改变 Project Module版本规则改变 这些改变将在下一个发布版本里体现出来,比如我们已经能看到的使用新规则命名版本号的是:Spring Data 2020.0.0、Spring Data 2020.0.1 Release Train版本规则改变 Spring自2013年以来一直按照字母表顺序来进行排序版本。举例两个典型的,也是我们比较熟悉的按照Release Train发版的项目给你瞧一瞧,我绘制成图标如下: Spring Data: Release Train 发布日期 Spring Data Arora 2013-02 Spring Data Babbage 2013-09 Spring Data Codd 2014-02 Spring Data Dijkstra 2014-05 ... ... Spring Data Neumann 2020-05 Spring Data 2020.0.0 2020-10 Spring Cloud: Release Train 发布日期 Spring Cloud Angel 2015-06 Spring Cloud Brixton 2016-05 Spring Cloud Camden 2016-09 Spring Cloud Dalston 2017-04 ... ... Spring Cloud Hoxton 2019-11 Spring Cloud 2020.0.0-M4 2020-10 注意:截止目前,Spring Cloud 2020的正式版还未正式发布,预计11月结束之前会正式推出,以支持Spring Boot 2.4.0 存在的问题 如上表所示,按照字母表排序作为版本号是存在如下问题的: 按照字母排序,对于非英文国家有一定门槛难以记忆(比如天朝的程序员们) 如果排序字母到达Z了,就会出现命名上的难题了 从版本号上不能体现出向下兼容性,着让使用者(准备升级者)很难做出判断而做出风险预估 单词的拼写很困难(版本号都得靠复制,现在是降低效率的表现) 解决问题(改变后) 为了解决这些问题,Spring采用了日历化版本,并且使用的规则/公式是YYYY.MINOR.MICRO[-MODIFIER],对各部分解释如下: YYYY:年份全称。eg:2020 MINOR:辅助版本号(一般升级些非主线功能),在当前年内从0递增 MICRO:补丁版本号(一般修复些bug),在当前年内从0递增 MODIFIER:非必填。后缀,它用于修饰一些关键节点,用这些字母表示 M数字:里程碑版本,如2020.0.0-M1、2020.0.0-M2 RC数字:发布候选版本,如2020.0.0-RC1、2020.0.0-RC2 SNAPSHOT:快照版本(后无数字哦),如2020.0.0-SNAPSHOT 啥都木有:正式版本(可放心使用,相当于之前的xxx-RELEASE),如2020.0.0 通过新的版本命名方式,解决了向后兼容带来的问题(一看版本号就能清晰的知道向后兼容性如何),不再存在上限焦虑了,并且这种排序对非英语国家非常友好,点赞。自此,对于Spring Cloud来说H版是它最后一个用英文单词命名的版本号了,下个版本将是Spring Cloud 2020.0.0,预计在本月正式发布。 Project Module版本规则改变 对于项目模块的版本号而言,其实Spring早在其3.0.0.M1版本(2008年)就使用了“语义化版本”规则进行发布管理。本可以不用做改动也行,但Spring官方觉得既然这次对Release Train做了修整,那就一起调整下是更好的。 项目模块的版本规则Spirng采用Semantic Versioning语义版本号规范,另外呢Spring还希望开发者很容易熟悉这个版本号,因此制定了这个模版:MAJOR.MINOR.PATCH[-MODIFIER]。前面三部分就不再解释啦,详情请看上面的关于Semantic Versioning的说明。对于最后面的MODIFIER部分保持了和Release Train一模一样的语义: MODIFIER:非必填。后缀,它用于修饰一些关键节点,用这些字母表示 M数字:里程碑版本,如2.4.0-M1、2.4.0-M2 RC数字:发布候选版本,如2.4.0-RC1、2.4.0-RC2 SNAPSHOT:快照版本(后无数字哦),如2.4.0-SNAPSHOT 啥都木有:正式版本(可放心使用,相当于之前的xxx-RELEASE),如2.4.0 总的来说此部分规则改变并不大,简单对比就是这样: 改变前 改变后 3.0.0.M1 3.0.0-M1 以最新发布的Spirng Framework版本为例,它应用了最新的发版规则: <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.0</version> <!-- 5.2.0.RELEASE --> </dependency> 对比新旧版本号可知,新规则最大的区别是干掉了 .RELEASE,因此书写时请稍加注意。 ✍总结 本次Spring做出版本号规则的调整,更加彰显活力。喜闻乐见的是这名称对于处于天朝的我们是利好啊,毕竟SC的那些英文单词你能记住几个?现在书写其版本号终于可以盲写了,并且通过版本号能非常直观的知晓到当前使用版本的新旧程度,从而做出相关判断/预估,非常便捷。 另外,截止稿前,Spring Boot 2.4.0(注意木有.RELEASE了哦)以及Spring Framework 5.3.0均已重磅发布,为了给马上到来的Spring Cloud 2020.0.0做好铺垫,接下来几篇文章将对它俩进行阐述,欢迎持续关注。 ✔推荐阅读: JDK15正式发布,划时代的ZGC同时宣布转正 IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 Spring Boot 2.3.0正式发布:优雅停机、配置文件位置通配符新特性一览 搞事情?Spring Boot今天一口气发布三个版本
1024,代码改变世界。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。又一年1024程序员节,你快乐吗?还是在加班上线呢? 上篇文章 介绍了Validator校验器的五大核心组件,在结合前面几篇所讲,相信你对Bean Validation已有了一个整体认识了。 本文将非常实用,因为将要讲述的是Bean Validation在4个层级上的验证方式,它将覆盖你使用过程中的方方面面,不信你看。 版本约定 Bean Validation版本:2.0.2 Hibernate Validator版本:6.1.5.Final ✍正文 Jakarta Bean它的验证约束是通过声明式方式(注解)来表达的,我们知道Java注解几乎可以标注在任何地方(package上都可标注注解你敢信?),那么Jakarta Bean支持哪些呢? Jakarta Bean共支持四个级别的约束: 字段约束(Field) 属性约束(Property) 容器元素约束(Container Element) 类约束(Class) 值得注意的是,并不是所有的约束注解都能够标注在上面四种级别上。现实情况是:Bean Validation自带的22个标准约束全部支持1/2/3级别,且全部不支持第4级别(类级别)约束。当然喽,作为补充的Hibernate-Validator它提供了一些专门用于类级别的约束注解,如org.hibernate.validator.constraints.@ScriptAssert就是一常用案例。 说明:为简化接下来示例代码,共用工具代码提前展示如下: public abstract class ValidatorUtil { public static ValidatorFactory obtainValidatorFactory() { return Validation.buildDefaultValidatorFactory(); } public static Validator obtainValidator() { return obtainValidatorFactory().getValidator(); } public static ExecutableValidator obtainExecutableValidator() { return obtainValidator().forExecutables(); } public static <T> void printViolations(Set<ConstraintViolation<T>> violations) { violations.stream().map(v -> v.getPropertyPath() + v.getMessage() + ",但你的值是: " + v.getInvalidValue()).forEach(System.out::println); } } 1、字段级别约束(Field) 这是我们最为常用的一种约束方式: public class Room { @NotNull public String name; @AssertTrue public boolean finished; } 书写测试用例: public static void main(String[] args) { Room bean = new Room(); bean.finished = false; ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(bean)); } 运行程序,输出: finished只能为true,但你的值是: false name不能为null,但你的值是: null 当把约束标注在Field字段上时,Bean Validation将使用字段的访问策略来校验,不会调用任何方法,即使你提供了对应的get/set方法也不会触碰。 话外音:使用Field#get()得到字段的值 使用细节 字段约束可以应用于任何访问修饰符的字段 不支持对静态字段的约束(static静态字段使用约束无效) 若你的对象会被字节码增强,那么请不要使用Field约束,而是使用下面介绍的属性级别约束更为合适。 原因:增强过的类并不一定能通过字段反射去获取到它的值 绝大多数情况下,对Field字段做约束的话均是POJO,被增强的可能性极小,因此此种方式是被推荐的,看着清爽。 2、属性级别约束(Property) 若一个Bean遵循Java Bean规范,那么也可以使用属性约束来代替字段约束。比如上例可改写为如下: public class Room { public String name; public boolean finished; @NotNull public String getName() { return name; } @AssertTrue public boolean isFinished() { return finished; } } 执行上面相同的测试用例,输出: finished只能为true,但你的值是: false name不能为null,但你的值是: null 效果“完全”一样。 当把约束标注在Property属性上时,将采用属性访问策略来获取要验证的值。说白了:会调用你的Method来获取待校验的值。 使用细节 约束放在get方法上优于放在set方法上,这样只读属性(没有get方法)依然可以执行约束逻辑 不要在属性和字段上都标注注解,否则会重复执行约束逻辑(有多少个注解就执行多少次) 不要既在属性的get方法上又在set方法上标注约束注解 3、容器元素级别约束(Container Element) 还有一种非常非常常见的验证场景:验证容器内(每个)元素,也就验证参数化类型parameterized type。形如List<Room>希望里面装的每个Room都是合法的,传统的做法是在for循环里对每个room进行验证: List<Room> beans = new ArrayList<>(); for (Room bean : beans) { validate(bean); ... } 很明显这么做至少存在下面两个不足: 验证逻辑具有侵入性 验证逻辑是黑匣子(不看内部源码无法知道你有哪些约束),非声明式 在本专栏第一篇知道了从Bean Validation 2.0开始就支持容器元素校验了(本专栏使用版本为:2.02),下面我们来体验一把: public class Room { @NotNull public String name; @AssertTrue public boolean finished; } 书写测试用例: public static void main(String[] args) { List<@NotNull Room> rooms = new ArrayList<>(); rooms.add(null); rooms.add(new Room()); Room room = new Room(); room.name = "YourBatman"; rooms.add(room); ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms)); } 运行程序,没有任何输出,也就是说并没有对rooms立面的元素进行验证。这里有一个误区:Bean Validator是基于Java Bean进行验证的,而此处你的rooms仅仅只是一个容器类型的变量而已,因此不会验证。 其实它是把List当作一个Bean,去验证List里面的标注有约束注解的属性/方法。很显然,List里面不可能标注有约束注解嘛,所以什么都不输出喽 为了让验证生效,我们只需这么做: @Data @NoArgsConstructor @AllArgsConstructor public class Rooms { private List<@Valid @NotNull Room> rooms; } public static void main(String[] args) { List<@NotNull Room> beans = new ArrayList<>(); beans.add(null); beans.add(new Room()); Room room = new Room(); room.name = "YourBatman"; beans.add(room); // 必须基于Java Bean,验证才会生效 Rooms rooms = new Rooms(beans); ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms)); } 运行程序,输出: rooms[0].<list element>不能为null,但你的值是: null rooms[2].finished只能为true,但你的值是: false rooms[1].name不能为null,但你的值是: null rooms[1].finished只能为true,但你的值是: false rooms[1].finished只能为true,但你的值是: false 从日志中可以看出,元素的验证顺序是不保证的。 小贴士:在HV 6.0 之前的版本中,验证容器元素时@Valid是必须,也就是必须写成这样:List<@Valid @NotNull Room> rooms才有效。在HV 6.0之后@Valid这个注解就不是必须的了 使用细节 若约束注解想标注在容器元素上,那么注解定义的@Target里必须包含TYPE_USE(Java8新增)这个类型 BV和HV(除了Class级别)的所有注解均能标注在容器元素上 BV规定了可以验证容器内元素,HV提供实现。它默认支持如下容器类型: java.util.Iterable的实现(如List、Set) java.util.Map的实现,支持key和value java.util.Optional/OptionalInt/OptionalDouble... JavaFX的javafx.beans.observable.ObservableValue 自定义容器类型(自定义很重要,详见下篇文章) 4、类级别约束(Class) 类级别的约束验证是很多同学不太熟悉的一块,但它却很是重要。 其实Hibernate-Validator已内置提供了一部分能力,但可能还不够,很多场景需要自己动手优雅解决。为了体现此part的重要性,我决定专门撰文描述,当然还有自定义容器类型类型的校验喽,我们下文见。 字段约束和属性约束的区别 字段(Field) VS 属性(Property)本身就属于一对“近义词”,很多时候口头上我们并不做区分,是因为在POJO里他俩一般都同时存在,因此大多数情况下可以对等沟通。比如: @Data public class Room { @NotNull private String name; @AssertTrue private boolean finished; } 字段和属性的区别 字段具有存储功能:字段是类的一个成员,值在内存中真实存在;而属性它不具有存储功能,属于Java Bean规范抽象出来的一个叫法 字段一般用于类内部(一般是private),而属性可供外部访问(get/set一般是public) 这指的是一般情况下的规律 字段的本质是Field,属性的本质是Method 属性并不依赖于字段而存在,只是他们一般都成双成对出现 如getClass()你可认为它有名为class的属性,但是它并没有名为class的字段 知晓了字段和属性的区别,再去理解字段约束和属性约束的差异就简单了,它俩的差异仅仅体现在待验证值访问策略上的区别: 字段约束:直接反射访问字段的值 -> Field#get(不会执行get方法体) 属性约束:调用属性get方法 -> getXXX(会执行get方法体) 小贴士:如果你希望执行了验证就输出一句日志,又或者你的POJO被字节码增强了,那么属性约束更适合你。否则,推荐使用字段约束 ✍总结 嗯,这篇文章还不错吧,总体浏览下来行文简单,但内容还是挺干的哈,毕竟1024节嘛,不来点的干的心里有愧。 作为此part姊妹篇的上篇,它是每个同学都有必要掌握的使用方式。而下篇我觉得应该更为兴奋些,毕竟那里才能加分。1024,撸起袖子继续干。 ✔推荐阅读: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸 4. Validator校验器的五大核心组件,一个都不能少
困难是弹簧,你弱它就强。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 [上篇文章]()介绍了校验器上下文ValidatorContext,知道它可以对校验器Validator的核心五大组件分别进行定制化设置,那么这些核心组件在校验过程中到底扮演着什么样的角色呢,本文一探究竟。 作为核心组件,是有必要多探究一分的。以此为基,再扩散开了解和使用其它功能模块便将如鱼得水。但是过程枯燥是真的,所以需要坚持呀。 版本约定 Bean Validation版本:2.0.2 Hibernate Validator版本:6.1.5.Final ✍正文 Bean Validation校验器的这五大核心组件通过ValidatorContext可以分别设置:若没设置(或为null),那就回退到使用ValidatorFactory默认的组件。 准备好的组件,统一通过ValidatorFactory暴露出来予以访问: public interface ValidatorFactory extends AutoCloseable { ... MessageInterpolator getMessageInterpolator(); TraversableResolver getTraversableResolver(); ConstraintValidatorFactory getConstraintValidatorFactory(); ParameterNameProvider getParameterNameProvider(); @since 2.0 ClockProvider getClockProvider(); ... } MessageInterpolator 直译为:消息插值器。按字面不太好理解:简单的说就是对message内容进行格式化,若有占位符{}或者el表达式${}就执行替换和计算。对于语法错误应该尽量的宽容。 校验失败的消息模版交给它处理就成为了人能看得懂的消息格式,因此它能够处理消息的国际化:消息的key是同一个,但根据不同的Locale展示不同的消息模版。最后在替换/技术模版里面的占位符即可~ 这是Bean Validation的标准接口,Hibernate Validator提供了实现:Hibernate Validation它使用的是ResourceBundleMessageInterpolator来既支持参数,也支持EL表达式。内部使用了javax.el.ExpressionFactory这个API来支持EL表达式${}的,形如这样:must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}它是能够动态计算出${inclusive == true ? 'or equal to ' : ''}这部分的值的。 public interface MessageInterpolator { String interpolate(String messageTemplate, Context context); String interpolate(String messageTemplate, Context context, Locale locale); } 接口方法直接了当:根据上下文Context填充消息模版messageTemplate。它的具体工作流程我用图示如下:context上下文里一般是拥有需要被替换的key的键值对的,如下图所示:Hibernate对Context的实现中扩展出了如图的两个Map(非JSR标准),可以让你优先于 constraintDescriptor取值,取不到再fallback到标准模式的ConstraintDescriptor里取值,也就是注解的属性值。具体取值代码如下: ParameterTermResolver: private Object getVariable(Context context, String parameter) { // 先从hibernate扩展出来的方式取值 if (context instanceof HibernateMessageInterpolatorContext) { Object variable = ( (HibernateMessageInterpolatorContext) context ).getMessageParameters().get( parameter ); if ( variable != null ) { return variable; } } // fallback到标准模式:从注解属性里取值 return context.getConstraintDescriptor().getAttributes().get( parameter ); } 大部分情况下我们只用得到注解属性里面的值,也就是错误消息里可以使用{注解属性名}这种方式动态获取到注解属性值,给与友好错误提示。 上下文里的Message参数和Expression参数如何放进去的?在后续高级使用部分,会自定义k-v替换参数,也就会使用到本部分的高级应用知识,后文见。 TraversableResolver 能跨越的处理器。从字面是非常不好理解,用粗暴的语言解释为:确定某个属性是否能被ValidationProvider访问,当妹访问一个属性时都会通过它来判断一下子,提供两个判断方法: public interface TraversableResolver { // 是否是可达的 boolean isReachable(Object traversableObject, Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType); // 是否是可级联的(是否标注有@Valid注解) boolean isCascadable(Object traversableObject, Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType); } 该接口主要根据配置项来进行判断,并不负责。内部使用,调用者基本无需关心,也不见更改其默认机制,暂且略过。 ConstraintValidatorFactory 约束校验器工厂。ConstraintValidator约束校验器我们应该不陌生:每个约束注解都得指定一个/多个约束校验器,形如这样:@Constraint(validatedBy = { xxx.class })。 ConstraintValidatorFactory就是工厂:可以根据Class生成对象实例。 public interface ConstraintValidatorFactory { // 生成实例:接口并不规定你的生成方式 <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key); // 释放实例。标记此实例不需要再使用,一般为空实现 // 和Spring容器集成时 .destroyBean(instance)时会调用此方法 void releaseInstance(ConstraintValidator<?, ?> instance); } Hibernate提供了唯一实现ConstraintValidatorFactoryImpl:使用空构造器生成实例 clazz.getConstructor().newInstance();。 小贴士:接口并没规定你如何生成实例,Hibernate Validator是使用空构造这么实现的而已~ ParameterNameProvider 参数名提供器。这个组件和Spring的ParameterNameDiscoverer作用是一毛一样的:获取方法/构造器的参数名。 public interface ParameterNameProvider { List<String> getParameterNames(Constructor<?> constructor); List<String> getParameterNames(Method method); } 提供的实现: DefaultParameterNameProvider:基于Java反射API Executable#getParameters()实现 @Test public void test9() { ParameterNameProvider parameterNameProvider = new DefaultParameterNameProvider(); // 拿到Person的无参构造和有参构造(@NoArgsConstructor和@AllArgsConstructor) Arrays.stream(Person.class.getConstructors()).forEach(c -> System.out.println(parameterNameProvider.getParameterNames(c))); } 运行程序,输出: [arg0, arg1, arg2, arg3] [] 一样的,若你想要打印出明确的参数名,请在编译参数上加上-parameters参数。 ReflectionParameterNameProvider:已过期。请使用上面的default代替 ParanamerParameterNameProvider:基于com.thoughtworks.paranamer.Paranamer实现参数名的获取,需要额外导入相应的包才行。嗯,这里我就不试了哈~ ClockProvider 时钟提供器。这个接口很简单,就是提供一个Clock,给@Past、@Future等阅读判断提供参考。唯一实现为DefaultClockProvider: public class DefaultClockProvider implements ClockProvider { public static final DefaultClockProvider INSTANCE = new DefaultClockProvider(); private DefaultClockProvider() { } // 默认是系统时钟 @Override public Clock getClock() { return Clock.systemDefaultZone(); } } 默认使用当前系统时钟作为参考。若你的系统有全局统一的参考标准,比如统一时钟,那就可以通过此接口实现自己的Clock时钟,毕竟每台服务器的时间并不能保证是完全一样的不是,这对于时间敏感的应用场景(如竞标)需要这么做。 以上就是对Validator校验器的五个核心组件的一个描述,总体上还是比较简单。其中第一个组件:MessageInterpolator插值器我认为是最为重要的,需要理解好了。对后面做自定义消息模版、国际化消息都有用。 加餐:ValueExtractor 值提取器。2.0版本新增一个比较重要的组件API,作用:把值从容器内提取出来。这里的容器包括:数组、集合、Map、Optional等等。 // T:待提取的容器类型 public interface ValueExtractor<T> { // 从原始值originalValue提取到receiver里 void extractValues(T originalValue, ValueReceiver receiver); // 提供一组方法,用于接收ValueExtractor提取出来的值 interface ValueReceiver { // 接收从对象中提取的值 void value(String nodeName, Object object); // 接收可以迭代的值,如List、Map、Iterable等 void iterableValue(String nodeName, Object object); // 接收有索引的值,如List Array // i:索引值 void indexedValue(String nodeName, int i, Object object); // 接收键值对的值,如Map void keyedValue(String nodeName, Object key, Object object); } } 容易想到,ValueExtractor的实现类就非常之多(所有的实现类都是内建的,非public的,这就是默认情况下支持的容器类型):举例两个典型实现: // 提取List里的值 LIST_ELEMENT_NODE_NAME -> <list element> class ListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> { static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor( new ListValueExtractor() ); private ListValueExtractor() { } @Override public void extractValues(List<?> originalValue, ValueReceiver receiver) { for ( int i = 0; i < originalValue.size(); i++ ) { receiver.indexedValue( NodeImpl.LIST_ELEMENT_NODE_NAME, i, originalValue.get( i ) ); } } } // 提取Optional里的值 @UnwrapByDefault class OptionalLongValueExtractor implements ValueExtractor<@ExtractedValue(type = Long.class) OptionalLong> { static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor( new OptionalLongValueExtractor() ); @Override public void extractValues(OptionalLong originalValue, ValueReceiver receiver) { receiver.value( null, originalValue.isPresent() ? originalValue.getAsLong() : null ); } } 校验器Validator通过它把值从容器内提取出来参与校验,从这你应该就能理解为毛从Bean Validation2.0开始就支持验证容器内的元素了吧,形如这样:List<@NotNull @Valid Person>、Optional<@NotNull @Valid Person>,可谓大大的方便了使用。 若你有自定义容器,需要提取的需求,那么你可以自定义一个ValueExtractor实现,然后通过ValidatorContext#addValueExtractor()添加进去即可 ✍总结 本文主要介绍了Validator校验器的五大核心组件的作用,Bean Validation2.0提供了ValueExtractor组件来实现容器内元素的校验,大大简化了对容器元素的校验复杂性,值得点赞。 ✔推荐阅读: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸
你发任你发,我用Java8。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 2020年9月15日,JDK15正式发布,可谓如约而至。按照Java SE的发展路线图,JDK14自此停止更新。值得注意的是JDK15并非LTS版本,Oracle官方对Java SE的支持路线图如下:JDK8的扩展支持时间超过了JDK11,Oracle你是认真的吗?开个玩笑~ 那么自Java11之后,哪个版本才是LTS版本呢?Oracle官方并没给出具体参考路线图,但可参考OpenJDK的这张:可以看到JDK17将是下一个LTS版本,预计发版日期是2021年9月份。当然喽这只是OpenJDK的发版线路图,并不代表Oracle官方,因此仅供参考,不过一般八九不离十。 小贴士:OpenJDK和Oracle JDK自从JDK11后,就共享了绝大部分代码了,节奏基本保持一致。 从JDK9之后,Oracle采用了新的发布周期:每6个月发布一个版本,每3年发布一个LTS版本。JDK14是继JDK9之后发布的第四个版本, 该版本为非LTS版本,最新的LTS版本为JDK11。因为是小鹿快跑,快速迭代,因此此处解释下这两个词:孵化器模块(Incubator)和预览特性(Preview)。 孵化器模块(孵化版/实验版) 尚未定稿的API/工具,主要用于从Java社区收集使用反馈,稳定性无保障,后期有较大可能性移除 预览特性(预览版) 规格已成型,实现已确定,但还未最终定稿。这些特性还是存在被移除的可能性,但一般来说最后都会被固定下来。 ✍正文 JDK15是Java SE平台的第15个版本,由JSR 390在Java社区进程中指定。 OpenJDK 15是9-15发布的,Oracle同步跟上。其它厂商的对应JDK版本也会随后跟上 该版本共提供14个新特性,通过这些JEP来表示,截图如下:下面针对其中对开发者日常编程关系较大的特性拉出来解释,并给出对应的使用示例(其实就是JEP 378喽)。 JDK14新特性回顾 老规矩,在进行JDK15的新特性介绍之前,先回顾下JDK14的主要特性有哪些。JDK 14于2020年3月17日发布。 一、Switch表达式 新的Switch表达式其实早在JDK 12、13中都已存在了,但只是预览版,到了JDK 14就彻底变为稳定版了,可以放心商用。 小贴士:预览版特性是有可能在后续版本中被移除的,但稳定版后几乎不可能被移除 switch新的表达式有两个显著的特点: 支持箭头表达式返回 支持yield和return返回值。 1、箭头表达式返回 JDK14之前写法: private static void printLetterCount(DayOfWeek dayOfWeek){ switch (dayOfWeek) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } } 要点:break可千万别忘记写,否则就是个大bug,并且还比较隐蔽,定位起来稍显困难。 JDK14等效的新写法: private static void printLetterCount(DayOfWeek dayOfWeek){ switch (dayOfWeek) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); } } 可明显看到新写法不需要一个个break了,从语法层面规避了我们犯错的可能性。 2、yield返回 JDK14之前写法: private static int getLetterCount(DayOfWeek dayOfWeek){ int letterCount; switch (dayOfWeek) { case MONDAY: case FRIDAY: case SUNDAY: letterCount = 6; break; case TUESDAY: letterCount = 7; break; case THURSDAY: case SATURDAY: letterCount = 8; break; case WEDNESDAY: letterCount = 9; break; default: throw new IllegalStateException("非法: " + dayOfWeek); } return letterCount; } JDK14等效的新写法: private static int getLetterCount(DayOfWeek dayOfWeek){ return switch (dayOfWeek) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; }; } 使用箭头操作符操作效果立竿见影。当然,你还可以使用yield关键字返回: private static int getLetterCount(DayOfWeek dayOfWeek){ return switch (dayOfWeek) { case MONDAY -> 6; default -> { int letterCount = dayOfWeek.toString().length(); yield letterCount; } }; } 二、instanceof的模式匹配(预览) 该功能在JDK14中处理预览版。 JDK14之前写法: public static void main(String[] args) { Object o = "hello world"; if(o instanceof String ){ String str = String.class.cast(o); System.out.println(str); } } JDK14等效的新写法: public static void main(String[] args) { Object o = "hello world"; // 屁股里直接可写个变量名,不再需要强转了 if(o instanceof String str){ System.out.println(str); } } 再如: if (obj instanceof String s && s.length() > 5) { s.contains(..) } 如果你运行时有如下错误: java: instanceof 中的模式匹配 是预览功能,默认情况下禁用。 (请使用 --enable-preview 以启用 instanceof 中的模式匹配) 那是因为此功能是预览特性,需要你主动开启,如下:注意:此特性在JDK15中依旧为预览版。 三、实用的NullPointerException 略。 四、Record(预览) Java年纪太大,语法不够新潮,有时候确实太麻烦,因此有了Record的出现:干掉那些get/set、toString、equals等方法。 public record Person(String name,Integer age) { } public static void main(String[] args) { Person person= new Person("YourBatman", 18); System.out.println(person); System.out.println(person.name()); System.out.println(person.age()); } 运行程序,结果打印: Person[name=YourBatman, age=18] YourBatman 18 注意:此特性在JDK15中依旧为预览版。 五、文本块Text Blocks(二次预览) 这个特性可是非常好用,它属于二次预览:已在JDK 13预览过一次。 public static void main(String[] args) { String html = """ <html> <body> <p>hello world</p> </body> </html> """; String query = """ SELECT * from USER WHERE `id` = 1 ORDER BY `id`, `name`; """; } 在JDK13中,这种是有换行的。在JDK14中,可以加上一个符号让其不让换行: public static void main(String[] args) { String query = """ SELECT * from USER \ WHERE `id` = 1 \ ORDER BY `id`, `name`;\ """; System.out.println(query); } 运行程序,输出(可以看到展示为一行了): SELECT * from USER WHERE `id` = 1 ORDER BY `id`, `name`; 注意:此特性在JDK15中已经为正式版。 六、删除CMS垃圾收集器 这款著名的垃圾回收器从这个版本就彻底被删除了。JDK9开始使用G1作为默认的垃圾回收器(JDK11中ZGC开始崭露头角),就已经把CMS标记为过期了,在此版本正式删除。 七、ZGC垃圾回收器(实验) 革命性的ZGC:任意堆大小(TB级别)都能保证延迟在10ms以内,是以低延迟为首要目标的一款垃圾回收器。 在JDK14之前,ZGC只能用于Linux上,现在也可使用在windows上了 注意:此特性在JDK15中已经为正式版(JDK11开始出现)。 JDK15新特性 有了JDK14新特性回顾做铺垫,再来了解JDK15的新特性就方便很多了。 特别说明:运行JDK15需要IDEA 2020.2才能支持哦(JDK14要求IDEA 2020.1),然后关于IDEA 2020.2的使用教程(新特性),请移步我公众号前面发的这篇文章:IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 一、文本块Text Blocks Text Blocks首次是在JDK 13中以预览功能出现的,然后在JDK 14中又预览了一次,终于在JDK 15中被确定下来,可放心使用了(使用示例请参考文上)。 二、ZGC转正 ZGC是Java 11引入的新的垃圾收集器(JDK9以后默认的垃圾回收器是G1),经过了多个实验阶段,自此终于成为正式特性。 ZGC是一个重新设计的并发的垃圾回收器,可以极大的提升GC的性能。支持任意堆大小而保持稳定的低延迟(10ms以内),性能非常可观。 打开方式:使用-XX:+UseZGC命令行参数打开,相信不久的将来它必将成为默认的垃圾回收器。 三、Shenandoah转正 怎么形容Shenandoah和ZGC的关系呢?异同点大概如下: 相同点:性能几乎可认为是相同的 不同点:ZGC是Oracle JDK的,根正苗红。而Shenandoah只存在于OpenJDK中,因此使用时需注意你的JDK版本 打开方式:使用-XX:+UseShenandoahGC命令行参数打开。 四、删除Nashorn JavaScript Engine Nashorn是在JDK提出的脚本执行引擎,早在JDK11就已经把它标记为过期了,JDK15完全移除。 在JDK11中取以代之的是GraalVM。GraalVM是一个运行时平台,它支持Java和其他基于Java字节码的语言,但也支持其他语言,如JavaScript,Ruby,Python或LLVM。性能是Nashorn的2倍以上。 五、CharSequence新增isEmpty默认方法 啥都不说,源码一看便知: @since 15 default boolean isEmpty() { return this.length() == 0; } String实现了CharSequence接口的,这应该地球人都知道吧。 升级建议 自己玩玩就行,毕竟不是LTS版本。 但是,虽然说仅限于自己玩玩就行,但不代表就没有关注的意义哈。还是那个道理,如果JDK12、13、14、15...都不关注些的话,到时候突然来个JDK17的LTS版本,接受起来就会稍显困难。 ✍总结 JDK15整体来看新特性方面并不算很亮眼,它主要是对之前版本预览特性的功能做了确定,如文本块、ZGC等,这么一来我们就可以放心大胆的使用啦。 半年一次的发版速度真心学不动了,不过还好我有我的坚持:你发任你发,我用Java8。 公众号后台回复:JDK15,一键打包获取IDEA2020.2.2 + JDK15安装包(mac + windows) ✔推荐阅读: IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效 Spring Boot 2.3.0正式发布:优雅停机、配置文件位置通配符新特性一览 搞事情?Spring Boot今天一口气发布三个版本
乔丹是我听过的篮球之神,科比是我亲眼见过的篮球之神。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 通过前两篇文章的叙述,相信能勾起你对Bean Validation的兴趣。那么本文就站在一个使用者的角度来看,要使用Bean Validation完成校验的话我们应该掌握、熟悉哪些接口、接口方法呢? 版本约定 Bean Validation版本:2.0.2 Hibernate Validator版本:6.1.5.Final ✍正文 Bean Validation属于Java EE标准技术,拥有对应的JSR抽象,因此我们实际使用过程中仅需要面向标准使用即可,并不需要关心具体实现(是hibernate实现,还是apache的实现并不重要),也就是我们常说的面向接口编程。 Tips:为了方便下面做示例讲解,对一些简单、公用的方法抽取如下: public abstract class ValidatorUtil { public static ValidatorFactory obtainValidatorFactory() { return Validation.buildDefaultValidatorFactory(); } public static Validator obtainValidator() { return obtainValidatorFactory().getValidator(); } public static ExecutableValidator obtainExecutableValidator() { return obtainValidator().forExecutables(); } public static <T> void printViolations(Set<ConstraintViolation<T>> violations) { violations.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); } } Validator 校验器接口:校验的入口,可实现对Java Bean、某个属性、方法、构造器等完成校验。 public interface Validator { ... } 它是使用者接触得最多的一个API,当然也是最重要的喽。因此下面对其每个方法做出解释+使用示例。 validate:校验Java Bean <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups); 验证Java Bean对象上的所有约束。示例如下: Java Bean: @ScriptAssert(script = "_this.name==_this.fullName", lang = "javascript") @Data public class User { @NotNull private String name; @Length(min = 20) @NotNull private String fullName; } @Test public void test5() { User user = new User(); user.setName("YourBatman"); Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validate(user); ValidatorUtil.printViolations(result); } 说明:@ScriptAssert是Hibernate Validator提供的一个脚本约束注解,可以实现垮字段逻辑校验,功能非常之强大,后面详解 运行程序,控制台输出: 执行脚本表达式"_this.name==_this.fullName"没有返回期望结果: User(name=YourBatman, fullName=null) fullName 不能为null: null 符合预期。值得注意的是:针对fullName中的@Length约束来说,null是合法的哟,所以不会有相应日志输出的 校验Java Bean所有约束中的所有包括:1、属性上的约束2、类上的约束 validateProperty:校验指定属性 <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups); 校验某个Java Bean中的某个属性上的所有约束。示例如下: @Test public void test6() { User user = new User(); user.setFullName("YourBatman"); Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validateProperty(user, "fullName"); ValidatorUtil.printViolations(result); } 运行程序,控制台输出: fullName 长度需要在20和2147483647之间: YourBatman 符合预期。它会校验属性上的所有约束,注意只是属性上的哦,其它地方的不管。 validateValue:校验value值 校验某个value值,是否符合指定属性上的所有约束。可理解为:若我把这个value值赋值给这个属性,是否合法? <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups); 这个校验方法比较特殊:不用先存在对象实例,直接校验某个值是否满足某个属性的所有约束,所以它可以做事钱校验判断,还是挺好用的。示例如下: @Test public void test7() { Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validateValue(User.class, "fullName", "A哥"); ValidatorUtil.printViolations(result); } 运行程序,输出: fullName 长度需要在20和2147483647之间: A哥 若程序改为:.validateValue(User.class, "fullName", "YourBatman-YourBatman");,再次运行程序,控制台将不再输出(字符串长度超过20,合法了嘛)。 获取Class类型描述信息 BeanDescriptor getConstraintsForClass(Class<?> clazz); 这个clazz可以是类or接口类型。BeanDescriptor:描述受约束的Java Bean和与其关联的约束。示例如下: @Test public void test8() { BeanDescriptor beanDescriptor = obtainValidator().getConstraintsForClass(User.class); System.out.println("此类是否需要校验:" + beanDescriptor.isBeanConstrained()); // 获取属性、方法、构造器的约束 Set<PropertyDescriptor> constrainedProperties = beanDescriptor.getConstrainedProperties(); Set<MethodDescriptor> constrainedMethods = beanDescriptor.getConstrainedMethods(MethodType.GETTER); Set<ConstructorDescriptor> constrainedConstructors = beanDescriptor.getConstrainedConstructors(); System.out.println("需要校验的属性:" + constrainedProperties); System.out.println("需要校验的方法:" + constrainedMethods); System.out.println("需要校验的构造器:" + constrainedConstructors); PropertyDescriptor fullNameDesc = beanDescriptor.getConstraintsForProperty("fullName"); System.out.println(fullNameDesc); System.out.println("fullName属性的约束注解个数:"fullNameDesc.getConstraintDescriptors().size()); } 运行程序,输出: 此类是否需要校验:true 需要校验的属性:[PropertyDescriptorImpl{propertyName=name, cascaded=false}, PropertyDescriptorImpl{propertyName=fullName, cascaded=false}] 需要校验的方法:[] 需要校验的构造器:[] PropertyDescriptorImpl{propertyName=fullName, cascaded=false} fullName属性的约束注解个数:2 获得Executable校验器 @since 1.1 ExecutableValidator forExecutables(); Validator这个API是1.0就提出的,它只能校验Java Bean,对于方法、构造器的参数、返回值等校验还无能为力。 这不1.1版本就提供了ExecutableValidator这个API解决这类需求,它的实例可通过调用Validator的该方法获得,非常方便。关于ExecutableValidator 的具体使用请移步上篇文章。 ConstraintViolation 约束违反详情。此对象保存了违反约束的上下文以及描述消息。 // <T>:root bean public interface ConstraintViolation<T> { } 简单的说,它保存着执行完所有约束后(不管是Java Bean约束、方法约束等等)的结果,提供了访问结果的API,比较简单: 小贴士:只有违反的约束才会生成此对象哦。违反一个约束对应一个实例 // 已经插值(interpolated)的消息 String getMessage(); // 未插值的消息模版(里面变量还未替换,若存在的话) String getMessageTemplate(); // 从rootBean开始的属性路径。如:parent.fullName Path getPropertyPath(); // 告诉是哪个约束没有通过(的详情) ConstraintDescriptor<?> getConstraintDescriptor(); 示例:略。 ValidatorContext 校验器上下文,根据此上下文创建Validator实例。不同的上下文可以创建出不同实例(这里的不同指的是内部组件不同),满足各种个性化的定制需求。 ValidatorContext接口提供设置方法可以定制校验器的核心组件,它们就是Validator校验器的五大核心组件: public interface ValidatorContext { ValidatorContext messageInterpolator(MessageInterpolator messageInterpolator); ValidatorContext traversableResolver(TraversableResolver traversableResolver); ValidatorContext constraintValidatorFactory(ConstraintValidatorFactory factory); ValidatorContext parameterNameProvider(ParameterNameProvider parameterNameProvider); ValidatorContext clockProvider(ClockProvider clockProvider); // @since 2.0 值提取器。 // 注意:它是add方法,属于添加哦 ValidatorContext addValueExtractor(ValueExtractor<?> extractor); Validator getValidator(); } 可以通过这些方法设置不同的组件实现,设置好后再来个getValidator()就得到一个定制化的校验器,不再千篇一律喽。所以呢,首先就是要得到ValidatorContext实例,下面介绍两种方法。 方式一:自己new @Test public void test2() { ValidatorFactoryImpl validatorFactory = (ValidatorFactoryImpl) ValidatorUtil.obtainValidatorFactory(); // 使用默认的Context上下文,并且初始化一个Validator实例 // 必须传入一个校验器工厂实例哦 ValidatorContext validatorContext = new ValidatorContextImpl(validatorFactory) .parameterNameProvider(new DefaultParameterNameProvider()) .clockProvider(DefaultClockProvider.INSTANCE); // 通过该上下文,生成校验器实例(注意:调用多次,生成实例是多个哟) System.out.println(validatorContext.getValidator()); } 运行程序,控制台输出: org.hibernate.validator.internal.engine.ValidatorImpl@1757cd72 这种是最直接的方式,想要啥就new啥嘛。不过这么使用是有缺陷的,主要体现在这两个方面: 不够抽象。new的方式嘛,和抽象谈不上关系 强耦合了Hibernate Validator的API,如:org.hibernate.validator.internal.engine.ValidatorContextImpl#ValidatorContextImpl 方式二:工厂生成 上面即使通过自己new的方式得到ValidatorContext实例也需要传入校验器工厂,那还不如直接使用工厂生成呢。恰好ValidatorFactory也提供了对应的方法: ValidatorContext usingContext(); 该方法用于得到一个ValidatorContext实例,它具有高度抽象、与底层API无关的特点,是推荐的获取方式,并且使用起来有流式编程的效果,如下所示: @Test public void test3() { Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext() .parameterNameProvider(new DefaultParameterNameProvider()) .clockProvider(DefaultClockProvider.INSTANCE) .getValidator(); } 很明显,这种方式是被推荐的。 获得Validator实例的两种姿势 在文章最后,再回头看看Validator实例获取的两种姿势。Validator校验器接口是完成数据校验(Java Bean校验、方法校验等)最主要API,经过了上面的讲述,下面可以来个获取方式的小总结了。 方式一:工厂直接获取 @Test public void test3() { Validator validator = ValidatorUtil.obtainValidatorFactory().getValidator(); } 这种方式十分简单、简约,对初学者十分的友好,入门简单,优点明显。各组件全部使用默认方式,省心。如果要挑缺点那肯定也是有的:无法满足个性化、定制化需求,说白了:无法自定义五大组件 + 值提取器的实现。 作为这么优秀的Java EE标准技术,怎么少得了对扩展的开放呢?继续方式二吧~ 方式二:从上下文获取 校验器上下文也就是ValidatorContext喽,它的步骤是先得到上下文实例,然后做定制,再通过上下文实例创建出Validator校验器实例了。 示例代码: @Test public void test3() { Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext() .parameterNameProvider(new DefaultParameterNameProvider()) .clockProvider(DefaultClockProvider.INSTANCE) .getValidator(); } 这种方式给与了极大的定制性,你可以任意指定核心组件实现,来达到自己的要求。 这两种方式结合起来,不就是典型的默认 + 定制扩展的搭配麽?另外,Validator是线程安全的,一般来说一个应用只需要初始化一个 Validator实例即可,所以推荐使用方式二进行初始化,对个性扩展更友好。 ✍总结 本文站在一个使用者的角度去看如何使用Bean Validation,以及哪些标准的接口API是必须掌握了,有了这些知识点在平时绝大部分case都能应对自如了。 规范接口/标准接口一般能解决绝大多数问题,这就是规范的边界,有些可为,有些不为 当然喽,这些是基本功。要想深入理解Bean Validation的功能,必须深入了解Hibernate Validator实现,因为有些比较常用的case它做了很好的补充,咱们下文见。 ✔推荐阅读: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知 2. Bean Validation声明式校验方法的参数、返回值
冤冤相报何时了,得饶人处且饶人。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 今天中午收到我司安全部发了一封邮件:Jackson存在安全漏洞。查了一下,这件事并不算很新鲜了(已经过了10天的样子),本文来聊聊吧。 说起来还蛮戏剧化:阿里云向Jackson官方提交了一个Jackson序列化安全漏洞。众所周知,在国内关于JSON库使用有两大主要阵营:国际著名的Jackson库和国内阿里巴巴出品的Fastjson。 同样的功能定位,不存在竞争想想也觉得不可能嘛。所以当我看到这个漏洞竟是阿里云上报的,就觉得这关系还蛮微妙呢,默默的腹黑了3秒钟,哈哈。 附:FasterXML/jackson-databind是一个简单基于Java应用库,Jackson可以轻松的将Java对象转换成json对象和xml文档,同样也可以将json、xml转换成Java对象。Jackson是美国FasterXML公司的一款适用于Java的数据处理工具。jackson-databind是其中的一个具有数据绑定功能的组件。 ✍正文 熟悉A哥的小伙伴知道,自从Fastjson上次爆出重大安全漏洞之后,我彻底的投入到了Jackson的阵营,工作中也慢慢去Fastjson化。这不还专门撰写了一篇文章来说明此事:Fastjson到了说再见的时候了。为了顺利完成“迁移”,我还专门写了一个,也有可能是全网唯一一个Jackson专栏,虽然很小众但就是干了~ 关于本次漏洞 2020年8月25日,jackson-databind(官方)发布了Jackson-databind序列化漏洞的安全通告,漏洞编号为CVE-2020-24616。 漏洞详情 该漏洞源于不安全的反序列化。远程攻击者可通过精心构造的恶意载荷利用该漏洞在系统执行任意代码。 其实上它的底层原理是利用某些类的反序列化利用链,可以绕过 jackson-databind 黑名单限制,远程攻击者通过向使用该组件的web服务接口发送特制请求包(精心构造的JSON),可以造成远程代码执行影响。 漏洞评级 评定方式 等级 威胁等级 高危 影响面 有限 漏洞评分 75 对此评级没有概念?那就来个参照物比一比嘛,我把Fastjson上次(2020-05)的安全漏洞评级给你做对比: 评定方式 等级 威胁等级 高危 影响面 广泛 漏洞评分 75 有限和广泛的的区别到底有多大,用文字不太好描述。打个比方,我觉得可类比艾滋病和新冠病毒的区别,前者对社会生态影响并不大,而后者恨不得让全球都停摆了,即使它致死率还远没有前者那么高,这就是影响范围的“力量”。 影响版本 jackson-databind < 2.9.10.6 因为现在大家都基于Spring Boot开发,针对版本号我扩展一下,方便你对号入座哈: Spring Boot版本 Jackson版本 1.5.22.RELEASE 2.8.x 2.0.9.RELEASE 2.9.x 2.1.16.RELEASE 2.9.10.5 2.2.9.RELEASE 2.10.x 2.3.3.RELEASE 2.11.x Spring Boot2.1.x应该是现行主流使用版本,因此从版本号上来看,大概率都在此次漏洞的射程范围内。 安全版本 jackson-databind 2.9.10.6或者2.10.x及以后版本 故事时间轴 2020-08-05,阿里云安全组同学向Jackson官方上报了这个安全漏洞:当天,官方回复预计会在8-15左右发布bug修复版本修复次问题(waht?知道问题了还得10后修复?):可结果是10天都不止。直到8.25这天,Jackson发布2.9.10.6版本修复了此问题,并向外界发公告公布此漏洞: 从8.5号Jackson官方知晓此漏洞,到8.25号最终发版解决此问题,整整20天,为何需要这么久?我想真像只有一个:此漏洞影响真的不大,或者说影响范围较窄。回忆下上次Fastjson出现的那个安全漏洞,24h内就给与了修复版本,并不是因为我们反映迅速,而是因为影响重大等不了... 修复建议 一股脑的全部升级到2.9.10.6或以上版本当然能规避此安全问题,但是你是否想过,你负责多少个服务?你团队、公司一共有多少个服务?你品,你细品,头大吗? 从官方对此次漏洞做出的反射弧时间来看,本次漏洞影响是相对较小的,因此我总结了下修复建议,倘若你同时满足如下三个条件,那么需要立马修复,否则可暂不理会: 对公网提供API接口 Jackson版本小于2.9.10.6 工程内有使用(或者引入)如下4个类任意一个: br.com.anteros.dbcp.AnterosDBCPDataSource com.pastdev.httpcomponents.configuration.JndiConfiguration com.nqadmin.rowset.JdbcRowSetImpl org.arrah.framework.rdbms.UpdatableJdbcRowsetImpl 条件3的理论支撑是我对比2.9.10.6版本release改动源码 + 我和我司安全组人员的讨论结果。修复方案也仅仅是在黑名单里新增了这4个类,截图如下: ✍总结 外行看热闹,内行看门道。千万不能说Fastjson出了个漏洞,Jackson也来一个就得出结论说打平手了,那会稍显外行。正所谓假设可以大胆,但小心求证,下结论需要谨慎。 总的来说,此次漏洞影响甚小,不用大惊小怪,我就继续我的Jackson之旅啦。 ✔推荐阅读: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑 7. Jackson用树模型处理JSON是必备技能,不信你看
你必须非常努力,才能干起来毫不费力。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 上篇文章 完整的介绍了JSR、Bean Validation、Hibernate Validator的联系和区别,并且代码演示了如何进行基于注解的Java Bean校验,自此我们可以在Java世界进行更完美的契约式编程了,不可谓不方便。 但是你是否考虑过这个问题:很多时候,我们只是一些简单的独立参数(比如方法入参int age),并不需要大动干戈的弄个Java Bean装起来,比如我希望像这样写达到相应约束效果: public @NotNull Person getOne(@NotNull @Min(1) Integer id, String name) { ... }; 本文就来探讨探讨如何借助Bean Validation 优雅的、声明式的实现方法参数、返回值以及构造器参数、返回值的校验。 声明式除了有代码优雅、无侵入的好处之外,还有一个不可忽视的优点是:任何一个人只需要看声明就知道语义,而并不需要了解你的实现,这样使用起来也更有安全感。 版本约定 Bean Validation版本:2.0.2 Hibernate Validator版本:6.1.5.Final ✍正文 Bean Validation 1.0版本只支持对Java Bean进行校验,到1.1版本就已支持到了对方法/构造方法的校验,使用的校验器便是1.1版本新增的ExecutableValidator : public interface ExecutableValidator { // 方法校验:参数+返回值 <T> Set<ConstraintViolation<T>> validateParameters(T object, Method method, Object[] parameterValues, Class<?>... groups); <T> Set<ConstraintViolation<T>> validateReturnValue(T object, Method method, Object returnValue, Class<?>... groups); // 构造器校验:参数+返回值 <T> Set<ConstraintViolation<T>> validateConstructorParameters(Constructor<? extends T> constructor, Object[] parameterValues, Class<?>... groups); <T> Set<ConstraintViolation<T>> validateConstructorReturnValue(Constructor<? extends T> constructor, T createdObject, Class<?>... groups); } 其实我们对Executable这个字眼并不陌生,向JDK的接口java.lang.reflect.Executable它的唯二两个实现便是Method和Constructor,刚好和这里相呼应。 在下面的代码示例之前,先提供两个方法用于获取校验器(使用默认配置),方便后续使用: // 用于Java Bean校验的校验器 private Validator obtainValidator() { // 1、使用【默认配置】得到一个校验工厂 这个配置可以来自于provider、SPI提供 ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); // 2、得到一个校验器 return validatorFactory.getValidator(); } // 用于方法校验的校验器 private ExecutableValidator obtainExecutableValidator() { return obtainValidator().forExecutables(); } 因为Validator等校验器是线程安全的,因此一般来说一个应用全局仅需一份即可,因此只需要初始化一次。 校验Java Bean 先来回顾下对Java Bean的校验方式。书写JavaBean和校验程序(全部使用JSR标准API),声明上约束注解: @ToString @Setter @Getter public class Person { @NotNull public String name; @NotNull @Min(0) public Integer age; } @Test public void test1() { Validator validator = obtainValidator(); Person person = new Person(); person.setAge(-1); Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出校验结果 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); } 运行程序,控制台输出: name 不能为null: null age 需要在1和18之间: -1 这是最经典的应用了。那么问题来了,如果你的方法参数就是个Java Bean,你该如何对它进行校验呢? 小贴士:有的人认为把约束注解标注在属性上,和标注在set方法上效果是一样的,其实不然,你有这种错觉全是因为Spring帮你处理了写东西,至于原因将在后面和Spring整合使用时展开 校验方法 对方法的校验是本文的重点。比如我有个Service如下: public class PersonService { public Person getOne(Integer id, String name) { return null; } } 现在对该方法的执行,有如下约束要求: id是必传(不为null)且最小值为1,但对name没有要求 返回值不能为null 下面分为校验方法参数和校验返回值两部分分别展开。 校验方法参数 如上,getOne方法有两个入参,我们需要对id这个参数做校验。如果不使用Bean Validation的话代码就需要这么写校验逻辑: public Person getOne(Integer id, String name) { if (id == null) { throw new IllegalArgumentException("id不能为null"); } if (id < 1) { throw new IllegalArgumentException("id必须大于等于1"); } return null; } 这么写固然是没毛病的,但是它的弊端也非常明显: 这类代码没啥营养,如果校验逻辑稍微多点就会显得臭长臭长的 不看你的执行逻辑,调用者无法知道你的语义。比如它并不知道id是传还是不传也行,没有形成契约 代码侵入性强 优化方案 既然学习了Bean Validation,关于校验方面的工作交给更专业的它当然更加优雅: public Person getOne(@NotNull @Min(1) Integer id, String name) throws NoSuchMethodException { // 校验逻辑 Method currMethod = this.getClass().getMethod("getOne", Integer.class, String.class); Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateParameters(this, currMethod, new Object[]{id, name}); if (!validResult.isEmpty()) { // ... 输出错误详情validResult validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); throw new IllegalArgumentException("参数错误"); } return null; } 测试程序就很简单喽: @Test public void test2() throws NoSuchMethodException { new PersonService().getOne(0, "A哥"); } 运行程序,控制台输出: getOne.arg0 最小不能小于1: 0 java.lang.IllegalArgumentException: 参数错误 ... 完美的符合预期。不过,arg0是什么鬼?如果你有兴趣可以自行加上编译参数-parameters再运行试试,有惊喜哦~ 通过把约束规则用注解写上去,成功的解决上面3个问题中的两个,特别是声明式约束解决问题3,这对于平时开发效率的提升是很有帮助的,因为契约已形成。 此外还剩一个问题:代码侵入性强。是的,相比起来校验的逻辑依旧写在了方法体里面,但一聊到如何解决代码侵入问题,相信不用我说都能想到AOP。一般来说,我们有两种AOP方式供以使用: 基于Java EE的@Inteceptors实现 基于Spring Framework实现 显然,前者是Java官方的标准技术,而后者是实际的标准,所以这个小问题先mark下来,等到后面讲到Bean Validation和Spring整合使用时再杀回来吧。 校验方法返回值 相较于方法参数,返回值的校验可能很多人没听过没用过,或者接触得非常少。其实从原则上来讲,一个方法理应对其输入输出负责的:有效的输入,明确的输出,这种明确就最好是有约束的。 上面的getOne方法题目要求返回值不能为null。若通过硬编码方式校验,无非就是在return之前来个if(result == null)的判断嘛: public Person getOne(Integer id, String name) throws NoSuchMethodException { // ... 模拟逻辑执行,得到一个result结果,准备返回 Person result = null; // 在结果返回之前校验 if (result == null) { throw new IllegalArgumentException("返回结果不能为null"); } return result; } 同样的,这种代码依旧有如下三个问题: 这类代码没啥营养,如果校验逻辑稍微多点就会显得臭长臭长的 不看你的执行逻辑,调用者无法知道你的语义。比如调用者不知道返回是是否可能为null,没有形成契约 代码侵入性强 优化方案 话不多说,直接上代码。 public @NotNull Person getOne(@NotNull @Min(1) Integer id, String name) throws NoSuchMethodException { // ... 模拟逻辑执行,得到一个result Person result = null; // 在结果返回之前校验 Method currMethod = this.getClass().getMethod("getOne", Integer.class, String.class); Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateReturnValue(this, currMethod, result); if (!validResult.isEmpty()) { // ... 输出错误详情validResult validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); throw new IllegalArgumentException("参数错误"); } return result; } 书写测试代码: @Test public void test2() throws NoSuchMethodException { // 看到没 IDEA自动帮你前面加了个notNull @NotNull Person result = new PersonService().getOne(1, "A哥"); } 运行程序,控制台输出: getOne.<return value> 不能为null: null java.lang.IllegalArgumentException: 参数错误 ... 这里面有个小细节:当你调用getOne方法,让IDEA自动帮你填充返回值时,前面把校验规则也给你显示出来了,这就是契约。明明白白的,拿到这样的result你是不是可以非常放心的使用,不再战战兢兢的啥都来个if(xxx !=null)的判断了呢?这就是契约编程的力量,在团队内能指数级的提升编程效率,试试吧~ 校验构造方法 这个,呃,(⊙o⊙)…...自己动手玩玩吧,记得牢~ 加餐:Java Bean作为入参如何校验? 如果一个Java Bean当方法参数,你该如何使用Bean Validation校验呢? public void save(Person person) { } 约束上可以提出如下合理要求: person不能为null 是个合法的person模型。换句话说:person里面的那些校验规则你都得遵守喽 对save方法加上校验如下: public void save(@NotNull Person person) throws NoSuchMethodException { Method currMethod = this.getClass().getMethod("save", Person.class); Set<ConstraintViolation<PersonService>> validResult = obtainExecutableValidator().validateParameters(this, currMethod, new Object[]{person}); if (!validResult.isEmpty()) { // ... 输出错误详情validResult validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); throw new IllegalArgumentException("参数错误"); } } 书写测试程序: @Test public void test3() throws NoSuchMethodException { // save.arg0 不能为null: null // new PersonService().save(null); new PersonService().save(new Person()); } 运行程序,控制台没有输出,也就是说校验通过。很明显,刚new出来的Person不是一个合法的模型对象,所以可以断定没有执行模型里面的校验逻辑,怎么办呢?难道仍要自己用Validator去用API校验麽? 好拉,不卖关子了,这个时候就清楚大名鼎鼎的@Valid注解喽,标注如下: public void save(@NotNull @Valid Person person) throws NoSuchMethodException { ... } 再次运行测试程序,控制台输出: save.arg0.name 不能为null: null save.arg0.age 不能为null: null java.lang.IllegalArgumentException: 参数错误 ... 这才是真的完美了。 小贴士:@Valid注解用于验证级联的属性、方法参数或方法返回类型。比如你的属性仍旧是个Java Bean,你想深入进入校验它里面的约束,那就在此属性头上标注此注解即可。另外,通过使用@Valid可以实现递归验证,因此可以标注在List上,对它里面的每个对象都执行校验 题外话一句:相信有小伙伴想问@Valid和Spring提供的@Validated有啥区别,我给的答案是:完全不是一回事,纯巧合而已。至于为何这么说,后面和Spring整合使用时给你讲得明明白白的。 加餐2:注解应该写在接口上还是实现上? 这是之前我面试时比较喜欢问的一个面试题,因为我认为这个题目的实用性还是比较大的。下面我们针对上面的save方法做个例子,提取一个接口出来,并且写上所有的约束注解: public interface PersonInterface { void save(@NotNull @Valid Person person) throws NoSuchMethodException; } 子类实现,一个注解都不写: public class PersonService implements PersonInterface { @Override public void save(Person person) throws NoSuchMethodException { ... // 方法体代码同上,略 } } 测试程序也同上,为: @Test public void test3() throws NoSuchMethodException { // save.arg0 不能为null: null // new PersonService().save(null); new PersonService().save(new Person()); } 运行程序,控制台输出: save.arg0.name 不能为null: null save.arg0.age 不能为null: null java.lang.IllegalArgumentException: 参数错误 ... 符合预期,没有任何问题。这还没完,还有很多组合方式呢,比如:约束注解全写在实现类上;实现类比接口少;比接口多...... 限于篇幅,文章里对试验过程我就不贴出来了,直接给你扔结论吧: 如果该方法是接口方法的实现,那么可存在如下两种case(这两种case的公用逻辑:约束规则以接口为准,有几个就生效几个,没有就没有): 保持和接口方法一毛一样的约束条件(极限情况:接口没约束注解,那你也不能有) 实现类一个都不写约束条件,结果就是接口里有约束就有,没约束就没有 如果该方法不是接口方法的实现,那就很简单了:该咋地就咋地 值得注意的是,在和Spring整合使用中还会涉及到一个问题:@Validated注解应该放在接口(方法)上,还是实现类(方法)上?你不妨可以自己先想想呢,答案那必然是后面分享喽。 ✍总结 本文讲述的是Bean Validation又一经典实用场景:校验方法的参数、返回值。后面加上和Spring的AOP整合将释放出更大的能量。 另外,通过本文你应该能再次感受到契约编程带来的好处吧,总之:能通过契约约定解决的就不要去硬编码,人生苦短,少编码多行乐。 最后,提个小问题哈:你觉得是代码量越多越安全,还是越少越健壮呢?被验证过100次的代码能不要每次都还需要重复去验证吗? ✔推荐阅读: 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知
乔丹是我听过的篮球之神,科比是我亲眼见过的篮球之神。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 作为一个开发者,聊起数据校验(Bean Validation),不管是前、中、后端都耳熟能详,并且心里暗爽:so easy。 的确,对数据做校验是一个程序员的基本素质,它不难但发生在我们程序的几乎每个角落,就像下面这幅图所示:每一层都需要做校验。如果你真的这么去写代码的话(每一层都写一份),肯定是不太合适的,良好的状态应该如下图所示: 作为一个Java开发者,在Spring大行其道的今天,很多小伙伴了解数据校验来自于Spring MVC场景,甚至止步于此。殊不知,Java EE早已把它抽象成了JSR标准技术,并且Spring还是借助整合它完成了自我救赎呢。 在我看来,按Spring的3C战略标准来比,Bean Validation数据校验这块是没有能够完成对传统Java EE的超越,自身设计存在过重、过度设计等特点。 本专栏命名为Bean Validation(数据校验),将先从JSR标准开始,再逐渐深入到具体实现Hibernate Validation、整合Spring使用场景等等。因此本专栏将让你将得到一份系统数据校验的知识。 ✍正文 在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的,比如生日必须是过去时,年龄必须>0等等。 为什么要有数据校验? 数据校验是非常常见的工作,在日常的开发中贯穿于代码的各个层次,从上层的View层到后端业务处理层,甚至底层的数据层。 我们知道通常情况下程序肯定是分层的,不同的层可能由不同的人来开发或者调用。若你是一个有经验的程序员,我相信你肯定见过在不同的层了都出现了相同的校验代码,这就是某种意义上的垃圾代码: public String queryValueByKey(String zhName, String enName, Integer age) { checkNotNull(zhName, "zhName must be not null"); checkNotNull(enName, "enName must be not null"); checkNotNull(age, "age must be not null"); validAge(age, "age must be positive"); ... } 从这个简单的方法入参校验至少能发现如下问题: 需要写大量的代码来进行参数基本验证(这种代码多了就算垃圾代码) 需要通过文字注释来知道每个入参的约束是什么(否则别人咋看得懂) 每个程序员做参数验证的方式可能不一样,参数验证抛出的异常也不一样,导致后期几乎没法维护 如上会导致代码冗余和一些管理的问题(代码量越大,管理起来维护起来就越困难),比如说语义的一致性问题。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定,这就是本文将要提供的一个新思路:Bean Validation。 关于Jakarta EE 2018年03月, Oracle 决定把 JavaEE 移交给开源组织 Eclipse 基金会,并且不再使用Java EE这个名称。这是它的新logo: 对应的名称修改还包括: 旧名称 新名称 Java EE Jakarta EE Glassfish Eclipse Glassfish Java Community Process (JCP) Eclipse EE.next Working Group (EE.next) Oracle development management Eclipse Enterprise for Java (EE4J) 和 Project Management Committee (PMC) JCP 将继续支持 Java SE社区。 但是,Jakarta EE规范自此将不会在JCP下开发。Jakarta EE标准大概由Eclipse Glassfish、Apache TomEE、Wildfly、Oracle WebLogic、JBoss、IBM、Websphere Liberty等组织来制定 迁移 既然名字都改了,那接下来就是迁移喽,毕竟Java EE这个名称(javax包名)不能再用了嘛。Eclipse接手后发布的首个Enterprise Java将是 Jakarta EE 9,该版本将以Java EE 8作为其基准版本(最低版本要求是Java8)。 有个意思的现象是:Java EE 8是2019.09.10发布的,但实际上官方名称是Jakarta EE 8了。很明显该版本并非由新组织设计和制定的,不是它们的产物。但是,彼时平台已更名为Jakarta有几个月了,因此对于一些Jar你在maven市场上经常能看见两种坐标: <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>2.0.1</version> </dependency> 虽然坐标不一样,但是内容是100%一样的(包名均还为javax.*),很明显这是更名的过度期,为后期全面更名做准备呢。 严格来讲:只要大版本号(第一个数字)还一样,包名是不可能变化的,因此一般来说均具有向下兼容性 既然Jakarta释放出了更名信号,那么下一步就是彻彻底底的改变喽。果不其然,这些都在Jakarta EE 9里得到实施。 Jakarta EE 9 2020.08.31,Jakarta后的第一个企业级平台Jakarta EE 9正式发布。如果说Jakarta EE 8只是冠了个名,那么这个就名正言顺了。 小贴士:我写本文时还没到2020.08.31呢,这个时间是我在官网趴来的,因此肯定准确 这次企业平台的升级最大的亮点是: 把旗下30于种技术的大版本号全部+1(Jakarta RESTful Web Services除外) 包名全部去javax.*化,全部改为jakarta.* JavaSE基准版本要求依旧保持为Java 8(而并非Java9哦) 可以发现本次升级的主要目的并着眼于功能点,仍旧是名字的替换。虽然大家对Java EE的javax有较深的情节,但旧的不去新的不来。我们以后开发过中遇到jakarta.*这种包名就不用再感到惊讶了,提前准备总是好的。 Jakarta Bean Validation Jakarta Bean Validation不仅仅是一个规范,它还是一个生态。 之前名为Java Bean Validation,2018年03月之后就得改名叫Jakarta Bean Validation喽,这不官网早已这么称呼了: Bean Validation技术隶属于Java EE规范,期间有多个JSR(Java Specification Requests)支持,截止到稿前共有三次JSR标准发布:说明:JCP这个组织就是来定义Java标准的,在Java行业鼎鼎有名的公司大都是JCP的成员,可以共同参与Java标准的制定,影响着世界。包括掌门人Oracle以及Eclipse、Redhat、JetBrains等等。值得天朝人自豪的是:2018年5月17日阿里巴巴作为一员正式加入JCP组织,成为唯一一家中国公司。 Bean Validation是标准,它的参考实现除了有我们熟悉的Hibernate Validator外还有Apache BVal,但是后者使用非常小众,忘了它吧。实际使用中,基本可以认为Hibernate Validator是Bean Validation规范的唯一参考实现,是对等的。 小贴士:Apache BVal胜在轻量级上,只有不到1m空间所以非常轻量,有些选手还是忠爱的(此项目还在发展中,并未停更哦,有兴趣你可以自己使用试试) JSR303 这个JSR提出很早了(2009年),它为 基于注解的 JavaBean验证定义元数据模型和API,通过使用XML验证描述符覆盖和扩展元数据。JSR-303主要是对JavaBean进行验证,如方法级别(方法参数/返回值)、依赖注入等的验证是没有指定的。 作为开山之作,它规定了Java数据校验的模型和API,这就是Java Bean Validation 1.0版本。 <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.0.0.GA</version> </dependency> 该版本提供了常见的校验注解(共计13个): 注解 支持类型 含义 null值是否校验 @AssertFalse bool 元素必须是false 否 @AssertTrue bool 元素必须是true 否 @DecimalMax Number的子类型(浮点数除外)以及String 元素必须是一个数字,且值必须<=最大值 否 @DecimalMin 同上 元素必须是一个数字,且值必须>=最大值 否 @Max 同上 同上 否 @Min 同上 同上 否 @Digits 同上 元素构成是否合法(整数部分和小数部分) 否 @Future 时间类型(包括JSR310) 元素必须为一个将来(不包含相等)的日期(比较精确到毫秒) 否 @Past 同上 元素必须为一个过去(不包含相等)的日期(比较精确到毫秒) 否 @NotNull any 元素不能为null 是 @Null any 元素必须为null 是 @Pattern 字符串 元素需符合指定的正则表达式 否 @Size String/Collection/Map/Array 元素大小需在指定范围中 否 所有注解均可标注在:方法、字段、注解、构造器、入参等几乎任何地方 可以看到这些注解均为平时开发中比较常用的注解,但是在使用过程中有如下事项你仍旧需要注意: 以上所有注解对null是免疫的,也就是说如果你的值是null,是不会触发对应的校验逻辑的(也就说null是合法的),当然喽@NotNull / @Null除外 对于时间类型的校验注解(@Future/@Past),是开区间(不包含相等)。也就是说:如果相等就是不合法的,必须是大于或者小于 这种case比较容易出现在LocalDate这种只有日期上面,必须是将来/过去日期,当天属于非法日期 @Digits它并不规定数字的范围,只规定了数字的结构。如:整数位最多多少位,小数位最多多少位 @Size规定了集合类型的范围(包括字符串),这个范围是闭区间 @DecimalMax和@Max作用基本类似,大部分情况下可通用。不同点在于:@DecimalMax设置最大值是用字符串形式表示(只要合法都行,比如科学计数法),而@Max最大值设置是个long值 我个人一般用@Max即可,因为够用了~ 另外可能有人会问:为毛没看见@NotEmpty、@Email、@Positive等常用注解?那么带着兴趣和疑问,继续往下看吧~ JSR349 该规范是2013年完成的,伴随着Java EE 7一起发布,它就是我们比较熟悉的Bean Validation 1.1。 <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> 相较于1.0版本,它主要的改进/优化有如下几点: 标准化了Java平台的约束定义、描述、和验证 支持方法级验证(入参或返回值的验证) Bean验证组件的依赖注入 与上下文和DI依赖注入集成 使用EL表达式的错误消息插值,让错误消息动态化起来(强依赖于ElManager) 跨参数验证。比如密码和验证密码必须相同 小贴士:注解个数上,相较于1.0版本并没新增~ 它的官方参考实现如下:可以看到,Java Bean Validation 1.1版本实现对应的是Hibernate Validator 5.x(1.0版本对应的是4.x) <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.4.3.Final</version> </dependency> 当你导入了hibernate-validator后,无需再显示导入javax.validation。hibernate-validator 5.x版本基本已停更,只有严重bug才会修复。因此若非特殊情况,不再建议你使用此版本,也就是不建议再使用Bean Validation 1.1版本,更别谈1.0版本喽。 小贴士:Spring Boot1.5.x默认集成的还是Bean Validation 1.1哦,但到了Boot 2.x后就彻底摒弃了老旧版本 JSR380 当下主流版本,也就是我们所说的Java Bean Validation 2.0和Jakarta Bean Validation 2.0版本。关于这两种版本的差异,官方做出了解释:他俩除了叫法不一样、除了GAV上有变化,其它地方没任何改变。它们各自的GAV如下: <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>2.0.1</version> </dependency> 现在应该不能再叫Java EE了,而应该是Jakarta EE。两者是一样的意思,你懂的。Jakarta Bean Validation 2.0是在2019年8月发布的,属于Jakarta EE 8的一部分。它的官方参考实现只有唯一的Hibernate validator了: 此版本具有很重要的现实意义,它主要提供如下亮点: 支持通过注解参数化类型(泛型类型)参数来验证容器内的元素,如:List<@Positive Integer> positiveNumbers 更灵活的集合类型级联验证;例如,现在可以验证映射的值和键,如:Map<@Valid CustomerType, @Valid Customer> customersByType 支持java.util.Optional类型,并且支持通过插入额外的值提取器来支持自定义容器类型 让@Past/@Future注解支持注解在JSR310时间上 新增内建的注解类型(共9个):@Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent和@FutureOrPresent 所有内置的约束现在都支持重复标记 使用反射检索参数名称,也就是入参名,详见这个API:ParameterNameProvider 很明显这是需要Java 8的启动参数支持的 Bean验证XML描述符的名称空间已更改为: META-INF/validation.xml -> http://xmlns.jcp.org/xml/ns/validation/configuration mapping files -> http://xmlns.jcp.org/xml/ns/validation/mapping JDK最低版本要求:JDK 8 Hibernate Validator自6.x版本开始对JSR 380规范提供完整支持,除了支持标准外,自己也做了相应的优化,比如性能改进、减少内存占用等等,因此用最新的版本肯定是没错的,毕竟只会越来越好嘛。 新增注解 相较于1.x版本,2.0版本在其基础上新增了9个实用注解,总数到了22个。现对新增的9个注解解释如下: 注解 支持类型 含义 null值是否校验 @Email 字符串 元素必须为电子邮箱地址 否 @NotEmpty 容器类型 集合的Size必须大于0 是 @NotBlank 字符串 字符串必须包含至少一个非空白的字符 是 @Positive 数字类型 元素必须为正数(不包括0) 否 @PositiveOrZero 同上 同上(包括0) 否 @Negative 同上 元素必须为负数(不包括0) 否 @NegativeOrZero 同上 同上(包括0) 否 @PastOrPresent 时间类型 在@Past基础上包括相等 否 @FutureOrPresent 时间类型 在@Futrue基础上包括相等 否 像@Email、@NotEmpty、@NotBlank之前是Hibernate额外提供的,2.0标准后hibernate自动退位让贤并且标注为过期了。Bean Validation 2.0的JSR规范制定负责人就职于Hibernate,所以这么做就很自然了。就是他: 小贴士:除了JSR标准提供的这22个注解外,Hibernate Validator还提供了一些非常实用的注解,这在后面讲述Hibernate Validator时再解释吧 使用示例 导入实现包: <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.5.Final</version> </dependency> 校验Java Bean 书写JavaBean和校验程序(全部使用JSR标准API哦): @ToString @Setter @Getter public class Person { @NotNull public String name; @NotNull @Min(0) public Integer age; } public static void main(String[] args) { Person person = new Person(); person.setAge(-1); // 1、使用【默认配置】得到一个校验工厂 这个配置可以来自于provider、SPI提供 ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); // 2、得到一个校验器 Validator validator = validatorFactory.getValidator(); // 3、校验Java Bean(解析注解) 返回校验结果 Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出校验结果 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println); } 运行程序,不幸抛错: Caused by: java.lang.ClassNotFoundException: javax.el.ELManager at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) ... 上面说了,从1.1版本起就需要El管理器支持用于错误消息动态插值,因此需要自己额外导入EL的实现。 小贴士:EL也属于Java EE标准技术,可认为是一种表达式语言工具,它并不仅仅是只能用于Web(即使你绝大部分情况下都是用于web的jsp里),可以用于任意地方(类比Spring的SpEL) 这是EL技术规范的API: <!-- 规范API --> <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>3.0.0</version> </dependency> Expression Language 3.0表达式语言规范发版于2013-4-29发布的,Tomcat 8、Jetty 9、GlasshFish 4都已经支持实现了EL 3.0,因此随意导入一个都可(如果你是web环境,根本就不用自己手动导入这玩意了)。 <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>9.0.22</version> </dependency> 添加好后,再次运行程序,控制台正常输出校验失败的消息: age 最小不能小于0: -1 name 不能为null: null 校验方法/校验构造器 请移步下文详解。 加餐:Bean Validation 3.0 伴随着Jakarta EE 9的发布,Jakarta Bean Validation 3.0也正式公诸于世。 <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>3.0.0</version> </dependency> 它最大的改变,甚至可以说唯一的改变就是包名的变化:至此不仅GAV上实现了更名,对代码执行有重要影响的包名也彻彻底底的去javax.*化了。因为实际的类并没有改变,因此仍旧可以认为它是JSR380的实现(虽然不再由JCP组织制定标准了)。 参考实现 毫无疑问,参考实现那必然是Hibernate Validator。它的步伐也跟得非常的紧,退出了7.x版本用于支持Jakarta Bean Validation 3.0。虽然是大版本号的升级,但是在新特性方面你可认为是无: ✍总结 本文着眼于讲解JSR规范、Bean Validation校验标准、官方参考实现Hibernate Validator,把它们之间的关系进行了关联,并且对差异进行了鉴别。我认为这篇文章对一般读者来说是能够刷新对数据校验的认知的。 wow,数据校验背后还有这么广阔的天地 数据校验是日常工组中接触非常非常频繁的一块知识点,我认为掌握它并且熟练运用于实际工作中,能起到事半功倍的效果,让代码更加的优雅,甚至还能实现别人加班你加薪呢。所以又是一个投出产出比颇高的小而美专栏在路上...... 作为本专栏的第一篇文章以JSR标准作为切入点进行讲解,是希望理论和实践能结合起来学习,毕竟理论的指导作用不可或缺。有了理论铺垫的基石,后面实践将更加流畅,正所谓着地走路更加踏实嘛。 ✔推荐阅读: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑 7. Jackson用树模型处理JSON是必备技能,不信你看
基础不牢,地动山摇。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 本号正在连载Jackson深度解析系列,虽然目前还只讲到了其流式API层面,但已接触到其多个Feature特征。更为重要的是我在文章里赞其设计精妙,处理优雅,因此就有小伙伴私信给我问这样的话:题外话:Jackson这个话题本就非常小众,看着阅读量我自己都快没信心写下去。但自己说过的话就是欠下的债,熬夜也得把承诺的付费内容给公开完了,毕竟还有那么几个人在白嫖不是。 话外音:以后闷头做事,少吹牛逼┭┮﹏┭┮ 虽然小众,竟然还有想深入了解一波的小伙伴,确实让我为之振奋了那么三秒。既然如此那就干吧,本文就先行来认识认识Java中的位运算。位运算在Java中很少被使用,那么为何Jackson里爱不释手呢?一切就为两字:性能/高效。用计算机能直接看懂的语言跟它打交道,你说快不快,不用多想嘛。 ✍正文 提及位运算,对绝大多数Java程序员来说,是一种既熟悉又陌生的感觉。熟悉是因为你在学JavaSE时肯定学过,并且在看一些开源框架(特别是JDK源码)时都能看到它的身影;陌生是因为大概率我们不会去使用它。当然,不能“流行”起来是有原因的:不好理解,不符合人类的思维,阅读性差…... 小贴士:一般来说,程序让人看懂远比被机器看懂来得更重要些 位运算它在low-level的语言里使用得比较多,但是对于Java这种高级语言它就很少被提及了。虽然我们使用得很少但Java也是支持的,毕竟很多时候使用位运算才是最佳实践。 位运算在日常开发中使用得较少,但是巧妙的使用位运算可以大量减少运行开销,优化算法。一条语句可能对代码没什么影响,但是在高重复,大数据量的情况下将会节省很多开销。 二进制 在了解什么是位运算之前,十分有必要先科普下二进制的概念。 二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是逢二进一,借位规则是借一当二。因为它只使用0、1两个数字符号,非常简单方便,易于用电子方式实现。 小贴士:半导体开代表1,关代表0,这也就是CPU计算的最底层原理 先看一个例子: 求 1011(二进制)+ 11(二进制) 的和? 结果为:1110(二进制) 二进制理解起来非常非常的简单,比10进制简单多了。你可能还会思考二进制怎么和十进制互转呢?毕竟1110这个也看不到啊。有或者往深了继续思考:如何转为八进制、十六进制、三十二进制......进制转换并非本文所想讲述的内容,请有兴趣者自行度娘。 二进制与编码 这个虽然和本文内容关联系并不是很大,但顺带捞一捞,毕竟编码问题在开发中还是比较常见的。 计算机能识别的只有1和0,也就是二进制,1和0可以表达出全世界的所有文字和语言符号。那如何表达文字和符号呢?这就涉及到字符编码了。字符编码强行将每一个字符对应一个十进制数字(请注意字符和数字的区别,比如0字符对应的十进制数字是48),再将十进制数字转换成计算机理解的二进制,而计算机读到这些1和0之后就会显示出对应的文字或符号。 一般对英文字符而言,一个字节表示一个字符,但是对汉字而言,由于低位的编码已经被使用(早期计算机并不支持中文,因此为了扩展支持,唯一的办法就是采用更多的字节数)只好向高位扩展 字符集编码的范围utf-8>gbk>iso-8859-1(latin1)>ascll。ascll编码是美国标准信息交换码的英文缩写,包含了常用的字符,如阿拉伯数字,英文字母和一些打印符号共255个(一般说成共128个字符问题也不大) UTF-8:一套以 8 位为一个编码单位的可变长编码,会将一个码位(Unicode)编码为1到4个字节(英文1字节,大部分汉字3字节)。 Java中的二进制 在Java7版本以前,Java是不支持直接书写除十进制以外的其它进制字面量。但这在Java7以及以后版本就允许了: 二进制:前置0b/0B 八进制:前置0 十进制:默认的,无需前置 十六进制:前置0x/0X @Test public void test1() { //二进制 int i = 0B101; System.out.println(i); //5 System.out.println(Integer.toBinaryString(i)); //八进制 i = 0101; System.out.println(i); //65 System.out.println(Integer.toBinaryString(i)); //十进制 i = 101; System.out.println(i); //101 System.out.println(Integer.toBinaryString(i)); //十六进制 i = 0x101; System.out.println(i); //257 System.out.println(Integer.toBinaryString(i)); } 结果程序,输出: 5 101 65 1000001 101 1100101 257 100000001 说明:System.out.println()会先自动转为10进制后再输出的;toBinaryString()表示转换为二进制进行字符串进行输出。 便捷的进制转换API JDK自1.0开始便提供了非常便捷的进制转换的API,这在我们有需要时非常有用。 @Test public void test2() { int i = 192; System.out.println("---------------------------------"); System.out.println("十进制转二进制:" + Integer.toBinaryString(i)); //11000000 System.out.println("十进制转八进制:" + Integer.toOctalString(i)); //300 System.out.println("十进制转十六进制:" + Integer.toHexString(i)); //c0 System.out.println("---------------------------------"); // 统一利用的为Integer的valueOf()方法,parseInt方法也是ok的 System.out.println("二进制转十进制:" + Integer.valueOf("11000000", 2).toString()); //192 System.out.println("八进制转十进制:" + Integer.valueOf("300", 8).toString()); //192 System.out.println("十六进制转十进制:" + Integer.valueOf("c0", 16).toString()); //192 System.out.println("---------------------------------"); } 运行程序,输出: --------------------------------- 十进制转二进制:11000000 十进制转八进制:300 十进制转十六进制:c0 --------------------------------- 二进制转十进制:192 八进制转十进制:192 十六进制转十进制:192 --------------------------------- 如何证明Long是64位的? 我相信每个Javaer都知道Java中的Long类型占8个字节(64位),那如何证明呢? 小贴士:这算是一道经典面试题,至少我提问过多次~ 有个最简单的方法:拿到Long类型的最大值,用2进制表示转换成字符串看看长度就行了,代码如下: @Test public void test3() { long l = 100L; //如果不是最大值 前面都是0 输出的时候就不会有那么长了(所以下面使用最大/最小值示例) System.out.println(Long.toBinaryString(l)); //1100100 System.out.println(Long.toBinaryString(l).length()); //7 System.out.println("---------------------------------------"); l = Long.MAX_VALUE; // 2的63次方 - 1 //正数长度为63为(首位为符号位,0代表正数,省略了所以长度是63) //111111111111111111111111111111111111111111111111111111111111111 System.out.println(Long.toBinaryString(l)); System.out.println(Long.toBinaryString(l).length()); //63 System.out.println("---------------------------------------"); l = Long.MIN_VALUE; // -2的63次方 //负数长度为64位(首位为符号位,1代表负数) //1000000000000000000000000000000000000000000000000000000000000000 System.out.println(Long.toBinaryString(l)); System.out.println(Long.toBinaryString(l).length()); //64 } 运行程序,输出: 1100100 7 --------------------------------------- 111111111111111111111111111111111111111111111111111111111111111 63 --------------------------------------- 1000000000000000000000000000000000000000000000000000000000000000 64 说明:在计算机中,负数以其正值的补码的形式表达。因此,用同样的方法你可以自行证明Integer类型是32位的(占4个字节)。 Java中的位运算 Java语言支持的位运算符还是非常多的,列出如下: &:按位与 |:按位或 ^:按位异或 <<:左位移运算符 >>:右位移运算符 >>>:无符号右移运算符 除~以 外,其余均为二元运算符,操作的数据只能是整型(长短均可)或者char字符型。针对这些运算类型,下面分别给出示例,一目了然。 既然是运算,依旧可以分为简单运算和复合运算两大类进行归类和讲解。 小贴士:为了便于理解,字面量例子我就都使用二进制表示了,使用十进制(任何进制)不影响运算结果 简单运算 简单运算,顾名思义,一次只用一个运算符。 &:按位与 操作规则:同为1则1,否则为0。仅当两个操作数都为1时,输出结果才为1,否则为0。 说明:1、本示例(下同)中所有的字面值使用的都是十进制表示的,理解的时候请用二进制思维去理解;2、关于负数之间的位运算本文章统一不做讲述 @Test public void test() { int i = 0B100; // 十进制为4 int j = 0B101; // 十进制为5 // 二进制结果:100 // 十进制结果:4 System.out.println("二进制结果:" + Integer.toBinaryString(i & j)); System.out.println("十进制结果:" + (i & j)); } |:按位或 操作规则:同为0则0,否则为1。仅当两个操作数都为0时,输出的结果才为0。 @Test public void test() { int i = 0B100; // 十进制为4 int j = 0B101; // 十进制为5 // 二进制结果:101 // 十进制结果:5 System.out.println("二进制结果:" + Integer.toBinaryString(i | j)); System.out.println("十进制结果:" + (i | j)); } ~:按位非 操作规则:0为1,1为0。全部的0置为1,1置为0。 小贴士:请务必注意是全部的,别忽略了正数前面的那些0哦~ @Test public void test() { int i = 0B100; // 十进制为4 // 二进制结果:11111111111111111111111111111011 // 十进制结果:-5 System.out.println("二进制结果:" + Integer.toBinaryString(~i)); System.out.println("十进制结果:" + (~i)); } ^:按位异或 操作规则:相同为0,不同为1。操作数不同时(1遇上0,0遇上1)对应的输出结果才为1,否则为0。 @Test public void test() { int i = 0B100; // 十进制为4 int j = 0B101; // 十进制为5 // 二进制结果:1 // 十进制结果:1 System.out.println("二进制结果:" + Integer.toBinaryString(i ^ j)); System.out.println("十进制结果:" + (i ^ j)); } <<:按位左移 操作规则:把一个数的全部位数都向左移动若干位。 @Test public void test() { int i = 0B100; // 十进制为4 // 二进制结果:100000 // 十进制结果:32 = 4 * (2的3次方) System.out.println("二进制结果:" + Integer.toBinaryString(i << 2)); System.out.println("十进制结果:" + (i << 3)); } 左移用得非常多,理解起来并不费劲。x左移N位,效果同十进制里直接乘以2的N次方就行了,但是需要注意值溢出的情况,使用时稍加注意。 >>:按位右移 操作规则:把一个数的全部位数都向右移动若干位。 @Test public void test() { int i = 0B100; // 十进制为4 // 二进制结果:10 // 十进制结果:2 System.out.println("二进制结果:" + Integer.toBinaryString(i >> 1)); System.out.println("十进制结果:" + (i >> 1)); } 负数右移: @Test public void test() { int i = -0B100; // 十进制为-4 // 二进制结果:11111111111111111111111111111110 // 十进制结果:-2 System.out.println("二进制结果:" + Integer.toBinaryString(i >> 1)); System.out.println("十进制结果:" + (i >> 1)); } 右移用得也比较多,也比较理解:操作其实就是把二进制数右边的N位直接砍掉,然后正数右移高位补0,负数右移高位补1。 >>>:无符号右移 注意:没有无符号左移,并没有<<<这个符号的 它和>>有符号右移的区别是:无论是正数还是负数,高位通通补0。所以说对于正数而言,没有区别;那么看看对于负数的表现: @Test public void test() { int i = -0B100; // 十进制为-4 // 二进制结果:11111111111111111111111111111110(>>的结果) // 二进制结果:1111111111111111111111111111110(>>>的结果) // 十进制结果:2147483646 System.out.println("二进制结果:" + Integer.toBinaryString(i >>> 1)); System.out.println("十进制结果:" + (i >>> 1)); } 我特意把>>的结果放上面了,方便你对比。因为高位补的是0,所以就没有显示啦,但是你心里应该清楚是怎么回事。 复合运算 广义上的复合运算指的是多个运算嵌套起来,通常这些运算都是同种类型的。这里指的复合运算指的就是和=号一起来使用,类似于+= -=。本来这属于基础常识不用做单独解释,但谁让A哥管生管养,管杀管埋呢。 混合运算:指同一个算式里包含了bai多种运算符,如加减乘除乘方开du方等。 以&与运算为例,其它类同: @Test public void test() { int i = 0B110; // 十进制为6 i &= 0B11; // 效果同:i = i & 3 // 二进制结果:10 // 十进制结果:2 System.out.println("二进制结果:" + Integer.toBinaryString(i)); System.out.println("十进制结果:" + (i)); } 复习一下&的运算规则是:同为1则1,否则为0。 位运算使用场景示例 位运算除了高效的特点,还有一个特点在应用场景下不容忽视:计算的可逆性。通过这个特点我们可以用来达到隐蔽数据的效果,并且还保证了效率。 在JDK的原码中。有很多初始值都是通过位运算计算的。最典型的如HashMap: HashMap: static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 static final int MAXIMUM_CAPACITY = 1 << 30; 位运算有很多优良特性,能够在线性增长的数据中起到作用。且对于一些运算,位运算是最直接、最简便的方法。下面我安排一些具体示例(一般都是面试题),感受一把。 判断两个数字符号是否相同 同为正数or同为负数都表示相同,否则为不同。像这种小小case用十进制加上>/<比较符当然可以做,但用位运算符处理来得更加直接(效率最高): @Test public void test4() { int i = 100; int j = -2; System.out.println(((i >> 31) ^ (j >> 31)) == 0); j = 10; System.out.println(((i >> 31) ^ (j >> 31)) == 0); } 运行程序,输出: false true int类型共32bit,右移31位那么就只剩下1个符号位了(因为是带符号右移动,所以正数剩0负数剩1),再对两个符号位做^异或操作结果为0就表明二者一致。 复习一下^异或操作规则:相同为0,不同为1。 判断一个数的奇偶性 在十进制数中可以通过和2取余来做,对于位运算有一个更为高效的方式: @Test public void test5() { System.out.println(isEvenNum(1)); //false System.out.println(isEvenNum(2)); //true System.out.println(isEvenNum(3)); //false System.out.println(isEvenNum(4)); //true System.out.println(isEvenNum(5)); //false } /** * 是否为偶数 */ private static boolean isEvenNum(int n) { return (n & 1) == 0; } 为何&1能判断基偶性?因为在二进制下偶数的末位肯定是0,奇数的最低位肯定是1。而二进制的1它的前31位均为0,所以在和其它数字的前31位与运算后肯定所有位数都是0(无论是1&0还是0&0结果都是0),那么唯一区别就是看最低位和1进行与运算的结果喽:结果为1表示奇数,反则结果为0就表示偶数。 交换两个数的值(不借助第三方变量) 这是一个很古老的面试题了,交换A和B的值。本题如果没有括号里那几个字,是一道大家都会的题目,可以这么来解: @Test public void test6() { int a = 3, b = 5; System.out.println(a + "-------" + b); a = a + b; b = a - b; a = a - b; System.out.println(a + "-------" + b); } 运行程序,输出(成功交换): 3-------5 5-------3 使用这种方式最大的好处是:容易理解。最大的坏处是:a+b,可能会超出int型的最大范围,造成精度丢失导致错误,造成非常隐蔽的bug。所以若你这样运用在生产环境的话,是有比较大的安全隐患的。 小贴士:如果你们评估数字绝无可能超过最大值,这种做法尚可。当然如果你是字符串类型,请当我没说 因为这种方式既引入了第三方变量,又存在重大安全隐患。所以本文介绍一种安全的替代方式,借助位运算的可逆性来完成操作: @Test public void test7() { // 这里使用最大值演示,以证明这样方式是不会溢出的 int a = Integer.MAX_VALUE, b = Integer.MAX_VALUE - 10; System.out.println(a + "-------" + b); a = a ^ b; b = a ^ b; a = a ^ b; System.out.println(a + "-------" + b); } 运行程序,输出(成功完成交换): 2147483647-------2147483637 2147483637-------2147483647 由于全文都没有对a/b做加法运算,因此不能出现溢出现象,所以是安全的。这种做法的核心原理依据是:位运算的可逆性,使用异或来达成目的。 位运算用在数据库字段上(重要) 这个使用case是极具实际应用意义的,因为在生产上我以用过多次,感觉不是一般的好。 业务系统中数据库设计的尴尬现象:通常我们的数据表中可能会包含各种状态属性, 例如 blog表中,我们需要有字段表示其是否公开,是否有设置密码,是否被管理员封锁,是否被置顶等等。 也会遇到在后期运维中,策划要求增加新的功能而造成你需要增加新的字段,这样会造成后期的维护困难,字段过多,索引增大的情况, 这时使用位运算就可以巧妙的解决。 举个例子:我们在网站上进行认证授权的时候,一般支持多种授权方式,比如: 个人认证 0001 -> 1 邮箱认证 0010 -> 2 微信认证 0100 -> 4 超管认证 1000 -> 8 这样我们就可以使用1111这四位来表达各自位置的认证与否。要查询通过微信认证的条件语句如下: select * from xxx where status = status & 4; 要查询既通过了个人认证,又通过了微信认证的: select * from xxx where status = status & 5; 当然你也可能有排序需求,形如这样: select * from xxx order by status & 1 desc 这种case和每个人都熟悉的Linux权限控制一样,它就是使用位运算来控制的:权限分为 r 读, w 写, x 执行,其中它们的权值分别为4,2,1,你可以随意组合授权。比如 chomd 7,即7=4+2+1表明这个用户具有rwx权限, 注意事项 需要你的DB存储支持位运算,比如MySql是支持的 请确保你的字段类型不是char字符类型,而应该是数字类型 这种方式它会导致索引失效,但是一般情况下状态值是不需要索引的 具体业务具体分析,别一味地为了show而用,若用错了容易遭对有喷的 流水号生成器(订单号生成器) 生成订单流水号,当然这其实这并不是一个很难的功能,最直接的方式就是日期+主机Id+随机字符串来拼接一个流水号,甚至看到非常多的地方直接使用UUID,当然这是非常不推荐的。 UUID是字符串,太长,无序,不能承载有效的信息从而不能给定位问题提供有效帮助,因此一般属于备选方案 今天学了位运算,有个我认为比较优雅方式来实现。什么叫优雅:可以参考淘宝、京东的订单号,看似有规律,实则没规律: 不想把相关信息直接暴露出去。 通过流水号可以快速得到相关业务信息,快速定位问题(这点非常重要,这是UUID不建议使用的最重要原因)。 使用AtomicInteger可提高并发量,降低了冲突(这是不使用UUID另一重要原因,因为数字的效率比字符串高) 实现原理简介 此流水号构成:日期+Long类型的值 组成的一个一长串数字,形如2020010419492195304210432。很显然前面是日期数据,后面的一长串就蕴含了不少的含义:当前秒数、商家ID(也可以是你其余的业务数据)、机器ID、一串随机码等等。 各部分介绍: 第一部分为当前时间的毫秒值。最大999,所以占10位 第二部分为:serviceType表示业务类型。比如订单号、操作流水号、消费流水号等等。最大值定为30,足够用了吧。占5位 第三部分为:shortParam,表示用户自定义的短参数。可以放置比如订单类型、操作类型等等类别参数。最大值定为30,肯定也是足够用了的。占5位 第四部分为:longParam,同上。用户一般可放置id参数,如用户id、商家id等等,最大支持9.9999亿。绝大多数足够用了,占30位 第五部分:剩余的位数交给随机数,随机生成一个数,占满剩余位数。一般至少有15位剩余(此部分位数是浮动的),所以能支持2的15次方的并发,也是足够用了的 最后,在上面的long值前面加上日期时间(年月日时分秒) 这是A哥编写的一个基于位运算实现的流水号生成工具,已用于生产环境。考虑到源码较长(一个文件,共200行左右,无任何其它依赖)就不贴了,若有需要,请到公众号后台回复流水号生成器免费获取。 ✍总结 位运算在工程的角度里缺点还是蛮多的,在实际工作中,如果只是为了数字的计算,是不建议使用位运算符的,只有一些比较特殊的场景,使用位运算去做会给你柳暗花明的感觉,如: N多状态的控制,需要兼具扩展性。比如数据库是否状态的字段设计 对效率有极致要求。比如JDK 场景非常适合。比如Jackson的Feature特针值 切忌为了炫(zhuang)技(bi)而使用,炫技一时爽,掉坑火葬场;小伙还年轻,还望你谨慎。代码在大多情况下,人能容易读懂比机器能读懂来得更重要。 ✔推荐阅读: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑
每棵大树,都曾只是一粒种子。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 ✍前言 你好,我是YourBatman。 上篇文章 体验了一把ObjectMapper在数据绑定方面的应用,用起来还是蛮方便的有木有,为啥不少人说它难用呢,着实费解。我群里问了问,主要原因是它不是静态方法调用,并且方法名取得不那么见名之意...... 虽然ObjectMapper在数据绑定上既可以处理简单类型(如Integer、List、Map等),也能处理完全类型(如POJO),看似无所不能。但是,若有如下场景它依旧不太好实现: 硕大的JSON串中我只想要某一个(某几个)属性的值而已 临时使用,我并不想创建一个POJO与之对应,只想直接使用值即可(类型转换什么的我自己来就好) 数据结构高度动态化 为了解决这些问题,Jackson提供了强大的树模型 API供以使用,这也就是本文的主要的内容。 小贴士:树模型虽然是jackson-core模块里定义的,但是是由jackson-databind高级模块提供的实现 版本约定 Jackson版本:2.11.0 Spring Framework版本:5.2.6.RELEASE Spring Boot版本:2.3.0.RELEASE ✍正文 树模型可能比数据绑定更方便,更灵活。特别是在结构高度动态或者不能很好地映射到Java类的情况下,它就显得更有价值了。 树模型 树模型是JSON数据内存树的表示形式,这是最灵活的方法,它就类似于XML的DOM解析器。Jackson提供了树模型API来生成和解析 JSON串,主要用到如下三个核心类: JsonNodeFactory:顾名思义,用来构造各种JsonNode节点的工厂。例如对象节点ObjectNode、数组节点ArrayNode等等 JsonNode:表示json节点。可以往里面塞值,从而最终构造出一颗json树 ObjectMapper:实现JsonNode和JSON字符串的互转 这里有个萌新的概念:JsonNode。它贯穿于整个树模型中,所以有必要先来认识它。 JsonNode JSON节点,可类比XML的DOM树节点结构来辅助理解。JsonNode是所有JSON节点的基类,它是一个抽象类,它有一个较大的特点:绝大多数的get方法均放在了此抽象类里(即使它没有实现),目的是:在不进行类型强制转换的情况下遍历结构。但是,大多数的修改方法都必须通过特定的子类类型去调用,这其实是合理的。因为在构建/修改某个Node节点时,类型类型信息一般是明确的,而在读取Node节点时大多数时候并不 太关心节点类型。 多个JsonNode节点构成Jackson实现的JSON树模型的基础,它是流式API中com.fasterxml.jackson.core.TreeNode接口的实现,同时它还实现了Iterable迭代器接口。 public abstract class JsonNode extends JsonSerializable.Base implements TreeNode, Iterable<JsonNode> { ... } JsonNode的继承图谱如下(部分):一目了然了吧,基本上每个数据类型都会有一个JsonNode的实现类型对应。譬如数组节点ArrayNode、数字节点NumericNode等等。 一般情况下,我们并不需要通过new关键字去构建一个JsonNode实例,而是借助JsonNodeFactory工厂来做。 JsonNodeFactory 构建JsonNode工厂类。话不多说,用几个例子跑一跑。 值类型节点(ValueNode) 此类节点均为ValueNode的子类,特点是:一个节点表示一个值。 @Test public void test1() { JsonNodeFactory factory = JsonNodeFactory.instance; System.out.println("------ValueNode值节点示例------"); // 数字节点 JsonNode node = factory.numberNode(1); System.out.println(node.isNumber() + ":" + node.intValue()); // null节点 node = factory.nullNode(); System.out.println(node.isNull() + ":" + node.asText()); // missing节点 node = factory.missingNode(); System.out.println(node.isMissingNode() + "_" + node.asText()); // POJONode节点 node = factory.pojoNode(new Person("YourBatman", 18)); System.out.println(node.isPojo() + ":" + node.asText()); System.out.println("---" + node.isValueNode() + "---"); } 运行程序,输出: ------ValueNode值节点示例------ true:1 true:null true_ true:Person(name=YourBatman, age=18) ---true--- 容器类型节点(ContainerNode) 此类节点均为ContainerNode的子类,特点是:本节点代表一个容器,里面可以装任何其它节点。 Java中容器有两种:Map和Collection。对应的Jackson也提供了两种容器节点用于表述此类数据结构: ObjectNode:类比Map,采用K-V结构存储。比如一个JSON结构,根节点 就是一个ObjectNode ArrayNode:类比Collection、数组。里面可以放置任何节点 下面用示例感受一下它们的使用: @Test public void test2() { JsonNodeFactory factory = JsonNodeFactory.instance; System.out.println("------构建一个JSON结构数据------"); ObjectNode rootNode = factory.objectNode(); // 添加普通值节点 rootNode.put("zhName", "A哥"); // 效果完全同:rootNode.set("zhName", factory.textNode("A哥")) rootNode.put("enName", "YourBatman"); rootNode.put("age", 18); // 添加数组容器节点 ArrayNode arrayNode = factory.arrayNode(); arrayNode.add("java") .add("javascript") .add("python"); rootNode.set("languages", arrayNode); // 添加对象节点 ObjectNode dogNode = factory.objectNode(); dogNode.put("name", "大黄") .put("age", 3); rootNode.set("dog", dogNode); System.out.println(rootNode); System.out.println(rootNode.get("dog").get("name")); } 运行程序,输出: ------构建一个JSON结构数据------ {"zhName":"A哥","enName":"YourBatman","age":18,"languages":["java","javascript","python"],"dog":{"name":"大黄","age":3}} "大黄" ObjectMapper中的树模型 树模型其实是底层流式API所提出和支持的,典型API便是com.fasterxml.jackson.core.TreeNode。但通过前面文章的示例讲解可以知道:底层流式API仅定义了接口而并未提供任何实现,甚至半成品都算不上。所以说要使用Jackson的树模型还得看ObjectMapper,它提供了TreeNode等API的完整实现。 不乏很多小伙伴对ObjectMapper的树模型是一知半解的,甚至从来都没有用过,其实它是非常灵活和强大的。有了上面的基础示例做支撑,再来了解它的实现就得心应手多了。 ObjectMapper中提供了树模型(tree model) API 来生成和解析 json 字符串。如果你不想为你的 json 结构单独建类与之对应的话,则可以选择该 API,如下图所示:ObjectMapper在读取JSON后提供指向树的根节点的指针, 根节点可用于遍历完整的树。 同样的,我们可从读(反序列化)、写(序列化)两个方面来展开。 写(序列化) 将Object写为JsonNode,ObjectMapper给我们提供了三个实用API俩操作它: 1、valueToTree(Object) 该方法属相对较为常用:将任意对象(包括null)写为一个JsonNode树模型。功能上类似于先将Object序列化为JSON串,再读为JsonNode,但很明显这样一步到位更加高效。 小贴士:高效不代表性能高,因为其内部实现好还是调用了readTree()方法的 @Test public void test1() { ObjectMapper mapper = new ObjectMapper(); Person person = new Person(); person.setName("YourBatman"); person.setAge(18); person.setDog(new Person.Dog("旺财", 3)); JsonNode node = mapper.valueToTree(person); System.out.println(person); // 遍历打印所有属性 Iterator<JsonNode> it = node.iterator(); while (it.hasNext()) { JsonNode nextNode = it.next(); if (nextNode.isContainerNode()) { if (nextNode.isObject()) { System.out.println("狗的属性:::"); System.out.println(nextNode.get("name")); System.out.println(nextNode.get("age")); } } else { System.out.println(nextNode.asText()); } } // 直接获取 System.out.println("---------------------------------------"); System.out.println(node.get("dog").get("name")); System.out.println(node.get("dog").get("age")); } 运行程序,控制台输出: Person(name=YourBatman, age=18, dog=Person.Dog(name=旺财, age=3)) YourBatman 18 狗的属性::: "旺财" 3 --------------------------------------- "旺财" 3 对于JsonNode在这里补充一个要点:读取其属性,你既可以用迭代器遍历,也可以根据key(属性)直接获取,是不是和Map的使用几乎一毛一样? 2、writeTree(JsonGenerator, JsonNode) 顾名思义:将一个JsonNode使用JsonGenerator写到输出流里,此方法直接使用到了JsonGenerator这个API,灵活度杠杠的,但相对偏底层,本处仍旧给个示例玩玩吧(底层API更多详解,请参见本系列前面几篇文章): @Test public void test2() throws IOException { ObjectMapper mapper = new ObjectMapper(); JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // 1、得到一个jsonNode(为了方便我直接用上面API生成了哈) Person person = new Person(); person.setName("YourBatman"); person.setAge(18); JsonNode jsonNode = mapper.valueToTree(person); // 使用JsonGenerator写到输出流 mapper.writeTree(jsonGenerator, jsonNode); } } 运行程序,控制台输出: {"name":"YourBatman","age":18,"dog":null} 3、writeTree(JsonGenerator,TreeNode) JsonNode是TreeNode的实现类,上面方法已经给出了使用示例,所以本方法不在赘述你应该不会有意见了吧。 读(反序列化) 将一个资源(如字符串)读取为一个JsonNode树模型。这是典型的方法重载设计,API更加友好,所有方法底层均为_readTreeAndClose()这个protected方法,可谓“万剑归宗”。 下面以最为常见的:读取JSON字符串为例,其它的举一反三即可。 @Test public void test3() throws IOException { ObjectMapper mapper = new ObjectMapper(); String jsonStr = "{\"name\":\"YourBatman\",\"age\":18,\"dog\":null}"; // 直接映射为一个实体对象 // mapper.readValue(jsonStr, Person.class); // 读取为一个树模型 JsonNode node = mapper.readTree(jsonStr); // ... 略 } 至于底层_readTreeAndClose(JsonParser)方法的具体实现,就有得捞了。不过鉴于它过于枯燥和稍有些烧脑,后面撰有专文详解,有兴趣可持续关注。 场景演练 理论和示例讲完了,光说不练假把式,下面A哥根据经验,举两个树模型的实际使用示例供你参考。 1、偌大JSON串中仅需1个值 这种场景其实还蛮常见的,比如有个很经典的场景便是在MQ消费中:生产者一般会恨不得把它能吐出来的属性尽可能都扔出来,但对于不同的消费者而言它们的所需往往是不一样的: 需要较多的属性值,这时候用完全数据绑定转换成POJO来操作更为方便和合理 需要1个(较少)的属性值,这时候“杀鸡岂能用牛刀”呢,这种case使用树模型来做就显得更为优雅和高效了 譬如,生产者生产的消息JSON串如下(模拟数据,总之你就当做它属性很多、嵌套很深就对了): {"name":"YourBatman","age":18,"dog":{"name":"旺财","color":"WHITE"},"hobbies":["篮球","football"]} 这时候,我仅关心狗的颜色,肿么办呢?相信你已经想到了:树模型 @Test public void test4() throws IOException { ObjectMapper mapper = new ObjectMapper(); String jsonStr = "{\"name\":\"YourBatman\",\"age\":18,\"dog\":{\"name\":\"旺财\",\"color\":\"WHITE\"},\"hobbies\":[\"篮球\",\"football\"]}"; JsonNode node = mapper.readTree(jsonStr); System.out.println(node.get("dog").get("color").asText()); } 运行程序,控制台输出:WHITE,目标达成。值得注意的是:如果node.get("dog")没有这个节点(或者值为null),是会抛出NPE异常的,因此请你自己保证代码的健壮性。 当你不想创建一个Java Bean与JSON属性相对应时,树模型的所见即所得特性就很好解决了这个问题。 2、数据结构高度动态化 当数据结构高度动态化(随时可能新增、删除节点)时,使用树模型去处理是一个较好的方案(稳定之后再转为Java Bean即可)。这主要是利用了树模型它具有动态可扩展的特性,满足我们日益变化的结构: @Test public void test5() throws JsonProcessingException { String jsonStr = "{\"name\":\"YourBatman\",\"age\":18}"; JsonNode node = new ObjectMapper().readTree(jsonStr); System.out.println("-------------向结构里动态添加节点------------"); // 动态添加一个myDiy节点,并且该节点还是ObjectNode节点 ((ObjectNode) node).with("myDiy").put("contry", "China"); System.out.println(node); } 运行程序,控制台输出: -------------向结构里动态添加节点------------ {"name":"YourBatman","age":18,"myDiy":{"contry":"China"}} 说白了,也没啥特殊的。拿到一个JsonNode后你可以任意的造它,就像Map<Object,Object>一样~ ✍总结 树模型(tree model) API比Jackson 流式(Streaming) API 简单了很多,不管是生成 json字符串还是解析json字符串。但是相对于自动化的数据绑定而言还是比较复杂的。 树模型(tree model) API在只需要取出一个大json串中的几个值时比较方便。如果json中每个(大部分)值都需要获得,那么这种方式便显得比较繁琐了。因此在实际应用中具体问题具体分析,但是,Jackson的树模型你必须得掌握。 ✔推荐阅读: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下 5. JsonFactory工厂而已,还蛮有料,这是我没想到的 6. 二十不惑,ObjectMapper使用也不再迷惑
少年易学老难成,一寸光阴不可轻。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位好,我是YourBatman。前面用四篇文章介绍完了Jackson底层流式API的读(JsonParser)、写(JsonGenerator)操作,我们清楚的知道,这哥俩都是abstract抽象类,使用时并没有显示的去new它们的(子类)实例,均通过一个工厂来搞定,这便就是本文的主角JsonFactory。 通过名称就知道,这是工厂设计模式。Jackson它并不建议你直接new读/写实例,因为那过于麻烦。为了对使用者屏蔽这些复杂的构造细节,于是就有了JsonFactory实例工厂的出现。 可能有的人会说,一个对象工厂有什么好了解的,很简单嘛。非也非也,一件事情本身的复杂度并不会凭空消失,而是从一个地方转移到另外一个地方,这另外一个地方指的就是JsonFactory。因此按照本系列的定位,了解它你绕不过去。 版本约定 Jackson版本:2.11.0 Spring Framework版本:5.2.6.RELEASE Spring Boot版本:2.3.0.RELEASE 正文 JsonFactory是Jackson的(最)主要工厂类,用于 配置和构建JsonGenerator和JsonParser,这个工厂实例是线程安全的,因此可以重复使用。 作为一个实例工厂,它最重要的职责当然是创建实例对象。本工厂职责并不单一,它负责读、写两种实例的创建工作。 创建JsonGenerator实例 JsonGenerator它负责向目的地写数据,因此强调的是目的地在哪?如何写? 如截图所示,一共有六个重载方法用于构建JsonGenerator实例,多个重载方法目的是对使用者友好,我们可以认为最终效果是一样的。比如,底层实现是: JsonFactory: @Override public JsonGenerator createGenerator(OutputStream out, JsonEncoding enc) throws IOException { IOContext ctxt = _createContext(out, false); ctxt.setEncoding(enc); // 如果编码是UTF-8 if (enc == JsonEncoding.UTF8) { return _createUTF8Generator(_decorate(out, ctxt), ctxt); } // 使用指定的编码把OutputStream包装为一个writer Writer w = _createWriter(out, enc, ctxt); return _createGenerator(_decorate(w, ctxt), ctxt); } 这就解释了,为何在详解JsonGenerator的这篇文章中,我一直以UTF8JsonGenerator作为实例进行讲解,因为例子中指定的编码就是UTF-8嘛。当然,即使你自己不显示的指定编码集,默认情况下Jackson也是使用UTF-8: JsonFactory: @Override public JsonGenerator createGenerator(OutputStream out) throws IOException { return createGenerator(out, JsonEncoding.UTF8); } 示例: @Test public void test1() throws IOException { JsonFactory jsonFactory = new JsonFactory(); JsonGenerator jsonGenerator1 = jsonFactory.createGenerator(System.out); JsonGenerator jsonGenerator2 = jsonFactory.createGenerator(System.out, JsonEncoding.UTF8); System.out.println(jsonGenerator1); System.out.println(jsonGenerator2); } 运行程序,输出: com.fasterxml.jackson.core.json.UTF8JsonGenerator@cb51256 com.fasterxml.jackson.core.json.UTF8JsonGenerator@59906517 创建JsonParser实例 JsonParser它负责从一个JSON字符串中提取出值,因此它强调的是数据从哪来?如何解析? 如截图所示,一共11个重载方法(其实最后一个不属于重载)用于构建JsonParser实例,它的底层实现是根据不同的数据媒介,使用了不同的处理方式,最终生成UTF8StreamJsonParser/ReaderBasedJsonParser。 你会发现这几个重载方法均无需我们指定编码集,那它是如何确定使用何种编码去解码形如byte[]数组这种数据来源的呢?这得益于其内部的编码自动发现机制实现,也就是ByteSourceJsonBootstrapper#detectEncoding()这个方法。 示例: @Test public void test2() throws IOException { JsonFactory jsonFactory = new JsonFactory(); JsonParser jsonParser1 = jsonFactory.createParser("{}"); // JsonParser jsonParser2 = jsonFactory.createParser(new FileReader("...")); JsonParser jsonParser3 = jsonFactory.createNonBlockingByteArrayParser(); System.out.println(jsonParser1); // System.out.println(jsonParser2); System.out.println(jsonParser3); } 运行程序,输出: com.fasterxml.jackson.core.json.ReaderBasedJsonParser@5f3a4b84 com.fasterxml.jackson.core.json.async.NonBlockingJsonParser@27f723 创建非阻塞实例 值得注意的是,上面截图的11个方法中,最后一个并非重载。它创建的是一个非阻塞JSON解析器,也就是NonBlockingJsonParser,并且它还没有指定入参(数据源)。 NonBlockingJsonParser是Jackson在2.9版本新增的的一个解析器,目标是进一步提升效率、性能。但它也有局限的地方:只能解析使用UTF-8编码的内容,否则抛出异常。 当然喽,现在UTF-8编码几乎成为了标准编码手段,问题不大。但是呢,我自己玩了玩NonBlockingJsonParser,发现复杂度增加不少(玩半天才玩明白),效果却并不显著,因此这里了解一下便可,至少目前不建议深入探究。 小贴士:不管是Spring还是Redis的反序列化,使用的均是普通的解析器(阻塞IO)。因为JSON解析过程从来都不会是性能瓶颈(特殊场景除外) JsonFactory的Feature 除了JsonGenerator和JsonParser有Feature来控制行为外,JsonFactory也有自己的Feature特征,来控制自己的行为,可以理解为它对读/写均生效。 同样的也是一个内部枚举类: public enum Feature { INTERN_FIELD_NAMES(true), CANONICALIZE_FIELD_NAMES(true), FAIL_ON_SYMBOL_HASH_OVERFLOW(true), USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING(true) } 小贴士:枚举值均为bool类型,括号内为默认值 每个枚举值都控制着JsonFactory不同的行为。 INTERN_FIELD_NAMES(true) 这是Jackson所谓的key缓存:对JSON的字段名是否调用String#intern方法,放进字符串常量池里,以提高效率,默认是true。 小贴士:Jackson在调用String#intern之前使用InternCache(继承自ConcurrentHashMap)挡了一层,以防止高并发条件下intern效果不显著问题 intern()方法的作用这个老生常谈的话题了,解释为:当调用intern方法时,如果字符串池已经包含一个等于此String对象的字符串(内容相等),则返回池中的字符串。否则,将此 String放进池子里。下面写个例子增加感受感受: @Test public void test2() { String str1 = "a"; String str2 = "b"; String str3 = "ab"; String str4 = str1 + str2; String str5 = new String("ab"); System.out.println(str5.equals(str3)); // true System.out.println(str5 == str3); // false // str5.intern()去常量池里找到了ab,所以直接返回常量池里的地址值了,因此是true System.out.println(str5.intern() == str3); // true System.out.println(str5.intern() == str4); // false } 可想而知,开启这个小功能的意义还是蛮大的。因为同一个格式的JSON串被多次解析的可能性是非常之大的,想想你的Rest API接口,被调用多少次就会进行了多少次JSON解析(想想高并发场景)。这是一种用空间换时间的思想,所以小小功能,大大能量。 小贴士:如果你的应用对内存很敏感,你可以关闭此特征。但,真的有这种应用吗?有吗? 值得注意的是:此特征必须是CANONICALIZE_FIELD_NAMES也为true(开启)的情况下才有效,否则是无效的。 CANONICALIZE_FIELD_NAMES(true) 是否需要规范化属性名。所谓的规范化处理,就是去字符串池里尝试找一个字符串出来,默认值为true。规范化借助的是ByteQuadsCanonicalizer去处理,简而言之会根据Hash值来计算每个属性名存放的位置~ 小贴士:ByteQuadsCanonicalizer拥有一套优秀的Hash算法来规范化属性存储,提高效率,抵御攻击(见下特征) 此特征开启了,INTERN_FIELD_NAMES特征的开启才有意义~ FAIL_ON_SYMBOL_HASH_OVERFLOW(true) 当ByteQuadsCanonicalizer处理hash碰撞达到一个阈值时,是否快速失败。 什么时候能达到阈值?官方的说明是:若触发了阈值,这基本可以确定是Dos(denial-of-service)攻击,制造了非常多的相同Hash值的key,这在正常情况下几乎是没有发生的可能性的。 所以,开启此特征值,可以防止攻击,在提高性能的同时也确保了安全。 USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING(true) 是否使用BufferRecycler、ThreadLocal、SoftReference来有效的重用底层的输入/输出缓冲区。这个特性在后端服务(JavaEE)环境下是很有意义的,提效明显。但是对于在Android环境下就不见得了~ 总而言之言而总之,JsonFactory的这几个特征值都建议开启,也就是维持默认即可。 定制读/写实例 读写行为的控制是通过各自的Feature来控制的,JsonFactory作为一个功能并非单一的工厂类,需要既能够定制化读JsonParser,也能定制化写JsonGenerator。 为此,对应的API它都提供了三份(一份定制化自己的Feature): public JsonFactory enable(JsonFactory.Feature f); public JsonFactory enable(JsonParser.Feature f); public JsonFactory enable(JsonGenerator.Feature f); public JsonFactory disable(JsonFactory.Feature f); public JsonFactory disable(JsonParser.Feature f); public JsonFactory disable(JsonGenerator.Feature f); // 合二为一的Configure方法 public JsonFactory configure(JsonFactory.Feature f, boolean state); public JsonFactory configure(JsonParser.Feature f, boolean state); public JsonFactory configure(JsonGenerator.Feature f, boolean state); 使用示例: @Test public void test3() throws IOException { String jsonStr = "{\"age\":18, \"age\": 28 }"; JsonFactory factory = new JsonFactory(); factory.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // 使用factory定制将不生效 // factory.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("age".equals(fieldname)) { jsonParser.nextToken(); System.out.println(jsonParser.getIntValue()); } } } } 运行程序,抛出异常。证明特征开启成功,符合预期。 com.fasterxml.jackson.core.JsonParseException: Duplicate field 'age' at [Source: (String)"{"age":18, "age": 28 }"; line: 1, column: 17] 在使用JsonFactory定制化读/写实例的时需要特别注意:请务必确保在factory.createXXX()之前配置好对应的Feature特征,若在实例创建好之后再弄的话,对已经创建的实例无效。 小贴士:实例创建好后若你还想定制,可以使用实例自己的对应API操作 JsonFactoryBuilder JsonFactory负责基类和实现类的双重任务,是比较重的,分离得也不彻底。同时,现在都2020年了,对于这种构建类工厂如果还不用Builder模式就现在太out了,书写起来也非常不便: @Test public void test4() throws IOException { JsonFactory jsonFactory = new JsonFactory(); // jsonFactory自己的特征 jsonFactory.enable(JsonFactory.Feature.INTERN_FIELD_NAMES); jsonFactory.enable(JsonFactory.Feature.CANONICALIZE_FIELD_NAMES); jsonFactory.enable(JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING); // JsonParser的特征 jsonFactory.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); jsonFactory.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER); // JsonGenerator的特征 jsonFactory.enable(JsonGenerator.Feature.QUOTE_FIELD_NAMES); jsonFactory.enable(JsonGenerator.Feature.ESCAPE_NON_ASCII); // 创建读/写实例 // jsonFactory.createParser(...); // jsonFactory.createGenerator(...); } 功能实现上没毛病,但总显得不够优雅。同时上面也说了:定制化操作一定得在create创建动作之前执行,这全靠程序员自行控制。 Jackson在2.10版本新增了一个JsonFactoryBuilder构件类,让我们能够基于builder模式优雅的构建出一个JsonFactory实例。 小贴士:2.10版本是2019.09发布的 比如上面例子的代码使用JsonFactoryBuilder可重构为: @Test public void test4() throws IOException { JsonFactory jsonFactory = new JsonFactoryBuilder() // jsonFactory自己的特征 .enable(INTERN_FIELD_NAMES) .enable(CANONICALIZE_FIELD_NAMES) .enable(USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING) // JsonParser的特征 .enable(ALLOW_SINGLE_QUOTES, ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER) // JsonGenerator的特征 .enable(QUOTE_FIELD_NAMES, ESCAPE_NON_ASCII) .build(); // 创建读/写实例 // jsonFactory.createParser(...); // jsonFactory.createGenerator(...); } 对比起来,使用Builder模式优雅太多了。 因为JsonFactory是线程安全的,因此一般情况下全局我们只需要一个JsonFactory实例即可,推荐使用JsonFactoryBuilder去完成你的构建。 小贴士:使用JsonFactoryBuilder确保你的Jackson版本至少是2.10版本哦~ SPI方式 从源码包里发现,JsonFactory是支持Java SPI方式构建实例的。文件内容为: com.fasterxml.jackson.core.JsonFactory 因此,我可以使用Java SPI的方式得到一个JsonFactory实例: @Test public void test5() { ServiceLoader<JsonFactory> jsonFactories = ServiceLoader.load(JsonFactory.class); System.out.println(jsonFactories.iterator().next()); } 运行程序,妥妥的输出: com.fasterxml.jackson.core.JsonFactory@4abdb505 这种方式,玩玩即可,在这里没实际用途。 总结 本文围绕JsonFactory工厂为核心,讲解了它是如何创建、定制读/写实例的。对于自己的实例的创建共有三种方式: 直接new实例 使用JsonFactoryBuilder构建(需要2.10或以上版本) SPI方式创建实例 其中方式2是被推荐的,如果你的版本较低,就老老实实使用方式1呗。至于方式3嘛,玩玩就行,别当真。 至此,jackson-core的三大核心内容:JsonGenerator、JsonParser、JsonFactory全部介绍完了,它们是jackson 其它所有模块 的基石,需要掌握扎实喽。 下篇文章更有意思,会分析Jackson里Feature机制的设计,使用补码、掩码来实现是高效的体现,同时设计上也非常优美,下文见。 相关推荐: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON 4. JSON字符串是如何被解析的?JsonParser了解一下
公司不是你家,领导不是你妈。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位好,我是A哥(YourBatman)。上篇文章:3. 懂了这些,方敢在简历上说会用Jackson写JSON 聊完,流式API的写部分可以认为你已完全掌握了,本文了解它读的部分。 版本约定 Jackson版本:2.11.0 Spring Framework版本:5.2.6.RELEASE Spring Boot版本:2.3.0.RELEASE 小贴士:截止到本文,本系列前面所有示例都只仅仅导入jackson-core而已,后续若要新增jar包我会额外说明,否则相同 正文 什么叫读JSON?就是把一个JSON 字符串 解析为对象or树模型嘛,因此也称作解析JSON串。Jackson底层流式API使用JsonParser来完成JSON字符串的解析。 最简使用Demo 准备一个POJO: @Data public class Person { private String name; private Integer age; } 测试用例:把一个JSON字符串绑定(封装)进一个POJO对象里 @Test public void test1() throws IOException { String jsonStr = "{\"name\":\"YourBatman\",\"age\":18}"; Person person = new Person(); JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // 只要还没结束"}",就一直读 while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("name".equals(fieldname)) { jsonParser.nextToken(); person.setName(jsonParser.getText()); } else if ("age".equals(fieldname)) { jsonParser.nextToken(); person.setAge(jsonParser.getIntValue()); } } System.out.println(person); } } 运行程序,输出: Person(name=YourBatman, age=18) 成功把一个JSON字符串的值解析到Person对象。你可能会疑问,怎么这么麻烦?那当然,这是底层流式API,纯手动档嘛。你获得了性能,可不要失去一些便捷性嘛。 小贴士:底层流式API一般面向“专业人士”,应用级开发使用高阶API ObjectMapper即可。当然,读完本系列就能让你完全具备“专业人士”的实力 JsonParser针对不同的value类型,提供了非常多的方法用于实际值的获取。 直接值获取: // 获取字符串类型 public abstract String getText() throws IOException; // 数字Number类型值 标量值(支持的Number类型参照NumberType枚举) public abstract Number getNumberValue() throws IOException; public enum NumberType { INT, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL }; public abstract int getIntValue() throws IOException; public abstract long getLongValue() throws IOException; ... public abstract byte[] getBinaryValue(Base64Variant bv) throws IOException; 这类方法可能会抛出异常:比如value值本不是数字但你调用了getInValue()方法~ 小贴士:如果value值是null,像getIntValue()、getBooleanValue()等这种直接获取方法是会抛出异常的,但getText()不会 带默认值的值获取,具有更好安全性: public String getValueAsString() throws IOException { return getValueAsString(null); } public abstract String getValueAsString(String def) throws IOException; ... public long getValueAsLong() throws IOException { return getValueAsLong(0); } public abstract long getValueAsLong(long def) throws IOException; ... 此类方法若碰到数据的转换失败时,不会抛出异常,把def作为默认值返回。 组合方法 同JsonGenerator一样,JsonParser也提供了高钙片组合方法,让你更加便捷的使用。 自动绑定 听起来像高级功能,是的,它必须依赖于ObjectCodec去实现,因为实际是全部委托给了它去完成的,也就是我们最为熟悉的readXXX系列方法:我们知道,ObjectMapper就是一个ObjectCodec,它属于高级API,本文显然不会用到ObjectMapper它喽,因此我们自己手敲一个实现来完成此功能。 自定义一个ObjectCodec,Person类专用:用于把JSON串自动绑定到实例属性。 public class PersonObjectCodec extends ObjectCodec { ... @SneakyThrows @Override public <T> T readValue(JsonParser jsonParser, Class<T> valueType) throws IOException { Person person = (Person) valueType.newInstance(); // 只要还没结束"}",就一直读 while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("name".equals(fieldname)) { jsonParser.nextToken(); person.setName(jsonParser.getText()); } else if ("age".equals(fieldname)) { jsonParser.nextToken(); person.setAge(jsonParser.getIntValue()); } } return (T) person; } ... } 有了它,就可以实现我们的自动绑定了,书写测试用例: @Test public void test3() throws IOException { String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { jsonParser.setCodec(new PersonObjectCodec()); System.out.println(jsonParser.readValueAs(Person.class)); } } 运行程序,输出: Person(name=YourBatman, age=18) 这就是ObjectMapper自动绑定的核心原理所在,其它更为强大能力将在后续章节详细展开。 JsonToken 在上例解析过程中,有一个非常重要的角色,那便是:JsonToken。它表示解析JSON内容时,用于返回结果的基本标记类型的枚举。 public enum JsonToken { NOT_AVAILABLE(null, JsonTokenId.ID_NOT_AVAILABLE), START_OBJECT("{", JsonTokenId.ID_START_OBJECT), END_OBJECT("}", JsonTokenId.ID_END_OBJECT), START_ARRAY("[", JsonTokenId.ID_START_ARRAY), END_ARRAY("]", JsonTokenId.ID_END_ARRAY), // 属性名(key) FIELD_NAME(null, JsonTokenId.ID_FIELD_NAME), // 值(value) VALUE_EMBEDDED_OBJECT(null, JsonTokenId.ID_EMBEDDED_OBJECT), VALUE_STRING(null, JsonTokenId.ID_STRING), VALUE_NUMBER_INT(null, JsonTokenId.ID_NUMBER_INT), VALUE_NUMBER_FLOAT(null, JsonTokenId.ID_NUMBER_FLOAT), VALUE_TRUE("true", JsonTokenId.ID_TRUE), VALUE_FALSE("false", JsonTokenId.ID_FALSE), VALUE_NULL("null", JsonTokenId.ID_NULL), } 为了辅助理解,A哥用一个例子,输出各个部分一目了然: @Test public void test2() throws IOException { String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}"; System.out.println(jsonStr); JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { while (true) { JsonToken token = jsonParser.nextToken(); System.out.println(token + " -> 值为:" + jsonParser.getValueAsString()); if (token == JsonToken.END_OBJECT) { break; } } } } 运行程序,输出: {"name":"YourBatman","age":18, "pickName":null} START_OBJECT -> 值为:null FIELD_NAME -> 值为:name VALUE_STRING -> 值为:YourBatman FIELD_NAME -> 值为:age VALUE_NUMBER_INT -> 值为:18 FIELD_NAME -> 值为:pickName VALUE_NULL -> 值为:null END_OBJECT -> 值为:null 从左至右解析,一一对应。各个部分用下面这张图可以简略表示出来: 小贴士:解析时请确保你的的JSON串是合法的,否则抛出JsonParseException异常 JsonParser的Feature 它是JsonParser的一个内部枚举类,共15个枚举值: public enum Feature { AUTO_CLOSE_SOURCE(true), ALLOW_COMMENTS(false), ALLOW_YAML_COMMENTS(false), ALLOW_UNQUOTED_FIELD_NAMES(false), ALLOW_SINGLE_QUOTES(false), @Deprecated ALLOW_UNQUOTED_CONTROL_CHARS(false), @Deprecated ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false), @Deprecated ALLOW_NUMERIC_LEADING_ZEROS(false), @Deprecated ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false), @Deprecated ALLOW_NON_NUMERIC_NUMBERS(false), @Deprecated ALLOW_MISSING_VALUES(false), @Deprecated ALLOW_TRAILING_COMMA(false), STRICT_DUPLICATE_DETECTION(false), IGNORE_UNDEFINED(false), INCLUDE_SOURCE_IN_LOCATION(true); } 小贴士:枚举值均为bool类型,括号内为默认值 每个枚举值都控制着JsonParser不同的行为。下面分类进行解释 底层I/O流相关 自2.10版本后,使用StreamReadFeature#AUTO_CLOSE_SOURCE代替 Jackson的流式API指的是I/O流,所以即使是读,底层也是用I/O流(Reader)去读取然后解析的。 AUTO_CLOSE_SOURCE(true) 原理和JsonGenerator的AUTO_CLOSE_TARGET(true)一样,不再解释,详见上篇文章对应部分。 支持非标准格式 JSON是有规范的,在它的规范里并没有描述到对注释的规定、对控制字符的处理等等,也就是说这些均属于非标准行为。比如这个JSON串: { "name" : "YourBarman", // 名字 "age" : 18 // 年龄 } 你看,若你这么写IDEA都会飘红提示你:但是,在很多使用场景(特别是JavaScript)里,我们会在JSON串里写注释(属性多时尤甚)那么对于这种串,JsonParser如何控制处理呢?它提供了对非标准JSON格式的兼容,通过下面这些特征值来控制。 ALLOW_COMMENTS(false) 自2.10版本后,使用JsonReadFeature#ALLOW_JAVA_COMMENTS代替 是否允许/* */或者//这种类型的注释出现。 @Test public void test4() throws IOException { String jsonStr = "{\n" + "\t\"name\" : \"YourBarman\", // 名字\n" + "\t\"age\" : 18 // 年龄\n" + "}"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // 开启注释支持 // jsonParser.enable(JsonParser.Feature.ALLOW_COMMENTS); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("name".equals(fieldname)) { jsonParser.nextToken(); System.out.println(jsonParser.getText()); } else if ("age".equals(fieldname)) { jsonParser.nextToken(); System.out.println(jsonParser.getIntValue()); } } } } 运行程序,抛出异常: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('/' (code 47)): maybe a (non-standard) comment? (not recognized as one since Feature 'ALLOW_COMMENTS' not enabled for parser) at [Source: (String)"{ "name" : "YourBarman", // 名字 "age" : 18 // 年龄 }"; line: 2, column: 26] 放开注释的代码,再次运行程序,正常work。 ALLOW_YAML_COMMENTS(false) 自2.10版本后,使用JsonReadFeature#ALLOW_YAML_COMMENTS代替 顾名思义,开启后将支持Yaml格式的的注释,也就是#形式的注释语法。 ALLOW_UNQUOTED_FIELD_NAMES(false) 自2.10版本后,使用JsonReadFeature#ALLOW_UNQUOTED_FIELD_NAMES代替 是否允许属性名不带双引号"",比较简单,示例略。 ALLOW_SINGLE_QUOTES(false) 自2.10版本后,使用JsonReadFeature#ALLOW_SINGLE_QUOTES代替 是否允许属性名支持单引号,也就是使用''包裹,形如这样: { 'age' : 18 } ALLOW_UNQUOTED_CONTROL_CHARS(false) 自2.10版本后,使用JsonReadFeature#ALLOW_UNESCAPED_CONTROL_CHARS代替 是否允许JSON字符串包含非引号控制字符(值小于32的ASCII字符,包含制表符和换行符)。 由于JSON规范要求对所有控制字符使用引号,这是一个非标准的特性,因此默认禁用。 那么,哪些字符属于控制字符呢?做个简单科普:我们一般说的ASCII码共128个字符(7bit),共分为两大类 控制字符 控制字符,也叫不可打印字符。第0~32号及第127号(共34个)是控制字符,例如常见的:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)等都属于此类。 控制字符大部分已经废弃不用了,它们的用途主要是用来操控已经处理过的文字,ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。 话外音:你看不见我,但我对你影响还蛮大 非控制字符 也叫可显示字符,或者可打印字符,能从键盘直接输入的字符。比如0-9数字,逗号、分号这些等等。 话外音:你肉眼能看到的字符就属于非控制字符 ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false) 自2.10版本后,使用JsonReadFeature#ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER代替 是否允许*反斜杠*转义任何字符。这句话不是非常好理解,看下面这个例子: @Test public void test4() throws IOException { String jsonStr = "{\"name\" : \"YourB\\'atman\" }"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // jsonParser.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("name".equals(fieldname)) { jsonParser.nextToken(); System.out.println(jsonParser.getText()); } } } } 运行程序,报错: com.fasterxml.jackson.core.JsonParseException: Unrecognized character escape ''' (code 39) at [Source: (String)"{"name" : "YourB\'atman" }"; line: 1, column: 19] ... 放开注释掉的代码,再次运行程序,一切正常,输出:YourB'atman。 ALLOW_NUMERIC_LEADING_ZEROS(false) 自2.10版本后,使用JsonReadFeature#ALLOW_LEADING_ZEROS_FOR_NUMBERS代替 是否允许像00001这样的“数字”出现(而不报错)。看例子: @Test public void test5() throws IOException { String jsonStr = "{\"age\" : 00018 }"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // jsonParser.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("age".equals(fieldname)) { jsonParser.nextToken(); System.out.println(jsonParser.getIntValue()); } } } } 运行程序,输出: com.fasterxml.jackson.core.JsonParseException: Invalid numeric value: Leading zeroes not allowed at [Source: (String)"{"age" : 00018 }"; line: 1, column: 11] ... 放开注掉的代码,再次运行程序,一切正常。输出18。 ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false) 自2.10版本后,使用JsonReadFeature#ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS代替 是否允许小数点.打头,也就是说.1这种小数格式是否合法。默认是不合法的,需要开启此特征才能支持,例子就略了,基本同上。 ALLOW_NON_NUMERIC_NUMBERS(false) 自2.10版本后,使用JsonReadFeature#ALLOW_NON_NUMERIC_NUMBERS代替 是否允许一些解析器识别一组“非数字”(如NaN)作为合法的浮点数值。这个属性和上篇文章的JsonGenerator#QUOTE_NON_NUMERIC_NUMBERS特征值是遥相呼应的。 @Test public void test5() throws IOException { String jsonStr = "{\"percent\" : NaN }"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // jsonParser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("percent".equals(fieldname)) { jsonParser.nextToken(); System.out.println(jsonParser.getFloatValue()); } } } } 运行程序,抛错: com.fasterxml.jackson.core.JsonParseException: Non-standard token 'NaN': enable JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS to allow at [Source: (String)"{"percent" : NaN }"; line: 1, column: 17] 放开注释掉的代码,再次运行,一切正常。输出: NaN 小贴士:NaN也可以表示一个Float对象,是的你没听错,即使它不是数字但它也是Float类型。具体你可以看看Float源码里的那几个常量 ALLOW_MISSING_VALUES(false) 自2.10版本后,使用JsonReadFeature#ALLOW_MISSING_VALUES代替 是否允许支持JSON数组中“缺失”值。怎么理解:数组中缺失了值表示两个逗号之间,啥都没有,形如这样[value1, , value3]。 @Test public void test6() throws IOException { String jsonStr = "{\"names\" : [\"YourBatman\",,\"A哥\",,] }"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("names".equals(fieldname)) { jsonParser.nextToken(); while (jsonParser.nextToken() != JsonToken.END_ARRAY) { System.out.println(jsonParser.getText()); } } } } } 运行程序,抛错: YourBatman // 能输出一个,毕竟第一个part(JsonToken)是正常的嘛 com.fasterxml.jackson.core.JsonParseException: Unexpected character (',' (code 44)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false') at [Source: (String)"{"names" : ["YourBatman",,"A哥",,] }"; line: 1, column: 27] 放开注释掉的代码,再次运行,一切正常,结果为: YourBatman null A哥 null null 请注意:此时数组的长度是5哦。 小贴士:此处用的String类型展示结果,是因为null可以作为String类型(jsonParser.getText()得到null是合法的)。但如果你使用的int类型(或者bool类型),那么如果是null的话就报错喽Current token (VALUE_NULL) not of boolean type,有兴趣的亲可自行尝试,巩固下理解的效果。报错原因文上已有说明~ ALLOW_TRAILING_COMMA(false) 自2.10版本后,使用JsonReadFeature#ALLOW_TRAILING_COMMA代替 是否允许最后一个多余的逗号(一定是最后一个)。这个特征是非常重要的,若开关打开,有如下效果: [true,true,]等价于[true, true] {"a": true,}等价于{"a": true} 当这个特征和上面的ALLOW_MISSING_VALUES特征同时使用时,本特征优先级更高。也就是说:会先去除掉最后一个逗号后,再进行数组长度的计算。 举个例子:当然这两个特征开关都打开时,[true,true,]等价于[true, true]好理解;并且呢,[true,true,,]是等价于[true, true, null]的哦,可千万别忽略最后的这个null。 @Test public void test7() throws IOException { String jsonStr = "{\"results\" : [true,true,,] }"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES); // jsonParser.enable(JsonParser.Feature.ALLOW_TRAILING_COMMA); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("results".equals(fieldname)) { jsonParser.nextToken(); while (jsonParser.nextToken() != JsonToken.END_ARRAY) { System.out.println(jsonParser.getBooleanValue()); } } } } } 运行程序,输出: YourBatman null A哥 null null 这完全就是上例的效果嘛。现在我放开注释掉的代码,再次运行,结果为: YourBatman null A哥 null 请注意对比前后的结果差异,并自己能能自己合理解释。 校验相关 Jackson在JSON标准之外,给出了两个校验相关的特征。 STRICT_DUPLICATE_DETECTION(false) 自2.10版本后,使用StreamReadFeature#STRICT_DUPLICATE_DETECTION代替 是否允许JSON串有两个相同的属性key,默认是允许的。 @Test public void test8() throws IOException { String jsonStr = "{\"age\":18, \"age\": 28 }"; JsonFactory factory = new JsonFactory(); try (JsonParser jsonParser = factory.createParser(jsonStr)) { // jsonParser.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); while (jsonParser.nextToken() != JsonToken.END_OBJECT) { String fieldname = jsonParser.getCurrentName(); if ("age".equals(fieldname)) { jsonParser.nextToken(); System.out.println(jsonParser.getIntValue()); } } } } 运行程序,正常输出: 18 28 若放开注释代码,再次运行,则抛错: 18 // 第一个数字还是能正常输出的哟 com.fasterxml.jackson.core.JsonParseException: Duplicate field 'age' at [Source: (String)"{"age":18, "age": 28 }"; line: 1, column: 17] IGNORE_UNDEFINED(false) 自2.10版本后,使用StreamReadFeature#IGNORE_UNDEFINED代替 是否忽略没有定义的属性key。和JsonGenerator.Feature#IGNORE_UNKNOWN的这个特征一样,它作用于预先定义了格式的数据类型,如Avro、protobuf等等,JSON是不需要预先定义的哦~ 同样的,你可以通过这个API预先设置格式: JsonParser: public void setSchema(FormatSchema schema) { ... } 其它 INCLUDE_SOURCE_IN_LOCATION(true) 自2.10版本后,使用StreamReadFeature#INCLUDE_SOURCE_IN_LOCATION代替 是否构建JsonLocation对象来表示每个part的来源,你可以通过JsonParser#getCurrentLocation()来访问。作用不大,就此略过。 总结 本文介绍了底层流式API JsonParser读JSON的方式,它不仅仅能够处理标准JSON,也能通过Feature特征值来控制,开启对一些非标准但又比较常用的JSON串的支持,这不正式一个优秀框架/库应有的态度麽:兼容性。 结合上篇文章对写JSON时JsonGenerator的描述,能够总结出两点原则: 写:100%遵循规范 读:最大程度兼容并包 写代表你的输出,遵循规范的输出能确保第三方在用你输出的数据时不至于对你破口大骂,所以这是你应该做好的本分。读代表你的输入,能够处理规范的格式是你的职责,但我若还能额外的处理一些非标准格式(一般为常用的),那绝对是闪耀点,也就是你给的情分。本分是你应该做的,而情分就是你的加分项。 相关推荐: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON
你必须非常努力,才能看起来毫不费力。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位好,我是A哥(YourBatman)。上篇文章:2. 妈呀,Jackson原来是这样写JSON的 知道了Jackson写JSON的姿势,切实感受了一把ObjectMapper原来是这样完成序列化的...本文继续深入讨论JsonGenerator写JSON的细节。 先闲聊几句题外话哈。我们在书写简历的时候,都会用一定篇幅展示自己的技能点(亮点),就像这样:这一part非常重要,它决定了面试官是否有跟你聊的兴趣,决定了你是否能在浩如烟海的简历中够脱颖而出。如何做到差异性?在当下如此发达的信息社会里,信息的获取唾手可得,所以在知识的广度方面,我认为人与人之间的差异其实并不大: 你知道DDD领域驱动、读过架构整洁之道、知道六边形架构、知道DevOps......难道你还在想凭一些概念卖钱?拉出差距?你在用Spring技术栈、在用Redis、在用ElasticSearch......难道你还以为现在像10年前一样,会用就能加分? 一聊就会,一问就退,一写就废。这是很多公司程序员的真实写照,基/中层管理者尤甚。早早的和技术渐行渐远,导致裁员潮到来时很容易获得一张“飞机票”,年纪越大,焦虑感越强。 在你的公司是否有过这种场景:四五个人指挥一个人干活。对,就像这样:扎不扎心,老铁。不过不用悲观,从这应该你看到的是机会,习xx都说了实干才能兴邦嘛,2019年裁员潮洗牌后,适者生存,不适者很多回老家了,这也让大批很有实力的程序员享受到了红利。应正了那句:当大潮褪去,才知道谁在裸泳。 扯远了,言归正传。Jackson单会简单使用我认为还不足矣立足,那就跟我来吧~ 版本约定 Jackson版本:2.11.0 Spring Framework版本:5.2.6.RELEASE Spring Boot版本:2.3.0.RELEASE 正文 一个框架/库好不好,不是看它的核心功能做得怎么样,而是非核心功能处理得如何。比如后台页面做得咋样?容错机制呢?定制化、可配置化,扩展性等等。 Jackson称得上优秀(甚至最佳)最主要是得益于它优秀的module模块化设计,在接触其之前,我们先完成本章节的内容:JsonGenerator写JSON的行为控制(配置)。 配置属于程序的一部分,它影响着程序执行的方方面面。Spring使用Environment/PropertySource管理配置,对应的在Jackson里会看到有很多Feature类来控制Jackson的读/写行为,均是使用enum枚举类型来管理。 上篇文章 我们学会了如何使用JsonGenerator去写一个JSON,本文将来学习它的需要掌握的使用细节。同样的,为围绕着JsonGenerator展开。 JsonGenerator的Feature 它是JsonGenerator的一个内部枚举类,共10个枚举值: public enum Feature { // Low-level I/O AUTO_CLOSE_TARGET(true), AUTO_CLOSE_JSON_CONTENT(true), FLUSH_PASSED_TO_STREAM(true), // Quoting-related features @Deprecated QUOTE_FIELD_NAMES(true), @Deprecated QUOTE_NON_NUMERIC_NUMBERS(true), @Deprecated ESCAPE_NON_ASCII(false), @Deprecated WRITE_NUMBERS_AS_STRINGS(false), // Schema/Validity support features WRITE_BIGDECIMAL_AS_PLAIN(false), STRICT_DUPLICATE_DETECTION(false), IGNORE_UNKNOWN(false); ... } 小贴士:枚举值均为bool类型,括号内为默认值 这个Feature的每个枚举值都控制着JsonGenerator写JSON时的不同行为,并且可分为三大类(源码处我也有标注): Low-level I/O:底层I/O流相关。 Jackson的流式API指的是I/O流,因此就涉及到关流、flush刷新流等操作 Quoting-related:双引号""引用相关。 JSON规范规定key都必须有双引号,但这对于某些场景下并不需要 Schema/Validity support:约束/规范/校验相关。 JSON作为K-V结构的数据,那么允许相同key出现吗?这便由这些特征去控制 下面分别来认识认识它们。 AUTO_CLOSE_TARGET(true) 含义即为字面意:自动关闭目标(流)。 true:调用JsonGenerator#close()便会自动关闭底层的I/O流,你无需再关心 false:底层I/O流请手动关闭 自动关闭: @Test public void test1() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // doSomething } } 如果改为false:那么你就需要自己手动去close底层使用的OutputStream或者Writer。形如这样: @Test public void test2() throws IOException { JsonFactory factory = new JsonFactory(); try (PrintStream err = System.err; JsonGenerator jg = factory.createGenerator(err, JsonEncoding.UTF8)) { // 特征置为false 采用手动关流的方式 jg.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); // doSomething } } 小贴士:例子均采用try-with-resources方式关流,所以并没有显示调用close()方法,你应该能懂吧 AUTO_CLOSE_JSON_CONTENT(true) 先来看下面这段代码: @Test public void test3() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { jg.writeStartObject(); jg.writeFieldName("names"); // 写数组 jg.writeStartArray(); jg.writeString("A哥"); jg.writeString("YourBatman"); } } 运行程序,输出: {"names":["A哥","YourBatman"]} wow,竟然输出一切正常。细心的你会发现,我的代码是缺胳膊少腿的:不管是Object还是Array都只start了,并没有显示调用end进行闭合。但是呢,结果却正常得很,这便是此Feature的作用了。 true:自动补齐(闭合)JsonToken#START_ARRAY和JsonToken#START_OBJECT类型的内容 false:啥都不做(不会主动抛错哦) 不过还是要啰嗦一句:虽然Jackson通过此Feature做了容错,但是自己在使用时,请务必显示书写闭合 FLUSH_PASSED_TO_STREAM(true) 在使用带有缓冲区的I/O写数据时,缺少“临门一脚”是初学者很容易犯的错误,比如下面这个例子: @Test public void test4() throws IOException { JsonFactory factory = new JsonFactory(); JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8); jg.writeStartObject(); jg.writeStringField("name","A哥"); jg.writeEndObject(); // jg.flush(); // jg.close(); } 运行程序,控制台没有任何输出。把注释代码放开任何一行,再次运行程序,控制台正常输出: {"name":"A哥"} true:当JsonGenerator调用close()/flush()方法时,自动强刷I/O流里面的数据 false:请手动处理 为何需要flush()? 对于此问题这里小科普一下。因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统(话外音:这是操作系统为之)并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。 小贴士:InputStream是没有flush()方法的哦 通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了,OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法强制刷一次缓冲区。但是,在某些情况下,我们必须手动调用flush()方法,比如上例子,比如发IM消息... QUOTE_FIELD_NAMES(true) 此属性自2.10版本后已过期,使用JsonWriteFeature#QUOTE_FIELD_NAMES代替,应用在JsonFactory上,后文详解 JSON对象字段名是否为使用""双引号括起来,这是JSON规范(RFC4627)规定的。 true:字段名使用""括起来 -> 遵循JSON规范 false:字段名不使用""括起来 -> 不遵循JSON规范 @Test public void test5() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // jg.disable(QUOTE_FIELD_NAMES); jg.writeStartObject(); jg.writeStringField("name","A哥"); jg.writeEndObject(); } } 运行程序,输出: {"name":"A哥"} 99.99%的情况下我们不需要改变默认值。Jackson添加了禁用引号的功能以支持那非常不常见的情况,最常见的情况直接从Javascript中使用时可能会发生。 打开注释掉的语句,再次运行程序,输出: {name:"A哥"} QUOTE_NON_NUMERIC_NUMBERS(true) 此属性自2.10版本后已过期,使用JsonWriteFeature#WRITE_NAN_AS_STRINGS代替,应用在JsonFactory上,后文详解 这个特征挺有意思,看例子(以写Float为例): @Test public void test6() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // jg.disable(JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS); jg.writeNumber(0.9); jg.writeNumber(1.9); jg.writeNumber(Float.NaN); jg.writeNumber(Float.NEGATIVE_INFINITY); jg.writeNumber(Float.POSITIVE_INFINITY); } } 运行程序,输出: 0.9 1.9 "NaN" "-Infinity" "Infinity" 同为Float数字类型,有的输出有""双引号包着,有的没有。放开注释的语句(禁用此特征),再次运行程序,输出: 0.9 1.9 NaN -Infinity Infinity 很明显,如果你是这么输出为一个JSON的话,那它就会是非法的JSON,是不符合JSON标准的(因为像NaN、Infinity这种明显是字符串嘛,必须用""包起来才是合法的value值)。 由于JSON规范中对数字的严格定义,加上Java可能具有的开放式数字集(如上例中Float类型并不100%是数字),很难做到既安全又方便,因此有了此特征让你根据需要来控制。 ESCAPE_NON_ASCII(false) 此属性自2.10版本后已过期,使用JsonWriteFeature#ESCAPE_NON_ASCII代替,应用在JsonFactory上,后文详解 @Test public void test7() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // jg.enable(ESCAPE_NON_ASCII); jg.writeString("A哥"); } } 运行程序,输出: "A哥" 放开注掉的代码(开启此属性),再次运行,输出: "A\u54E5" WRITE_NUMBERS_AS_STRINGS(false) 此属性自2.10版本后已过期,使用JsonWriteFeature#WRITE_NUMBERS_AS_STRINGS代替,应用在JsonFactory上,后文详解 该特性强制将所有Java数字写成字符串,即使底层数据格式真的是数字。 true:所有数字强制写为字符串 false:不做处理 @Test public void test8() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // jg.enable(WRITE_NUMBERS_AS_STRINGS); Long num = Long.MAX_VALUE; jg.writeNumber(num); } } 运行程序,输出: 9223372036854775807 放开注释代码(开启此特征),再次运行程序,输出: "9223372036854775807" 有什么使用场景?一个用例是避免Javascript限制的问题:因为Javascript标准规定所有的数字处理都应该使用64位ieee754浮点值来完成,结果是一些64位整数值不能被精确表示(因为尾数只有51位宽)。 采坑提醒:时间戳后端用Long类型反给前端是没有问题的。但如果你是很大的一个Long值(如雪花算法算出的很大的Long值),直接返回前端的话,Javascript就会出现精度丢失的bug WRITE_BIGDECIMAL_AS_PLAIN(false) 控制写java.math.BigDecimal的行为: true:使用BigDecimal#toPlainString()方法输出 false: 使用默认输出方式(取决于BigDecimal是如何构造的) @Test public void test7() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // jg.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); BigDecimal bigDecimal1 = new BigDecimal(1.0); BigDecimal bigDecimal2 = new BigDecimal("1.0"); BigDecimal bigDecimal3 = new BigDecimal("1E11"); jg.writeNumber(bigDecimal1); jg.writeNumber(bigDecimal2); jg.writeNumber(bigDecimal3); } } 运行程序,输出: 1 1.0 1E+11 放开注释代码,再次运行程序,输出: 1 1.0 100000000000 STRICT_DUPLICATE_DETECTION(false) 是否去严格的检测重复属性名。 true:检测是否有重复字段名,若有,则抛出JsonParseException异常 false:不检测JSON对象重复的字段名,即:相同字段名都要解析 @Test public void test8() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) { // jg.enable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION); jg.writeStartObject(); jg.writeStringField("name","YourBatman"); jg.writeStringField("name","A哥"); jg.writeEndObject(); } } 运行程序,输出: {"name":"YourBatman","name":"A哥"} 打开注释掉的哪行代码:开启此特征值为true。再次运行程序,输出: com.fasterxml.jackson.core.JsonGenerationException: Duplicate field 'name' at com.fasterxml.jackson.core.json.JsonWriteContext._checkDup(JsonWriteContext.java:224) at com.fasterxml.jackson.core.json.JsonWriteContext.writeFieldName(JsonWriteContext.java:217) ... 注意:谨慎打开此开关,如果检查的话性能会下降20%-30%。 IGNORE_UNKNOWN(false) 如果底层数据格式需要输出所有属性,以及如果找不到调用者试图写入的属性的定义,则该特性确定是否要执行的操作。 可能你听完还一脸懵逼,什么底层数据格式,什么找不到,我明明是写JSON啊,何解?其实这不是针对于写JSON来说的,对于JSON,这个特性没有效果,因为属性不需要预先定义。通常,大多数文本数据格式不需要模式信息,而某些二进制数据格式需要定义(如Avro、protobuf),因此这个属性是为它们而生(Smile、BSON等这些二进制也是不需要预定模式信息的哦)。 强调:JsonGenerator不是只能写JSON格式,毕竟底层是I/O流嘛,理论上啥都能写 true:启动该功能 可以预先调用(在写数据之前)这个API设定好模式信息即可: JsonGenerator: public void setSchema(FormatSchema schema) { ... } false:禁用该功能。如果底层数据格式需要所有属性的知识才能输出,那就抛出JsonProcessingException异常 定制Feature 通过上一part知晓了控制JsonGenerator的特征值们,以及其作用是。Feature的每个枚举值都有个默认值(括号里面),那么如果我们希望对不同的JsonGenerator实例应用不同的配置该怎么办呢? 自然而然的JsonGenerator提供了相关API供以我们操作: // 开启 public abstract JsonGenerator enable(Feature f); // 关闭 public abstract JsonGenerator disable(Feature f); // 开启/关闭 public final JsonGenerator configure(Feature f, boolean state) { ... }; public abstract boolean isEnabled(Feature f); public boolean isEnabled(StreamWriteFeature f) { ... }; 替换者:StreamWriteFeature 本类是2.10版本新增的,用于完全替换上面的Feature。目的:完全独立的属性配置,不依赖于任何后端格式,因为JsonGenerator并不局限于写JSON,因此把Feature放在JsonGenerator作为内部类是不太合适的,所以单独摘出来。 StreamWriteFeature用在JsonFactory里,后面再讲解到它的构建器JsonFactoryBuilder时再详细探讨。 序列化POJO对象 上篇文章用代码演示过了如何使用writeObject(Object pojo)来把一个POJO一次性序列化成为一个JSON串,它主要依赖于ObjectCodec去完成: public abstract JsonGenerator setCodec(ObjectCodec oc); ObjectCodec可谓是Jackson里极其重要的一个基础组件,我们最熟悉的ObjectMapper它就是一个解码器,实现了序列化和反序列化、树模型等操作。这将在后面章节里重点介绍~ 输出漂亮的JSON格式 我们知道JSON之所以快速流行的原因之一是得益于它的可读性好,可读性好又表现在它漂亮的(规则)的展示格式上。 默认情况下,使用JsonGenerator写JSON时,所有的部分都是输出在同一行里,显然这种格式对人阅读来说是不够友好的。作为最流行的JSON库自然考虑到了这一点,提供了格式化器来美化输出: // 自己指定漂亮格式打印器 public JsonGenerator setPrettyPrinter(PrettyPrinter pp) { ... } // 应用默认的漂亮格式打印器 public abstract JsonGenerator useDefaultPrettyPrinter(); PrettyPrinter有如下两个实现类:使用不同的实现类,对输出结果的影响如下: 什么都不设置: MinimalPrettyPrinter: {"zhName":"A哥","enName":"YourBatman","age":18} DefaultPrettyPrinter: useDefaultPrettyPrinter(): { "zhName" : "A哥", "enName" : "YourBatman", "age" : 18 } 由此可见,在什么都不设置的情况下,结果会全部在一行显示(紧凑型输出)。DefaultPrettyPrinter表示带层级格式的输出(可读性好),若有此需要,建议直接调用更为快捷的useDefaultPrettyPrinter()方法,而不用自己去new一个实例。 总结 本文的主要内容和重点是介绍了用Feature去控制JsonGenerator的写行为,不同的特征值控制着不同的行为。在实际使用时可针对不同的需求,定制出不同的JsonGenerator实例,因地制宜和互相隔离。 相关推荐: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的
学无止境?本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位好,我是A哥(YourBatman)。今天是2020-07-25,上午我正从https://start.spring.io准备down一个工程下来的时候,打开页面发现默认选中的Spring Boot版本号是2.3.2:并非我刻意的去找到这个变化,而是由于我昨天 down下来的工程使用的Spring Boot版本是2.3.1,印象还在,所以今天一下子就发现了差异。 既然升级了(虽然是小版本号),那就去官方楼一眼呗。不看不知道,一看还真让发现些内容:Spring Boot在同一天(2020-07-25)一口气发布了三个版本,这是要搞事情啊? 小贴士:本文所有时间若未做特殊说明,指的均是北京时间 正文 Spring Boot目前还处于Active活跃的分支共有三个:因此就在今天,同一天里Spring Boot对它的这三条线做了更新: Spring Boot 2.3.1 -> Spring Boot 2.3.2 Spring Boot 2.2.8 -> Spring Boot 2.2.9 Spring Boot 2.1.15 -> Spring Boot 2.1.16 此次发版,距离上次正式发版(不区分分支)已足足有44天之久。 有哪些升级? 参考github上的Release详情,三个分支都有如下三方面的改进: 修复bug 文档同步 升级依赖 修复bug 小版本号的升级,最重要的使命就是修复bug,这是它存在的意义。针对这三个版本,各自的bug修复总数如下: 2.3.2:34个。遥遥领先 2.2.9:10个。 2.1.16:1个。 能发现规律吧,版本越新,bug越多,这是符合常理的。另外,从小版本号里能知道:2.1.x版本都修复16次bug了,而2.3.x才第2次修复,正处于bug井喷阶段呢。所以一味的追新的话,还需谨慎哈。 也许你会吐槽,Spring Boot这啥编码水平,咋这么多bug?其实非也,个数虽多(其实也还好),但每一个都是非严重bug,影响甚微,无需大惊小怪。 另外,从bug的原因上来看,不少bug是各个版本都有的共性问题。比如2.1.x版本那个唯一的bug,其它两个版本均有: 文档同步 此part用于对文档上的改变做出一些说明,比如文字描述错误、排班不正确等等。举例本次的一个修复: 修复前:修复后:不得不说,这老外还挺较真(挺仔细)的。 升级依赖 由于是小版本的升级,因此对应的依赖也是小版本升级。举例: Tomcat升级到9.0.37 Spring Framework升级到5.2.8(此版本4天前发布) 值得注意的是,拿Spring Framework的升级举例:Spring Boot的2.2.x和2.3.x都是升级到了5.2.8版本,而Spring Boot的2.1.x分支依赖的是Spring Framework 5.1.17版本哦。 除此之外,Spring Boot它的最新版本,也就是2.3.2里还新增了3个新特性,了解一下: 改进 Kubernates Liveness/Readiness 健康指标和探针配置 添加运行镜像选项用于Docker镜像构建 增加对reactive Elasticsearch的健康检查 小贴士:小版本号的升级是可以新增这种很小的功能点的,但不允许新增大功能 三个版本核心依赖的区别 Spring Boot目前活跃的分支有3个,也就是这三个主线版本。那么他们三在核心依赖上有啥区别呢?A哥特意翻资料帮你整理了一下,绘制如下表: 说明:因为表格兼容性不太好,所以我以图片方式展示 关于1.5.x和2.0.x版本 这两个分支已经是古董分支了: stale中文意思:不新鲜的,老掉牙的,没有新意的 它们早已寿终正寝,最后一个版本和发布时间为: 1.5.22.RELEASE,2019.08 2.0.9.RELEASE,2019.04 有意思的是,2.0.x版本的生命周期非常的短暂,几乎刚好一年(2018.3 - 2019.4)。但是不可否认2.0.x版本是具有划时代意义的,在1.5.x的基础上垮了一大步,上了一个大台阶。 所以如果你的项目还在使用这两个版本,特别是1.5.x,那么尽快升级吧。官方推荐的是使用最新的2.3.x分支,这也是当前最为活跃的分支。 小贴士:1.5.x升级到2.x.x属于阻断式升级,需要十分谨慎 总结 Spring Boot作为微服务、云原生开发的基础设施,每个Java开发者都应该理解它、跟上它、学习它,才得以保证自己不掉队,不被后浪拍死。 但是,如此之快的更新速度,Spring官方是认真的,但你能认真起来吗?歪果仁,这是周末唉,你们不用休息的吗?疫情期间在家办公就这么任性? 相关推荐: Fastjson到了说再见的时候了 1. 初识Jackson -- 世界上最好的JSON库 2. 妈呀,Jackson原来是这样写JSON的 3. 懂了这些,方敢在简历上说会用Jackson写JSON
没有人永远18岁,但永远有人18岁。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位好,我是A哥(YourBatman)。上篇文章 整体介绍了世界上最好的JSON库 -- Jackson,对它有了整体了解:知晓了它是个生态,其它的仅是个JSON库而已。 有人说Jackson小众?那么请先看看上篇文章吧。学Jackson性价比特别高,因为它使用广泛、会的人少,因此在团队内如果你能精通,附加价值的效应就会非常明显了... 我挠头想了想,本系列来不了虚的,只能肝。本系列教程不仅仅教授基本使用,目标是搞完后能够解决日常99.99%的问题,毕竟每个小团队都最好能有某些方面的小专家,毕竟大家都不乏遇见过一个技术问题卡一天的情况。只有从底层把握,方能游刃有余。 命名为core的模块一般都不简单,jackson-core自然也不例外。它是三大核心模块之一,并且是核心中的核心,提供了对JSON数据的完整支持(包括各种读、写)。它是三者中最强大的模块,具有最低的开销和最快的读/写操作。 此模块提供了最具底层的Streaming JSON解析器/生成器,这组流式API属于Low-Level API,具有非常显著的特点: 开销小,损耗小,性能极高 因为是Low-Level API,所以灵活度极高 又因为是Low-Level API,所以易错性高,可读性差 jackson-core模块提供了两种处理JSON的方式(纵缆整个Jackson共三种): 流式API:读取并将JSON内容写入作为离散事件 -> JsonParser读取数据,而JsonGenerator负责写入数据 树模型:JSON文件在内存里以树形式表示。此种方式也很灵活,它类似于XML的DOM解析,层层嵌套的 作为“底层”技术,应用级开发中确实接触不多。为了引起你的重视,提前预告一下:Spring MVC对JSON消息的转换器AbstractJackson2HttpMessageConverter它就用到了底层流式API -> JsonGenerator写数据。想不想拿下Spring呢?我想你的答案应该是Yes吧~ 相信做难事必有所得,你我他都会用的技术、都能解决的问题,那绝成不了你的核心竞争力,自然在团队内就难成发光体。 版本约定 原则:均选当前最新版本(忽略小版本) Jackson版本:2.11.0 Spring Framework版本:5.2.6.RELEASE Spring Boot版本:2.3.0.RELEASE 内置的Jackson和Spring版本均和保持一致,避免了版本交叉 说明:类似2.11.0和2.11.x这种小版本号的差异,你权可认为没有区别 工程结构 鉴于是首次展示工程示例代码,将基本结构展示如下: 全部源码地址在本系列的最后一篇文章中会全部公示出来 正文 Jackson提供了一种对性能有极致要求的方式:流式API。它用于对性能有极致要求的场景,这个时候就可以使用此种方式来对JSON进行读写。 概念解释:流式、增量模式、JsonToken 流式(Streaming):此概念和Java8中的Stream流是不同的。这里指的是IO流,因此具有最低的开销和最快的读/写操作(记得关流哦) 增量模式(incremental mode):它表示每个部分一个一个地往上增加,类似于垒砖。使用此流式API读写JSON的方式使用的均是增量模式 JsonToken:每一部分都是一个独立的Token(有不同类型的Token),最终被“拼凑”起来就是一个JSON。这是流式API里很重要的一个抽象概念。 关于增量模式和Token概念,在Spirng的SpEL表达式中也有同样的概念,这在Spring相关专栏里你将会再次体会到 本文将看看它是如何写JSON数据的,也就是JsonGenerator。 JsonGenerator使用Demo JsonGenerator定义用于编写JSON内容的公共API的基类(抽象类)。实例使用的工厂方法创建,也就是JsonFactory。 小贴士:纵观整个Jackson,它更多的是使用抽象类而非接口,这是它的一大“特色”。因此你熟悉的面向接口编程,到这都要转变为面向抽象类编程喽。 话不多说,先来一个Demo感受一把: @Test public void test1() throws IOException { JsonFactory factory = new JsonFactory(); // 本处只需演示,向控制台写(当然你可以向文件等任意地方写都是可以的) JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8); try { jsonGenerator.writeStartObject(); //开始写,也就是这个符号 { jsonGenerator.writeStringField("name", "YourBatman"); jsonGenerator.writeNumberField("age", 18); jsonGenerator.writeEndObject(); //结束写,也就是这个符号 } } finally { jsonGenerator.close(); } } 因为JsonGenerator实现了AutoCloseable接口,因此可以使用try-with-resources优雅关闭资源(这也是推荐的使用方式),代码改造如下: @Test public void test1() throws IOException { JsonFactory factory = new JsonFactory(); // 本处只需演示,向控制台写(当然你可以向文件等任意地方写都是可以的) try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); //开始写,也就是这个符号 { jsonGenerator.writeStringField("name", "YourBatman"); jsonGenerator.writeNumberField("age", 18); jsonGenerator.writeEndObject(); //结束写,也就是这个符号 } } } 运行程序,控制台输出: {"name":"YourBatman","age":18} 这是最简使用示例,这也就是所谓的序列化底层实现,从示例中对增量模式能够有所感受吧。 纯手动档有木有,灵活性和性能极高,但易出错。这就像头文字D的赛车一样,先要速度、高性能、灵活性,那必须上手动档。 JsonGenerator详细介绍 JsonGenerator是个抽象类,它的继承体系如下: WriterBasedJsonGenerator:基于java.io.Writer处理字符编码(话外音:使用Writer输出JSON) 因为UTF-8编码基本标准化了,因此Jackson内部也提供了SegmentedStringWriter/UTF8Writer来简化操作 UTF8JsonGenerator:基于OutputStream + UTF-8处理字符编码(话外音:明确指定了使用UTF-8编码把字节变为字符) 默认情况下(不指定编码),Jackson默认会使用UTF-8进行编码,也就是说会使用UTF8JsonGenerator作为实际的JSON生成器实现类,具体逻辑将在讲述JsonFactory章节中有所体现,敬请关注。 值得注意的是,抽象基类JsonGenerator它只负责JSON的生成,至于把生成好的JSON写到哪里去它并不关心。比如示例中我给写到了控制台,当然你也可以写到文件、写到网络等等。 Spring MVC中的JSON消息转换器就是向HttpOutputMessage(网络输出流)里写JSON数据 关键API JsonGenerator虽然仅是抽象基类,但Jackson它建议我们使用JsonFactory工厂来创建其实例,并不需要使用者去关心其底层实现类,因此我们仅需要面向此抽象类编程即可,此为对使用者非常友好的设计。 对于JSON生成器来说,写方法自然是它的灵魂所在。众所周知,JSON属于K-V数据结构,因此针对于一个JSON来说,每一段都k额分为写key和写value两大阶段。 写JSON Key JsonGenerator一共提供了3个方法用于写JSON的key: @Test public void test2() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("zhName"); jsonGenerator.writeEndObject(); } } 运行程序,输出: {"zhName"} 可以发现,key可以独立存在(无需value),但value是不能独立存在的哦,下面你会看到效果。而3个方法中的其它2个方法: public abstract void writeFieldName(SerializableString name) throws IOException; public void writeFieldId(long id) throws IOException { writeFieldName(Long.toString(id)); } 这两个方法,你可以忘了吧,记住writeFieldName()就足够了。 总的来说,写JSON的key非常简单的,这得益于JSON的key有且仅可能是String类型,所以情况单一。下面继续了解较为复杂的写Value的情况。 写JSON Value 我们知道在Java中数据存在的形式(类型)非常之多,比如String、int、Reader、char[]...,而在JSON中值的类型只能是如下形式: 字符串(如{ "name":"YourBatman" }) 数字(如{ "age":18 }) 对象(JSON 对象)(如{ "person":{ "name":"YourBatman", "age":18}}) 数组(如{"names":[ "YourBatman", "A哥" ]}) 布尔(如{ "success":true }) null(如:{ "name":null }) 小贴士:像数组、对象等这些“高级”类型可以互相无限嵌套 很明显,Java中的数据类型和JSON中的值类型并不是一一对应的关系,那么这就需要JsonGenerator在写入时起到一个桥梁(适配)作用:下面针对不同的Value类型分别作出API讲解,给出示例说明。在此之前,请先记住两个结论,会更有利于你理解示例: JSON的顺序,和你write的顺序保持一致 写任何类型的Value之前请记得先write写key,否则可能无效 字符串 可把Java中的String类型、Reader类型、char[]字符数组类型等等写为JSON的字符串形式。 @Test public void test3() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("zhName"); jsonGenerator.writeString("A哥"); jsonGenerator.writeFieldName("enName"); jsonGenerator.writeString("YourBatman"); jsonGenerator.writeEndObject(); } } 运行程序,输出: {"zhName":"A哥","enName":"YourBatman"} 数字 参考上例,不解释。 对象(JSON 对象) @Test public void test4() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("zhName"); jsonGenerator.writeString("A哥"); // 写对象(记得先写key 否则无效) jsonGenerator.writeFieldName("person"); jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("enName"); jsonGenerator.writeString("YourBatman"); jsonGenerator.writeFieldName("age"); jsonGenerator.writeNumber(18); jsonGenerator.writeEndObject(); jsonGenerator.writeEndObject(); } } 运行程序,输出: {"zhName":"A哥","person":{"enName":"YourBatman","age":18}} 对象属于一个比较特殊的value值类型,可以实现各种嵌套。也就是我们平时所说的JSON套JSON 数组 写数组和写对象有点类似,也会有先start再end的闭环思路。如何向数组里写入Value值?我们知道JSON数组里可以装任何数据类型,因此往里写值的方法都可使用,形如这样: @Test public void test5() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("zhName"); jsonGenerator.writeString("A哥"); // 写数组(记得先写key 否则无效) jsonGenerator.writeFieldName("objects"); jsonGenerator.writeStartArray(); // 1、写字符串 jsonGenerator.writeString("YourBatman"); // 2、写对象 jsonGenerator.writeStartObject(); jsonGenerator.writeStringField("enName", "YourBatman"); jsonGenerator.writeEndObject(); // 3、写数字 jsonGenerator.writeNumber(18); jsonGenerator.writeEndArray(); jsonGenerator.writeEndObject(); } } 运行程序,输出: {"zhName":"A哥","objects":["YourBatman",{"enName":"YourBatman"},18]} 理论上JSON数组里的每个元素可以是不同类型,但原则上请确保是同一类型哦 对于JSON数组类型,很多时候里面装载的是数字或者普通字符串类型,因此JsonGenerator也很暖心的为此提供了专用方法(可以调用该方法来一次性便捷的写入单个数组): @Test public void test6() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("zhName"); jsonGenerator.writeString("A哥"); // 快捷写入数组(从第index = 2位开始,取3个) jsonGenerator.writeFieldName("values"); jsonGenerator.writeArray(new int[]{1, 2, 3, 4, 5, 6}, 2, 3); jsonGenerator.writeEndObject(); } } 运行程序,输出: {"zhName":"A哥","values":[3,4,5]} 布尔和null 比较简单,JsonGenerator各提供了一个方法供你使用: public abstract void writeBoolean(boolean state) throws IOException; public abstract void writeNull() throws IOException; 示例代码: @Test public void test7() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("success"); jsonGenerator.writeBoolean(true); jsonGenerator.writeFieldName("myName"); jsonGenerator.writeNull(); jsonGenerator.writeEndObject(); } } 运行程序,输出: {"success":true,"myName":null} 组合写JSON Key和Value 在写每个value之前,都必须写key。为了简化书写,JsonGenerator提供了二合一的组合方法,一个顶两: @Test public void test8() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeStartObject(); jsonGenerator.writeStringField("zhName","A哥"); jsonGenerator.writeBooleanField("success",true); jsonGenerator.writeNullField("myName"); // jsonGenerator.writeObjectFieldStart(); // jsonGenerator.writeArrayFieldStart(); jsonGenerator.writeEndObject(); } } 运行程序,输出: {"zhName":"A哥","success":true,"myName":null} 实际使用时,推荐使用这些组合方法去简化书写,毕竟新盖中盖高钙片,一片能顶过去2片,效率高。 其它写方法 如果说上面写方法是必修课,那下面的write写方法就当选修课吧。 writeRaw()和writeRawValue():该方法将强制生成器不做任何修改地逐字复制输入文本(包括不进行转义,也不添加分隔符,即使上下文[array,object]可能需要这样做)。如果需要这样的分隔符,请改用writeRawValue方法。 绝大多数情况下,使用writeRaw()就够了,writeRawValue的使用场景愈发的少 @Test public void test9() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) { jsonGenerator.writeRaw("{'name':'YourBatman'}"); } } 运行程序,输出: {'name':'YourBatman'} 如果换成writeString()方法,结果为(请注意比较差异): "{'name':'YourBatman'}" writeBinary():使用Base64编码把数据写进去。 writeEmbeddedObject():2.8版本新增的方法。看看此方法的源码你就知道它是什么意思,不解释: public void writeEmbeddedObject(Object object) throws IOException { // 01-Sep-2016, tatu: As per [core#318], handle small number of cases if (object == null) { writeNull(); return; } if (object instanceof byte[]) { writeBinary((byte[]) object); return; } throw new JsonGenerationException(...); } writeObject()(重要):写POJO,但前提是你必须给JsonGenerator指定一个ObjectCodec解码器才能正常work,否则抛出异常: java.lang.IllegalStateException: No ObjectCodec defined for the generator, can only serialize simple wrapper types (type passed cn.yourbatman.jackson.core.beans.User) at com.fasterxml.jackson.core.JsonGenerator._writeSimpleObject(JsonGenerator.java:2238) at com.fasterxml.jackson.core.base.GeneratorBase.writeObject(GeneratorBase.java:391) ... 值得注意的是,Jackson里我们最为熟悉的API ObjectMapper它就是一个ObjectCodec解码器,具体我们在数据绑定章节会再详细讨论,下面我给出个简单的使用示例模拟一把: 准备一个User对象,以及解码器UserObjectCodec: @Data public class User { private String name = "YourBatman"; private Integer age = 18; } // 自定义ObjectCodec解码器 用于把User写为JSON // 因为本例只关注write写,因此只需要实现此这一个方法即可 public class UserObjectCodec extends ObjectCodec { ... @Override public void writeValue(JsonGenerator gen, Object value) throws IOException { User user = User.class.cast(value); gen.writeStartObject(); gen.writeStringField("name",user.getName()); gen.writeNumberField("age",user.getAge()); gen.writeEndObject(); } ... } 测试用例: @Test public void test11() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) { jsonGenerator.setCodec(new UserObjectCodec()); jsonGenerator.writeObject(new User()); } } 运行程序,输出: {"name":"YourBatman","age":18} 这就是ObjectMapper的原理雏形,是不是开始着道了? writeTree():顾名思义,它便是Jackson大名鼎鼎的树模型。可惜的是core模块并没有提供树模型TreeNode的实现,以及它也是得依赖于ObjectCodec才能正常完成解码。 方法用来编写给定的JSON树(表示为树,其中给定的JsonNode是根)。这通常只调用给定节点的writeObject,但添加它是为了方便起见,并使代码在专门处理树的情况下更显式。 可能你会想,已经有了writeObject()方法还要它干啥呢?这其实是蛮有必要的,因为有时候你并不想定义POJO时,就可以用它快速写/读数据,同时它也可以达到模糊掉类型的概念,做到更抽象和更公用。 说到模糊掉类型的的操作,你也可以辅以Spring的AnnotationAttributes的设计和使用来理解 准备一个TreeNode的实现UserTreeNode: public class UserTreeNode implements TreeNode { private User user; public User getUser() { return user; } public UserTreeNode(User user) { this.user = user; } ... } UserObjectCodec改写如下: public class UserObjectCodec extends ObjectCodec { ... @Override public void writeValue(JsonGenerator gen, Object value) throws IOException { User user = null; if (value instanceof User) { user = User.class.cast(value); } else if (value instanceof TreeNode) { user = UserTreeNode.class.cast(value).getUser(); } gen.writeStartObject(); gen.writeStringField("name", user.getName()); gen.writeNumberField("age", user.getAge()); gen.writeEndObject(); } ... } 书写测试用例: @Test public void test12() throws IOException { JsonFactory factory = new JsonFactory(); try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) { jsonGenerator.setCodec(new UserObjectCodec()); jsonGenerator.writeObject(new UserTreeNode(new User())); } } 运行程序,输出: {"name":"YourBatman","age":18} 本案例绕过了TreeNode的真实处理逻辑,是因为树模型这块会放在databind数据绑定模块进行更加详细的描述,后面再会喽。 说明:Jackson的树模型是比较重要的,当然直接使用core模块的树模型没有意义,所以这里先卖个关子,保持好奇心哈 思考题 国人很喜欢把Jackson的序列化(写JSON)效率和Fastjson进行对比,那么你敢使用本文的流式API和Fastjson比吗?结果你猜一下呢? 总结 本文介绍了jackson-core模块的流式API,以及JsonGenerator写JSON的使用,相信对你理解Jackson生成JSON方面是有帮助的。它作为JSON处理的基石,虽然并不推荐直接使用,但仅仅是应用开发级别不推荐哦,如果你是个框架、中间件开发者,这些原理你很可能绕不过。 还是那句话,本文介绍它的目的并不是建议大家去项目上使用,而是为了后面理解ObjectMapper夯实基础,毕竟做技术的要知其然,知其所以然了后,面对问题才能坦然。
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位小伙伴大家好,我是A哥。上篇文章 【Fastjson到了说再见的时候了】 A哥跟Fastjson说了拜拜,从本系列开始,我们将一起进入Jackson库的学习。当然喽说它是世界上最好的JSON库并非一家之言,是官网上它自己说的,我免责申明哈。 一个工程仅需一个JSON库 Java的JSON库,你至少应该用过/听过这三种:Jackson、Gson、Fastjson。一个独立的工程,按照依赖最少原则,本应该only one JSON库是足矣的。但现状是:各位同仁可观察观察各自的项目,大都同时存在2种JSON库,亦或者3种甚至更多... 说明:在同一个工程内,同一功能若有多种实现,实属不好的现象。这会让管理起来显得混乱(譬如对日期的格式化就不方便做到统一),出口若有多个,想收口时就是个大难题了 作为一个合格的架构师/工程师,保持最简依赖(一致性依赖)是应该有的技术范,因为简单一致性它能带来很大的收益,道理很简单:两个Java程序员的沟通/协同效率,一定会比1个Java + 1个Python沟通效率高。 so what,我们应该选择哪一种JSON库呢?答案显而易见,那便是Jackson。因为它各个方面表现均非常优秀,是世界最流行、最好的JSON库。把Jackson作为工程唯一JSON库是有一丢丢门槛的(想想你为何使用Fastjson就知道啦),所以它来啦,祝你跨越此门槛,规范化使用,助你增加一项主流的硬核实力,这也是本专栏的最大意义所在。 Jackson是世界最好的JSON库 Jackson是一个简单的、功能强大的、基于Java的应用库。它可以很方便完成Java对象和Json对象(xml文档or其它格式)进行互转。Jackson社区相对比较活跃,更新速度也比较快。Jackson库有如下几大特性: 高性能且稳定:低内存占用,对大/小JSON串,大/小对象的解析表现均很优秀 流行度高:是很多流行框架的默认选择 容易使用:提供高层次的API,极大简化了日常使用案例 无需自己手动创建映射:内置了绝大部分序列化时和Java类型的映射关系 干净的JSON:创建的JSON具有干净、紧凑、体积小等特点 无三方依赖:仅依赖于JDK Spring生态加持:jackson是Spring家族的默认JSON/XML解析器(明白了吧,学完此专栏你对Spring都能更亲近些了,一举两得) 版本约定:本专栏统一使用的版本号固定为2.11.0(2020-04发布),GAV如下: <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.11.1</version> </dependency> 为了保持版本的统一性,后续的Spring Boot(2.3.0.RELEASE)/Spring Framework(5.2.6.RELEASE)使用的均为当前最新版本,因为它内置的jackson也恰好就是本专栏讲解的版本。 正文 细心的朋友从上面的groupId里可以看到:jackson它隶属于fasterxml这个组织。本着追本溯源的精神,可以稍微的了解了解这个组织:fasterxml官网 截图如下 简单翻译:FasterXML是Woodstox流的XML解析器、Jackson流的JSON解析器、Aalto非阻塞XML解析器以及不断增长的实用程序库和扩展家族背后的业务。 作为一个高度流行的开源库,这种官网页面应该刷新了你的认知吧。并不是它内容不多,而其实是它的详细介绍都发布在github上了,这便是接下来我们来认识它的主要渠道。 这种做法貌似已经成为了一种流行的趋势:越来越多的开源软件倾向于把github作为他们的Home Page了 fasterxml组织它直属的一级工程其实也有不少: com.fasterxml.jackson com.fasterxml.uuid com.fasterxml.woodstox ... 很显然,本专栏仅仅只会关注jackson工程,该工程便是该组织最出名且最最最为重要的部分。 官网介绍 了解一门新的技术,第一步应该就是看它的官网。上面已然解释了,fasterxml组织它把各工程的首页内容都托管在了github上,Jackson当然也不例外。Jackson官网 上对它自己有如下描述: Jackson旧称为:Java(或JVM平台)的标准JSON库,或者是Java的最佳JSON解析器,或者简称为“Java的JSON” 从这名字就霸气外露了,NB得不行,足以见得它在JSON解析方面的地位和流行程度,当然主要是自信 更重要的是,Jackson是一套JVM平台的 数据处理(不限于JSON) 工具集:包括 一流的 JSON解析器/ JSON生成器、数据绑定库(POJOs to and from JSON);并且提供了相关模块来支持 Avro, BSON, CBOR, CSV, Smile, Properties, Protobuf, XML or YAML等数据格式,甚至还支持大数据格式模块的设置。 分支:1.x和2.x Jackson有两个主要的分支: 1.x分支,1.9.13。处于维护模式,只发布bug修复版本(最近一次发布于Jul, 2013) 2.x是正在开发的版本(持续更新升级中,2.0.0发布于Mar, 2012) 注意:这两个主要版本使用不同的Java包名和Maven GAV,因此它们并不相互兼容,但可以和平共存。一个项目可以同时依赖于这两个版本是没有冲突的。这是经过设计而为之,选择这种策略是为了更顺利地从1.x进行迁移2. x 说明:现在都2020年了,1.x可以毫不客气的说已经淘汰了(除了非常老的项目还在用),因此针对1.x版本本专栏不会有任何涉猎。 活跃的2.x分支说明 目前2.x分支存在如下活跃的分支们: 2.12:下一个小版本,目前正在开发中 2.11:当前稳定版。积极维护的分支,会积极持续发布补丁 2.10:上一个稳定的分支,没有停止维护仍会发布补丁 2.9: 非活跃分支,只会发布微补丁了,虽然还活跃但活跃度较低 其它2.x分支:只会发布重大安全补丁 master:主分支。下一个主要分支3.0正在快速开发中 说明:对于2.11.0和2.11.x这种小分支之间的区别,可以忽略 模块介绍 Jackson是个开源的、且开放的社区。下面列出的大多数项目/模块是由Jackson开发团队领导的,但也有一些来自Jackson社区的成员 三大核心模块 core module(核心模块) 是扩展模块构建的基础。Jackson目前有3个核心模块: 说明:核心模块的groupId均为:<groupId>com.fasterxml.jackson.core</groupId>,artifactId见下面各模块所示 Streaming流处理模块(jackson-core):定义底层处理流的API:JsonPaser和JsonGenerator等,并包含特定于json的实现。 Annotations标准注解模块(jackson-annotations):包含标准的Jackson注解 Databind数据绑定模块(jackson-databind):在streaming包上实现数据绑定(和对象序列化)支持;它依赖于上面的两个模块,也是Jackson的高层API(如ObjectMapper)所在的模块 实际应用级开发中,我们只会使用到Databind数据绑定模块,so它是本系列重中之重。下面介绍那些举足轻重的第三方模块。 数据类型模块 这些扩展是Jackson插件模块(通过ObjectMapper.registerModule()注册,下同),并通过添加序列化器和反序列化器来对各种常用Java库数据类型的支持,以便Jackson databind包(ObjectMapper / ObjectReader / ObjectWriter)能够顺利读写/转换这些类型。 第三方模块有些是Jackson官方人员直接lead和维护的(主流模块),也有些是纯社区行为。现在按照这两个分类分别介绍一下各个模块的作用: 官方直接维护: 说明:官方维护的这些数据类型模块的groupId统一为:<groupId>com.fasterxml.jackson.datatype</groupId>,且版本号是和主版本号保持一致的 标准集合数据类型模块: Guava:支持Guava的集合数据类型 HPPC:略 PCollections:略 (Jackson 2.7新增的支持) Hibernate:支持Hibernate的一些特性,如懒加载、proxy代理等 Joda:支持Joda date/time的数据类型 JDK7:对JDK7的支持(说明:2.7以后就无用了,以为2.7版本后最低的JDK版本要求是7) Java8:它分为如下三个子模块来支持Java8 jackson-module-parameter-names:此模块能够访问构造函数和方法参数的名称,从而允许省略@JsonProperty(当然前提是你必须加了编译参数:-parameters) jackson-datatype-jsr310:支持Java8新增的JSR310时间API jackson-datatype-jdk8:除了Java8的时间API外其它的API的支持,如Optional JSR-353/org.json:略 非官方直接维护: 说明:非官方直接维护的这些模块groupId是不定的,每个模块可能都不一样,并且它们的版本号不会随着官方的主版本号一起走 jackson-datatype-bolts:对 Yandex Bolts collection types 的支持 jackson-datatype-commons-lang3:支持Apache Commons Lang v3里面的一些类型 jackson-datatype-money:支持javax.money jackson-datatype-json-lib:对久远的json-lib这个库的支持 ... 数据格式模块 Data format modules(数据格式模块)提供对JSON之外的数据格式的支持。它们中的大多数只是实现streaming API抽象,以便数据绑定组件可以按原样使用。 官方直接维护: 说明:这些数据格式的模块的groupId均为<groupId>com.fasterxml.jackson.dataformat</groupId>,且跟着主版本号走 Avro/CBOR/Ion/Protobuf/Smile(binary JSON) :这些均属于二进制的数据格式,它们的artifactId为:<artifactId>jackson-dataformat-[FORMAT]</artifactId> CSV/Properties/XML/YAML:这些格式熟悉吧,同样的支持到了这些常用的文本格式 非官方直接维护:因非官方直接维护的模块过于偏门,因此省略 JVM平台其它语言 官网有说,Jackson是一个JVM平台的解析器,因此语言层面不局限于Java本身,还涵盖了另外两大主流JVM语言:Kotlin和Scala 说明:这块的groupId均为:<groupId>com.fasterxml.jackson.module</groupId>,版本号跟着主版本号走 jackson-module-kotlin:处理kotlin源生类型 jackson-module-scala_[scala版本号]:处理scala源生类型 模式支持 Jackson注解为POJO定义了预期的属性和预期的处理,除了Jackson本身将其用于读取/写入JSON和其他格式之外,它还允许生成外部模式。上面已讲述的数据格式扩展中包含了部分功能,但也仍还有许多独立的模式工具,如: Ant Task for JSON Schema Generation:使用Apache Ant时,使用Jackson库和扩展模块从Java类生成JSON jackson-json-schema-maven-plugin:maven插件,用于生成JSON ... 说明:本部分因实际应用场景实在太少,为了不要混淆主要内容,此部分后面亦不会再提及 Jackson jr(用于移动端) 虽然Jackson databind(如ObjectMapper)是通用数据绑定的良好选择,但它的占用空间(Jar包大小)和启动开销在某些领域可能存在问题:比如移动端,特别是对于轻量使用(读或写)。这种case下,完整的Jackson API是让人接受不了的。 由于所有这些原因,Jackson官方决定创建一个更简单、更小的库:Jackson jr。它仍旧构建在Streaming API之上,但不依赖于databind和annotation。因此,它的大小(jar和运行时内存使用)要小得多,它的API非常紧凑,所以适合APP等移动端。 <dependency> <groupId>com.fasterxml.jackson.jr</groupId> <artifactId>jackson-jr-objects</artifactId> </dependency> 它仅仅只依赖了jackson-core模块,所以体积上控制得非常的好。Jackson单单三大核心模块大小合计1700KB左右(320 + 70 + 1370)。而Jackson jr的体积控制在了95KB(就算加上core模块的320也不到500KB)。 而对于开发Java后台的我们对内存并不敏感,简单易用、功能强大才是硬道理。因此jackson-jr只是在此处做个简单了解即可,本专栏后面也不会再提及。 漏洞报告 Jackson虽然已经足够稳定并且安全了,但哪有圣人呢。针对它的相关漏洞报告,最近一次发生在2019-07-23:FasterXML jackson-databind 远程代码执行(CVE-2019-12384)更多、更新的详细漏洞报告参考链接(持续更新中):知道创宇Jackson漏洞报告 Java JSON库比较 市面上的JSON库非常之多,综合一些Java人员的意见,关于使用哪个库,这里有一些现有的独立比较的链接供以你参考: Top 7 Open-Source JSON-binding providers Be a Lazy but a Productive Android Developer, Part 3: JSON Parsing Library "Can anyone recommend a good Java JSON library" (Linked-In group) "Which JSON library to use on Android?" 说明:此处贴出的几个参考链接均为官网给出的参考文章,均为国外较权威的文献。 当然天朝的你可能更关心Jackson和Fastjson的对比,那暂先不用着急(虽然上文也比较过),这是本专栏后面的一道主菜,那里会详细道来。 总结 本文结合官网认识了Jackson的全貌,用全面的视角整体上把握到了Jackson所提供的功能模块,这为专栏后续的讲解提供一个索引。 从Jackson的升级之快、模块支持之多足矣看得见它社区的活跃。并且为了迎合市场它在2.10版本后还提供了商业支持的服务:与Tidelift公司合作,为用户构建应用程序的开源依赖项提供商业支持和维护。节省时间、降低风险和改善代码健康状况(商业支持是收费的)。 相信通过本文你对Jackson有了个大概的了解,不出意外你应该是有兴趣去学它了的。当你深入研究后会发现它的设计之优雅,扩展性之强,不是一般国产类库所能比拟的。如果说Fastjson是一个优秀的JSON库,那么Jackson就是一个更优秀的JSON生态。 本专栏在CSDN付费,在公众号全部免费公开,欢迎你关注A哥的公众号【BAT的乌托邦】
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位好,我是A哥(YourBatman)。以下文章来源于softech华山论剑 ,作者徐凌云。 技术选型是个很大的话题。「灵活」与「高开发效率」是技术选型最看重的两点。感谢徐总的分享,很受用。 正文 关于技术选型,我们不少技术从业的朋友容易进一些误区,而这些误区大多俗话是某种技术开发思维定势在作怪。选型怕遇到喷子,也怕诋毁性总结。技术选型没选好,每往前走一步,都可能变成挨揍的理由,也是让人心碎得理由。 技术选型 我相信自驱动的团队学习,意识提升,分析度量,团队信任,勇敢能做好选型的更好实践。当然技术选型中也存在着天时、地利、人和。技术选型的能力是一个各方面综合作用的能力,而不是仅仅我们认为的技术范畴。 很多技术同学对新技术有天生得冲动,有时候开发人员自己玩的很high,但项目却玩死了,这是作为技术管理者需要面对的魔鬼。这是件很悲哀的事情,我们需要抑制内心深处的魔鬼,技术只有跟业务有机的结合起来,产出所追求的价值,才是有意义的。 在工作中完成一次技术选型,绝不能简单的仅仅从纯技术角度出发思考。一次看似偶然的选型会给后续工作带来方向性的影响,这里的影响指的不光是技术层面,更多的是管理层面。这就如同在公司一次公开的项目招标中,考虑绝不仅仅是解决方案本身的优劣,更重要的考量方案的成本是否符合预期,方案提供方的实力、诚信度,甚至还要从商业模式上去思考未来的合作方式是什么,等等。而这一切,都能在一次技术选型的过程中,得以体现。下面就从几个主要阐述下选型中遇到的常见问题。团队的稳定性重要性要远大于一些其他的因素的重要性。 技术选型误区与雷区 做任何决策时,搜集资料,无论是在简书,掘金,公众号还是csdn这些平台上,亦或是开源项目地址和官网上,请记住:最重要的不是它告诉了你什么,而是它对你隐瞒了什么,这些隐瞒的信息最终会置你于险境。 搜集资料的时候如果资料的作者对某项技术具有显著的倾向性时,请深入想想,他向你推荐的每一项优点是否真的“对你”有价值,以及它背后的代价是什么。比如,推崇“自由”的技术往往不够“严谨”,如果你的产品需要严谨,那么请把“自由”看做减分项而不是加分项。比如,推崇“体积小”的技术在现在动辄几T硬盘、几M带宽的环境下,到底对你来说有多大价值?它是不是因为没有其它的优点了才把这种细枝末节亮出来吸引你? 如何避免减少技术选型踩坑或者踏雷呢,在这里我们需要一些原则和意识进行精确的指导。 技术选型原则 我们为了团队影响力适度使用新技术,也鼓励在各方面情况需要的时候造点轮子,但是前提是稳定第一,并且还要善于应用新技术和自己造成熟了的轮子。 技术选型的意识 生命周期的意识 《聊聊架构》这本书,贯穿全书的词恐怕就是生命周期了。系统都有他系统特性所带的生命周期,从生到死,经历少年、中年、老年三个阶段。复杂度的管理贯穿系统的整个生命周期,就像进化论的自然选择一样,不停的优化着系统,不停的断舍离,保持着系统的生命力。 度量意识 《人月神话》是把软件工程的过程量化的国内最早的一本书。没有度量无法说服自己,更别谈说服团队。 突破定势思维 突破自己的技术思维定势的意识个体技术认知是有局限的,如何来打破这种局限? 学习,分享,交流,提升,这也是技术创新的基础。 权衡取舍意识 选型也是一种精细化选择,权衡取舍意识我们技术和工程领域的朋友很多都是完美主义者,追求完美,但是我么要明白没有银弹。 技术选型的取舍也是一种取舍的艺术。不仅仅是限于技术视野的综合判断力的体现。 有时候不必纠结于技术本身的挑战踩坑的认可自己的观念,而是在遇到技术难题,长时间无法解决的时候,可以选择绕口,曲线超车。不把过多经历放在细微之处,而把精力聚焦到核心问题上。 职责划分意识 做好选型也需要格局,不仅仅是深深认识到业务规模是发展变化的,技术是演进迭代的,还有企业的商业战略(技术人也要培养商业敏感度)。还有技术选型谁主导,谁参与,谁监督。 运维需要参与吗,测试需要参与吗,安全部门需要参与吗,当然谁需要参与取决于选型的对象的规模和选型的目的。 风险意识 需要识别好风险,在清楚风险的基础上,考虑推进过程中的影响面以及推进过程需要把握的度。特别强调关于已知的未知 和 未知的未知进行区分,一个人花了四个月时间试图弄清楚为什么会出现 GC 停顿,结果发现是因为他往文件中写入了统计信息。显然,他事先并不知道会发生这样的事情。软件中的很多 bug 都是这样的。我们并不知道系统里存在这些 bug,它们都是"未知的未知"。 一个项目最好超过30%得新技术,对于完全未知的新技术,很难控制使用过程中出现的风险。如果技术leader不能得到下属的尊重,很可能受到惩罚。 产品意识 作为使用者是否有能力解决问题,给你一本亿级流量,但是需求是只需要做个管理系统给国企下面一个部门的办公室几个行政人员使用,而且可能一个月也就使用几次。这其实反应了技术人员的产品意识。 很多技术人员喜欢玩酷的东西,愿意探索新的领域,把不可能的变成可能,但是很多时候,他们做出来的东西很难使用。 技术发展的史学观意识 中国近现代是一部师夷长技以制夷的历史,而当年的中国逐步从这种现状转变成自主创新的国家,虽然完全自主创新的科技少之又少,但是这种变化标志着中国对科技的认知和科技长远发展树立了一个里程碑,而这一点在计算机应用领域更是淋漓尽致,造福每一个当代人。我深信多读历史才能增长智慧。 只有了解技术的发展历史,才能更好,更精准,更稳的做好技术选型。喜欢深入研究技术的从业人员多半会喜欢读一些技术发展史,如《数学之美》这本书就是历史的看从信息论到计算机的发展史。这本书是本不错的计算机从业人员的计算机启蒙书籍。 技术选型考量因素 成本考量 在开发层面,造轮子和开源是我们技术从业同学绕不开的两个问题。很多技术领导害怕重造轮子, 大多数技术领导层希望尽量避免造轮子。我们都知道重造轮子会耗费人力和时间成本。前段技术琐话右导组织过一场造轮子的讨论,有几个同学的观点挺有意思。一个同学提出,造轮子的几个正面论调:自我展示和他人超越;自我保护和代码安全;自我超越和代码专利。 还有个同学说到开源不易,做得不好没人用,做得太好又投入不起。所谓的造轮子是小事,重要的是解决开源能更好的提供服务,商业更好的服务开源,只有达成良性循环才能有更好的开源,有更全面的服务,商业与开源相辅相成。还有个同学聊的很多,大体提到三点,认知问题和商业利益以及自我能力提升,认为自己的需求独一无二,现有的库就是在某个点上满足不了,对现有的轮子理解不知比如老轮子没有规格说明书,或者接口太复杂,不知道怎么用,搞明白太难,不相信老轮子。 譬如老轮子可能有后门、漏洞(想想OpenSSL的心脏出血漏洞)、后期万一要修改没把握等,反正是觉得自己造轮子心里更踏实,需要对老轮子上添加新功能,然而老轮子代码难读无人可问,不知道何时能弄明白,看不到结果,容易放弃,眼界有限,不知道已有这样的轮子,版权原因无法使用第三方库,比如Google Android实现JVM(Google曾因为一行代码而和Oracle打官司),比如阿里YunOS自己实现JVM不想让自己产品的关键技术掌握在别人手里,也不想让自己的核心用户数据流经别人的系统,别人的轮子不开放,我就是要赶紧造(山寨)一个出来以便获得话语权或商业利益,就想锻炼自己,因为造轮子对自己的设计、编码能力有很大好处,对理解业务也有很大好处。 当你造轮子的时候,你要考虑到总有一天还可能会换轮子,互联网行业换轮子是一种高风险操作,有时候我们也偶尔要说服自己,自己造轮子反倒是可能挖坑了。因为确实遇到了一些技术小伙伴参数调优没研究透,遇到问题就认为组件或者框架不好,想自己造轮子的。之所以提到造轮子和开源的问题,是因为做技术选型很多时候我们都会遇到这两个问题。无论是造轮子还是开源项目,代码的可读性和可维护性也是很重要的考量因素。开发效率还是执行效率,这个选择是个老生常谈的问题。对于不同阶段的公司和项目会有不同的选择。新的商业项目更趋向于选择开发效率优先。因为商业模式的尽早验证比其他因素更重要。老项目优化更多提到执行效率,性能调优等问题。 团队考量 选大家熟悉的,方便开发,排查问题。 康威定律深刻地影响着很多方面,技术选型也不例外。特别是做宏观技术选型时,必须考虑它在最终技术架构中的位置,以及与团队沟通结构的匹配程度。即使是一项很先进的技术,假如它与体系中的其它技术栈不匹配,也可能导致翻车。 当选择多个第三方库的时候更要加倍小心,因为它们开发时互相不知道彼此的存在,特别是对于一些较新的技术,可能都没人把它们搭配使用过。除了开发架构之外,还要考虑更广泛的运维架构。 除非你是个前后端 + DevOps 全栈,否则就需要尽早对组织架构方面的因素进行验证并排除风险。也就是说,在一个可控的演习环境中,用一个小型案例,完整地走一遍开发、上线、发新版的流程。在这个过程中,一些显著的风险将会暴露出来,要评估其影响,来决定如何选型。 我认为技术管理者身上必须有一个特质:技术布道。充分激发团队的技术热情,感染团队里的每个成员,是技术管理者必备的人格魅力。一项技术适不适合团队,只有用了才知道。但是技术管理者大可不必亲力亲为,尤其是CTO,总监级别的管理者,只需要指定技术目标,保持一定的技术热情即可,最后再积极配合进行技术布道。 不同的企业,不同的部门,不同的团队管理模式不同,技术文化不同,当然就很可能在技术选型上发生不一样的故事,像一个技术同学的团队teamleader+业务架构师+技术架构,进行民主决策,少数服从多数。这个方式体现了组织架构的重要性,技术架构师和业务架构师以及teamleader。有的企业有技术架构委员会,会参与大项目的技术选型的评审,有些时候可能他们对待选型的对象所选择的技术并不太熟悉的时候,他们最差也能站在自己技术经验的角度考量一些可能会遇到的问题。提到这个主要强调组织架构对技术选型的影响力,《架构即未来》这本书,对弹性,高效的组织架构做了很详细的阐述。 方案1/2/3...N,综合评价,倾向选型优缺点详细列明,如果某个环节可能会出现问题,风险备案是什么。并且附上开发计划排期,关键事件的里程碑和时间截点。相信上面也是能看得懂的,这个时候就看要你个人游说能力了。那这样好不好,有一句话说的好,人捋顺了,事就好办了。但是也有另一面,因人而废制度,就变成了人治,谁的嗓音高,谁的权限大,谁就有话语权,这样的组织长久不了,团队会潜移默化的形成个因人而成事,因人而废法这样的团队你呆个两三年出来之后基本就知会唯唯诺诺,很难养成自己的独立思考和决策能力。 那究竟事制度重要还是人重要呢,这是另一个层面的讨论。这个话题的思考从《大江大河》里面老水的因人成事,因人废事那句话开始。因为我认为错的人好的制度,未必能办成好事,但是犯错误的可能性会低一些。对的人好的制度是比较完美的状态。先客观的从团队各维度梳理团队现状,然后据此选型,要知道选型错误只能团队来承受。阿波罗神庙上镌刻着一句警世名言-了解你自己。千万不要懒于梳理,懒于总结盲从潮流。惰性确实是技术发展的驱动力之一, 而过于懒惰却不是。 团队技术选型自然会选择团队所有成员熟悉的技术,否则会出现开发节奏问题。比如所有人熟悉 Java,小部分人熟悉 Scala 的情况下,需要忍痛割爱选择即使从其他层面考虑更适合的 Scala 语言。一般情况下,某项技术至少需要 1 – 2 位高级工程师解答遇到的所有相关问题,这位工程师需要从源码级别理解这项技术。优先选择成熟技术。 技术选型一定要考虑当前团队人员的技术组成。对于一些比较基础的技术的选型,比如语言和框架、数据库等等,往往最合适的选择就是团队最熟悉的技术。如果打算选择不同的技术的话,那就要考虑下团队是否有人能够 Hold 住了。另一个必须要考虑的是招聘,如果使用的是比较小众的技术,那么当需要扩充团队的时候,招聘人员会比较困难。 还有一点就是,虽然技术选型需要考虑团队人员的喜好,但千万不要因为某几个人的个人喜好,来决定技术的选型。还是通过细致的分析和实验来进行选型。而决策者也需要看的更长远一些,推动团队技术向前发展。 我们经常会遇到,一项新技术在公司内久久难以推行,因为业务主管的阻挠。即使排除利益纠葛,仍然会发现一种发自内心的不信任存在。而这种不信任,又往往来源于对同事工作的不认可。 对于技术管理者,在技术选型时,重点还需注意团队人员流动性。人员流动带来的损失比大多数人所认为的要大得多。人员流动会带走知识和文化。企业要避免损失,就要把这些知识和文化尽可能记录在代码中。 当然,这并不意味着应该要求大量写注释,而应该使用那些能留存知识的技术,比如类型系统和规范化命名。类型系统和规范化命名可以半强制性地要求开发人员把原本只存在于自己脑子里的知识记录到代码中。如果更有追求一点,可以再尝试普及单元测试。这样,当他离开的时候,即使没有文档,这些知识也仍然能留存下来。从效果上说,代码往往比文档和注释更好。而文化的留存则更加困难,事实上,代码中的奇葩注释往往留存的是负面文化。应该在代码中留存的文化,是严谨、专业的工作态度。虽然自由也是文化的一部分,甚至在管理领域是非常值得向往的文化,但在工程领域,它往往是一种负面文化,因为软件开发领域并没有公认的法律甚至道德。你可以想象一下管理领域中没有约束的自由会导致怎样的后果。 所以,要想应对人员流动的风险,除非你有信心留存知识与文化,否则就应该在技术选型时,倾向于选择更加严谨的、隐式信息更少的技术。 项目产品考量 短生命周期的产品通常要求快速起步:门槛低、书写自由、不强制遵循任何最佳实践。当它的使命结束时,代码会被直接抛弃。所以,对于这类产品,“快糙猛”的技术是较好的选择,当然,能做到“快精猛”更佳。 而长生命周期的产品则会强烈要求可维护性,因为它们在很长时间内都是不可报废的。甚至对于一些生命线产品,连重写都会要求在重写期间线上系统平稳过渡,一点点迁移到新技术。 这种要求对团队的工程化能力是个极端的考验。如果没有相应的工程能力,其代价甚至会高于用新技术重新写一个功能相同的系统。 稳定第一的项目比如银行项目,虽然不少银行也研究新技术,但是较少用在生产,因为银行受到评级和监管约束,一旦将新技术引入线上,会导致评级下降,监管问询等。 探索型产品往往也是短周期产品,但是同时也有自己的特点。它要求快速,但往往同时会要求高质量。探索型的产品如果证明了可行性,那么过渡到长生命周期的可能性很大。 这就要求它最好是一个微内核系统,提前留出一些扩展的空间。当然,设计微内核系统对架构师的能力具有相当的考验,如果没有一个优秀的架构师,建议还是不要刻意做任何预留,优先保障系统的简单性。 除此之外,探索型产品的技术栈必须支持可靠的、自动化的重构。因为探索型产品的迭代速度很快,如果完全靠人工去添加功能并手动重构,那么一旦出现 BUG,将给此产品的用户体验带来严重的负面影响。 所以,除非由于人才储备等原因而被迫做出折中,否则探索型产品的技术栈一定要快速而严谨。而对守成型产品的选型则会侧重于与现有技术栈的相似程度和无缝整合能力。如果整合时需要借助很多技巧,那么可能你就是在给自己挖坑。 在引入新技术的过程中,要尽可能符合现有的开发流程、基础设施和开发习惯。当然,如果现有的这些已经严重过时,那么应该找新老技术的专家,共同帮你设计一个路线图,让你可以平稳地引入新技术,这份投资绝对值得。如果老技术已经有新版本,则应该优先考虑升级它。不要幻想换个技术栈就能解决一切问题,事实上,它带来的问题往往会更多。 业务考量 所有脱离业务需求的技术方案,都是耍流氓。只有真正契合业务上下文的方案才是好的方案,而每个项目都有自己的特殊性,需要把项目上下文尽可能了解清楚,找出项目成败最核心的1到2个标准,以此作为基础来做选择题。譬如说,创业项目,灵活是明显的述求,产品推出后必然会面对朝令夕改的需求,如何快速反应是选型的重点。再譬如说,陈年项目性能遇到瓶颈需要重构,再往下挖可能原有系统吞吐量不成问题,但瞬时响应太差,选型时就需要特别注意这个点。 如何做好技术选型 如果我们从天时,地利,人和这几方面去梳理当前业务的情况,会发现很多我们原本没有注意的问题。我们必须从业务角度去梳理企业业务规划,业务战略以及当前业务遇到的痛点和难点,组织架构。团队成员的技术特征以及技术栈偏好。还有当前使用的技术情况等。 梳理当前能解决我们需求的开源项目或者工具,并分析一些核心指标能满足我们大部分的需求进行筛选并列出表格。从中我们梳理一些选型中会考量的核心指标,并把核心指标对应的权重做出表格,让相关人给对应的指标分配合适的权重,综合相关人的权重算出合适的权重。做出指标和权重的表格,让参与选型的相关人进行指标值打分。对打分结果进行计算,对不同的待选项的得分高低进行筛选。切记筛选核心指标要细致准确。 技术选型最好罗列至少3个以上的待选,并在选好型之后,还附加一个备选技术方案来兜底。我们应该了解选型对象的不同,有时候核心考量指标可能差距偏大。以大数据平台为例:这些指标是我和技术分享@华山论剑@湖北群组中的大数据平台架构师wander聊过后梳理的。因为在从业早期,对不同的选型对象,所偏向的指标在某几个点上是有很大差距的,大数据平台,中间件,云平台等等。 有一个点可能是共通的,就是无论是什么技术产品或者项目,我们都需要有个人能体系化的熟悉选型对象涉及的技术并了解相关生态,基于这点我们排除团队,成本等因素更可能做一个靠谱执行顺利的技术选型。 常见选型案例 开发语言,开发工具,项目管理工具,知识库,中间件,框架,存储,监控运维平台的选型是我们经常会遇到的。 通常我们选型会考虑一些通用的选型指标,具体的选型可以在通用选型的基础上具体化对应的指标,亦可根据选型的特殊性在通用选型的基础上筛选核心指标。以API网关为例,在某公有云公司的时候,当时开发一个人,运维属于跨部门沟通没有垂直SRE,由于当时公有云在发展初期的样子,规划是整个公有云的开放平台和网关,IAAS和PAAS部门几十条产品线陆续在开发结束等着接入API网关把能力开放给大客户。IAAS部门高级中间和产品高级技术总监对项目赶的很紧,但是人力投入不够,为什么人力投入不够,这个涉及到领导各方面意识的错位,这点错位一直到半年之后的一次公司重大组织架构调整之后才有所改观,对于网关和开放平台的主要研发来说,一个人从各部门沟通,方案,设计,维护,测试各个层面来讲,承受的压力都不小。2017年的时候可选的开源网关不如如今这么多,当时比较多的zuul1被技术圈的朋友在设计上和性能上诟病,sc gateway。从技术栈上看,lua栈的有kong和orange,基于OR自研。go栈的悟空,tyk,自研。java栈的zuul1,sc gateway刚开始版本v1在进行中(下半年才开始推广中小企业应用),zuul2一直未发。 面对当时的情况,有几个想法:1 找一个功能强大,口碑较好,方便易用的(如kong)快速验证上线,后期做二次开发。2 基于内部的RPC框架做开发,但是当时公有云部分正在去集团的RPC框架,纠结了一会。3 基于go语言改造一套,但是问题是我们部门多是java栈的,我对lua的比go熟一些,公有云很多是go栈的,又是一个纠结。真是天时,地利,人和三点都不沾。当时的处境如同,老板给你递了两个烂核桃,你还不得不吃。 还是得一步一步来,而且要快速去验证,于是快速对kong的社区和源码进行查阅,并快速部署,去验证。任何新东西的引入都会踩坑,我验证的过程中发现对于kong集群的通信,节点数据一致性,konga管理后台的配置和使用,kong的部署安装,数据库的支持,运维部署的复杂度等等都或多或少要不就是坑,要不就是不符合国内使用习惯和架构设计风格,但是唯一就是插件很丰富,功能很多。其实如果时间充裕一点,不是一个人做,我宁可选择java自研或者go自研。或者说当时kong不成熟吧,与kong的国外研发团队沟通的过程中却是也觉得,他们技术的深度可以,然后再不断的踩坑中进行着kong的二开之路。我相信这次选型不是一次充分的选型,当下可选的API网关很多,开源的也太多,soul,APISIX进入Apache基金会,sc gateway的成熟等等新成员陆续出现。 API网关除了上面通用的选型要素还应该注意什么呢。大多数人会说协议支持,路由粒度及策略,负载策略,安全扩展,高性能这样几块,这些属于技术要素里面我们需要考虑的细项。 我们根据通用选型指标给出对应的专家打分权重,筛选了几个备选的API网关:APISIX,soul,kong,tyk等。注意三个以下备选行不算有选择。 接下来基于通用指标考量之后,我们进行细化指标考量。带着以上指标去快速调研和验证,并填充上面表格,根据当前的选型背景选择合适的。至于具体如何量化的对网关进行选型,因为考虑到不同的团队面对的情况不一样,将在下一篇 API网关的设计中去聊API网关的量化要素。 题外话,最近和一些技术圈的兄弟准备发起《技术价值分享会(湖北) 暨老乡会》的聚会,希望技术朋友们,特别是老家在湖北的技术从业朋友,不管你在北京还是深圳,广州,或者在杭州,苏州,成都,武汉,我们都很欢迎和期待您的加入。
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位小伙伴大家好,我是A哥。停更1个月后回归啦,今天咱们聊聊一个比较有意思的话题:是否真的需要跟Fastjson说再见了? 我的态度 我在CSDN写过好些篇关于JSON的文章,特别是2020年专门写了一个付费专栏:享学Jackson这个专栏“销量”在我心目中还凑合,4个月“卖出”200份的样子(虽不值一提,但我很满足了),小小的一个JSON库而已,热度可见一斑。专栏里不可避免的提到了Jackson和Fastjson的比较,我本人一直持中立态度,主要原因有二: 两者都很流行(国内Fastjson流行度甚至超过Jackson),因此平时开发中我两者都用(需要随大流嘛) 国产开源软件是需要被支持的,即使现在还存在差距(联想下最初的国产手机和苹果手机的差距,再看看现在呢?) 当然,本文不一样了,必须得加点料。态度中立并不代表没有偏向:很明显我偏向于使用Jackson作为你的 唯一 JSON库。 从本文起,我将把CSDN里该付费专栏全部内容搬到公众号,免费助你轻松拥抱世界上最好的JSON库:Jackson。从本文起,我将把CSDN里该付费专栏全部内容搬到公众号,免费助你轻松拥抱世界上最好的JSON库:Jackson。从本文起,我将把CSDN里该付费专栏全部内容搬到公众号,免费助你轻松拥抱世界上最好的JSON库:Jackson。 市面上并无成体系介绍Jackson的教程(官网都木有),独此一家哦。当然喽,这必将损伤到我的CSDN专栏售卖权益(小钱也是钱嘛),所以希望你关注公众号,关注此专栏,然后学到手我就觉得值了 2020-05-30阿里云应急响应中心监测到Fastjson爆发新的反序列化远程代码执行漏洞,黑客利用漏洞,可绕过autoType限制,直接远程执行任意命令攻击服务器,风险极大(话外音:此bug必须Fix)。幸运的是,官方的响应速度非常快:还记得上一次Fastjson 高级别风险 安全漏洞是什么时候吗?是的,它就发生在2019-09-04,两次相距着实不远,不满你说我还记忆犹新呢,我司安全部门发的邮件还能找到。 当然,之前也有些漏洞问题,但关注度不如这两次。主要是这两次时间相近,危险级别非常高影响面很大,所以社区反应较为强烈 这两次“相邻”的安全漏洞着实把Fastjson推到了风口浪尖,吃瓜群众一波接一波,一时间 “弃用Fastjson,拥抱Jackson/Gson” 的声音不绝于耳。这很容易理解,因为谁都不情愿时不时收到公司安全部门的这种邮件:针对此漏洞,虽说咱们Fix起来步骤简单:升级Fastjson的版本,然后重启应用。看起来毫不费力,实则是个大坑。你是否曾想过这个问题:倘若有上百个、几百个Java应用呢?且不谈你操作上的时间和人力成本有多高,单单管理起来的工作量也不容小觑。所以如果你是技术Leader,胸中的怒火释放一下是在情理之中的。 相信很少有部门/团队把Spring Boot应用做成Jar包分离的形式的吧~因此大概率都需要经过升版本 -> 提交代码 -> 合代码 -> 上pre -> 上线 -> 验证等步骤,so还是比较麻烦的 你为何用Fastjson? 这个问题你可以问自己,也可以问身边的同事。汇总一下就是答案,这才是来自用户最真实的声音嘛。我针对此也简单“调查”过,把我听到的了解到的汇总为如下三点: API简单(static方法直接使用),上手快,对开发者友好 阿里巴巴出品,背靠大厂值得信赖. 社区相对活跃,维护升级有保障 容我猜猜,这3个理由大概率命中了你心中所想吧?有大厂做背书自然能给产品加分,但自身优秀才是硬道理。虽然原因有三点,但我认为让很多人决定去使用它、赞它的最最最主要原因其实就一点:API简单,static方法直接调用对开发者友好。 我感觉对于大多数Java Coder(特别对于初学者)来说,使用时会有这样的一种情节在里面:静态方法的逼格比实例方法高。而实际上不应该是这样子的,初学者(初/中级选手)热爱使用静态方法,而高手在设计一个库/框架时应在静态方法+实例方法间运用自如。一味地、过多地使用静态方法只会让你的的思维倾向于面向过程,而非更好的利用Java 面向对象 的特性,因此高下立判。 没有孰优孰劣,适合的才是最好的 发现了没,使用Fastjson的原因中,我们至始至终都没有提到性能高/速度快等字眼,但这却是Fastjson最最最为核心的特性,可谓是它能立足于众多JSON库中、“脱颖而出”的立身之本。岂不怪哉,我们使用它竟不是因为它最核心的特性有多好,那这是为何呢? 你为何仍在用Fastjson? 原因可以说出5678种,总而言之言而总之,你不(敢)切换的原因或许只有一个:Fastjson的静态方法调用用着省心;最重要的是,对其它JSON库(如Jackson/Gson)并不熟悉不敢切换。 我认为害怕来自于未知不可否认Jackson/Gson的使用门槛的确比Fastjson高那么一丢丢,但这绝不是你拒绝去使用它的理由。受Fastjson这“连续”两次高危漏洞影响,A哥更加坚定了把Jackson当作 唯一 JSON库的决心,甚至在团队内严令禁止使用Fastjson。大家统一了语言/工具,更能提高生产力~ 如果你也是因为不太了解Jackson而不敢离开温室,那么看到本文就很幸运了,本系列会免费带你拥抱Jackson这个高级JSON库,功能上比Fastjson强了不止一点点。 正文 坊间在某坛里看到这样一句言论:若你还依赖于使用Fastjson,那么你大概率还只是初/中级水平。这句话必然让Fastjson的忠实用户火冒三丈,抄起家伙嘎嘎就是干。话出必然有因,那么这句话是否真的言过于词呢?接下来就絮叨絮叨 我很愿意用存在即合理原则来表达一个观点:Fastjson出个bug就能有这么高的关注度,不可否认这本身就是一种成功。 误区描述:“合理”请不要误读为“合乎情理”之类,而是当做“理由”来讲。“存在即合理”正确理解为:一切存在的事物都有它存在的理由 任何技术能够流行起来,well-known被我们所熟知必然有它的优势,哪怕这个过人之处只有一个。下面我们来看看为何Fastjson能一步步被宠爱,它的魔力到底在哪? 技术选型不应该像相亲:肯定你只需要一个理由,而否定你却能... Why Fastjson? 虽然最近Fastjson由于出现安全漏洞,社区言论一边倒。即使如此,几乎没人直接否定过Fastjson本身的优秀,特别是当你知道这个使用广泛的库几乎全部来自于一人之手时。他就是匠人温少: 值得一提的是:温少另一个开源项目Druid是国内最流行的(甚至没有之一)数据库连接池产品,广受好评 成人只看利弊,小孩才分对错。为何要使用它能流行开来,那必然是因为它优秀。它优秀品质在其官网可一览无遗:这些“优点”用中文描述出来更加直(震)观(撼): 1、速度快 fastjson相对其他JSON库的特点是快,从2011年fastjson发布1.1.x版本之后,其性能从未被其他Java实现的JSON库超越。 话外音:速度/性能这一块,Fastjson一直拿捏得死死的 2、使用广泛 fastjson在阿里巴巴大规模使用,在数万台服务器上部署,fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。 话外音:阿里巴巴数以万计的大规模集群实例做规模背书,说服力杠杠的 3、测试完备 fastjson有非常多的testcase,在1.2.11版本中,testcase超过3321个。每次发布都会进行回归测试,保证质量稳定。 话外音:单测覆盖率高,代码健壮性有保证 4、使用简单 fastjson的API十分简洁。 String text = JSON.toJSONString(obj); //序列化 VO vo = JSON.parseObject("{...}", VO.class); //反序列化 话外音:不管你是小白还是小小白,轻松上手,使用起来都无障碍 5、功能完备 支持泛型,支持流处理超大文本,支持枚举,支持序列化和反序列化扩展。 话外音:我这一家就够了,你要的,这都有 Why Not Fastjson? 文首有表态本文我是有态度和有偏向的,因此不来几点原因实则不妥。那么我就针对于官网列出的5点(见上),给出个人观点供以参考。是否言过于辞,咱们拿出另外一个JSON库做出对比,本文以Jackson为例。 版本约定 因为要做比较嘛,所以对使用的JSON库做出版本约定: Jackson:2.10.1 演示代码均使用最常用的高层API,而非底层API。毕竟用底层API去PK Fastjson并不公平,毕竟那并不常用 Fastjson:1.2.72 only one jar 1、速度上并没有那么的快 速度快/性能高是Fastjson 最最最最最最 大的“卖点”,可谓是立身之本,从它的命名以及它的logo设计上你都能感受到这一点。 没有调研就没有发言权,本文针对于最常用的使用场景来一波测试对比(对比尽量公正,切勿钻牛角尖)。关于Fastjson和Jackson在性能PK这一块,网上的案例有不少,我自己也书写了多个场景的比较代码。但最终我还是决定引用Robin的结果展示给大家,我看了他的测试方案(代码)更加专业些:几种常用 JSON 库性能比较,结论如下两张图 总的结论:除了Json-lib是来搞笑的(它早已停止更新,切勿在生产上使用),Fastjson、Jsckson、Gson三者不分伯仲,差异性较小。 综合各种测试case,网上的 + 我自己写的测试用例,三者在性能方面除了Gson稍微差点外,Jackson和Fastjson在速度上可认为是差不多的(甚至Jackson综合性能表现更好) 既然差异性这么小,Fastjson一味的强调它是最快的真的有意义吗? JSON的解析速度绝不会制约系统的性能 比如我们一次REST调用环节全流程可能100ms;其中操作一次数据库,可能需要几十ms;序列化反序列化一次json 一般只需要几ms;也就是说不同的json库,性能相差都在毫秒间;在一次REST调用全流程里,不同的JSON库在性能表现上影响甚微。 在现代应用程序中,即使最慢的Gson,也是满足需求的;解析文档速度的快慢,并不能作为选型的唯一标准,可能连主要标准都算不上。对IO优化、网络优化、并行处理等优化措施,远比选用一个更快的库更有效。 言而总之,如果你选择一个JSON库把性能当作了一个标准,就犯了方向性的错误。 2、并没有那么的流行 使用广不广泛、流行度有多高这玩意是相对的。有一个最直观的数据,那就是在Maven中的引用量,我截图如下:从usages数值上看,似乎不在一个量级上。当然这么比较我个人认为不算特别客观,主要原因有二: 开源的技术发展的越早,使用者越多,主流框架越支持(比如Spring MVC内置Jackson支持),就会形成聚集效应,赢者通吃 Fastjson起步较晚,且主要发力于国内 不可否认Fastjson在国内的流行度是非常高的,甚至超过Jackson。否则最近一次的安全漏洞也不会有那么多人吃瓜嘛,但是这种“使用广泛”你也得辩证性的去看,毕竟在中国Java领域里,阿里巴巴是绝对的执牛耳者。 Fastjson的流行,是有着内在的原因的,比如这个无奈: 3、测试真的完备吗? 额,这个我只能说:Fastjson自己知道就成,并无必要当作亮点show出来,毕竟使用者只关心出bug、出漏洞的频率和严重性,并不关心工程内部是如何保证健壮性的。 在使用者眼中:不出bug,一行单测没有都没关系。出了严重bug,有上万个test case也难以让人信服。 4、API真的简单吗? 答:真的。文上有解释,这也许可能大概是你选择使用Jackson作为JSON库最重要的理由。 当然,API使用简单针对于simple场景,对于复杂场景它也并不能简单应对。道理很简单,POP is simple,OOP is Complex。但恰好的是,在互联网应用场景中使用JSON库,大多属于简单场景,因此Fastjson把它当做一个亮点我觉得是无可厚非的。 5、功能并没有那么完备 官网强调了它是支持泛型、枚举等类型的序列化、反序列化的。但是对着JavaBean + JSON规范来讲,Fastjson有不少功能缺失(没遵循规范),这是A哥最为忍受不了的地方,因为它已经不能实现我的功能了。如果你基于它做过中间件开发、框架开发或者是DDD驱动设计开发,相信你也深有体会: 总结 真理是相对的,没有绝对的真理。真理是让人明白道理的,不是用来诡辩的,更不是用来抬扛的。 同样的,Fastjson还是Jackson也就没有标准的答案,各位还是结合自己的具体情况,见仁见智。本文只是阐述我的个人观点,表达了我的使用倾向,供以你决策时参考。 如果你和我一样,也想把Jackson作为你的唯一 JSON库,那就关注我吧,接下来我会把付费专栏里的内容全部搬过来给你,免费助你平滑过渡到这个世界上最好的JSON库。
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位小伙伴大家好,我是A哥。最近遇到两个问题,都是关于IDEA的(言外之意和代码无关),很是让我“生气”呀(关键是浪费时间)。在痛定思痛后,我决定写此专栏,来专门分享/记录使用IntelliJ IDEA过程中遇到的那些奇葩问题和解决方案,以帮助你缩短日常排错时间,这么一思考好像还功德无量呢。 IntelliJ IDEA作为Java开发者中最为流行的开发工具(eclipse粉勿喷),熟练掌握它(包括排雷)对提升编码效率能有显著提升。但工具毕竟是工具,这么长时间使用IDEA以来,每个人或多或少的都遇到过关于IDEA七七八八、奇奇怪怪的问题,这些与代码舞棍,但它很容易偷走你的时间,半天又更或者是一天之久。 说明:千万不要忽视对IDEA的研究,因为把它玩熟练了它就相当于你的物理外挂 本专栏内容并非 IDEA教程,而是着眼于分享IDEA使用过程中,那些我遇到(或者大家遇到)的但又不是能够很快速解决,总之就是比较棘手的问题的汇总,有一种错题本的意思有木有。总之就是希望它能够帮助到大家迅速定位or解决问题,避免不必要的时间浪费,毕竟咱们的主业还是敲代码嘛~ 版本约定 本文内容若没做特殊说明,均基于以下版本: IntelliJ IDEA:2020.1.2旗舰版 正文 使用IDEA这么久,虽然之前时不时地的跟IDEA问题“交过手”,但真正促使我决定写此专栏的原因还是源自于前两天使用IDEA启动Spring Boot程序时的这个报错: Error running 'Application': Command line is too long. Shorten command line for Application or also for Spring Boot default configuration. 说实话这个错误我前所未见,看起来还蛮有意思,因此决定研究一番。这不,把研究结果分享给大家,信息共享。 为了解释好这个问题,我们得先来做些功课,知晓写概念。 控制台首行路径 在IDEA里,你每次启动一个main函数时,控制台第一行输出的“日志”称作为:控制台首行路径。这里,我运行一个最最最简单的程序,看看它长啥样,程序如下: public class Application { public static void main(String[] args) { System.out.println("Hello world"); } } 运行程序,控制台输出如下截图:相信小伙伴每天都能看见它但大概率不会注意到它,我也不例外。你想不到的是,恰巧这行“日志”就成为了本文今天的主角,会围绕它来展阐述。 特别说明:如果你是用外置tomcat驱动应用启动的话效果不是这样子的。因为它使用的是tomcat的脚本来启动,所以首行日志形如这样:D:\developer\apache-tomcat-9.0.34\bin\catalina.bat run 首行路径内容 知道了什么叫首行路径,那么它的内容才是我们要关心的。如上截图中,细心的你会发现最后是...省略号,因此内容绝不止你现在看到的那么简单。你可以鼠标点击一下,展开全部内容,截图如下:这一行实在太长了,无法横向截图全部展示出来,因此我把它复制出来放在文本编辑器中查看:这个截图是一行哦(只是我在文本编辑器了自动折行了而已),仍旧不能看到全部内容,因为字数真的太多了,总字数统计如下:仅仅一行,字数超过26000个。咋舌吧:第一行控制台“日志”竟然输出了超过2.6w个字符。从内容结构上来看,这是一个command命令:调用java.exe程序启动一个java进程的命令。 为何启动抛错Command line is too long 99.99%的情况下,你可以在IDEA里正常启动你的应用,即使首行路径很长很长。但是直到当我启动我的这个Spring Boot应用时,弹出红色提示:直接禁止了我的running运行。提示内容中文释义为:运行“Application”时出错:命令行太长。缩短应用程序或Spring Boot默认配置的命令行。我相信如果你也是第一次见到此case,表情和我一样是这样的:main方法都启不动了,那还得了。遇到这种情况,我只能使用百度大法了:一看能搜出这么多结果,我也就不慌了,按照“教程”很容易的把问题解决了。另外呢,通过此次搜索到的结果聊两句题外话: 虽然Result Count不少,但是我发现实质上内容几乎一毛一样,真乃天下文章一大抄 访问量并不代表文章质量高,只是它刚好命中了关键字而已,比如标题党 我得出如此感悟,也是促使我写本文的原因之一。因为A哥的文章一贯如此,是有些B格的。接下来以点带面,把这部分内容帮大家展开展开,解决问题并非最终目的,而是为了:记得牢,能装x,一切为了加薪。 原因分析 出现此问题的直接原因是:IDEA集成开发环境运行你的“源码”的时候(注意是源码基础上运行,并非打好的jar包哦),是通过命令(首行那个非常非常长的)来启动Java进程的。这个命令主要包含两大部分: vm/程序参数。也就是你看到的那些-XX -D等参数,这部分理论上可以无限长但实际上一般不会太长 -classpath参数,它用于指定运行时jar包路径(因为jar包理论上是可以在任何地方的),这部分可能性就多了 关键就在于-classpath参数,它可以非常长,你依赖的jar包越多此路径就越长;你的base基路径越长它就越长;倘若你还要做复杂的Junit单元测试,那加入的jar包就更多长度可能就越长喽。总的来说:此part是很有可能超长从而导致Command line is too long现象的。 如果类路径太长(可能性大),或者您有许多VM参数(可能性小),则无法启动该程序。原因是大多数操作系统都有命令行长度限制。在这种情况下,IntelliJ IDEA将提供尝试缩短类路径的能力。 IDEA老版本方案 针对此问题,在之前版本(确切的说是2017.3之前的版本),需要通过XML文件配置来解决:找到工程下的.idea/workspace.xml这个文件,添加如下项: <component name="PropertiesComponent"> ... <!-- 这句是你需要添加的项 --> <property name="dynamic.classpath" value="true" /> ... </component> 再次启动程序发现问题解决。我有理由相信,在这个时间节点上应该没有人用这么古老的版本了吧,但你在网上搜的文章大多数都还是这种解决方案,因此请务必注意甄别哦(2017.3以后的版本请参照下面方案解决)。 所以我不是说了麽,任何不指定版本的解决方案、源码分析文章都是不太负责任的。作为一个程序员,应该适当提高自己的版本意识 IDEA新版本方案:命令行缩短器 在IDEA的2017.3版本中提供了一项新特性:命令行缩短器。旨在用来解决此类问题,也就是说从此版本开始,不再需要通过XML文件来编辑IDE的设置那么麻烦了,而是直接在界面操作即可:最初,IntelliJ IDEA尝试将长类路径写入文本文件(这意味着应用程序是中间类加载器)。但是不幸的是,这不适用于某些框架,例如JMock。然后,IntelliJ IDEA尝试使用或多或少的标准方法,即将长类路径打包到classpath.jar中。不幸的是,对于其他一些框架,这也不起作用。 总结:这两种方案都不是100%完美的,具体情况具体分析 从上对话框中可以看到IDEA一共提供了三种命令行缩短器供你选择: none。这是默认选项。IDE不会缩短长类路径。如果命令行超出操作系统限制,则IDEA将无法运行您的应用程序 jar manifest。IDE通过临时classpath.jar传递长类路径。原始类路径在MANIFEST.MF中定义为classpath.jar中的类路径属性 classpath file。IDE将把长类路径写入文本文件 jar manifest方式 选择此种方式,运行测试程序,首行全部内容展示如下: D:\developer\jdks\1.8.0_241\bin\java.exe -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=5975:C:\Program Files\JetBrains\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\xxx\AppData\Local\Temp\classpath1199511058.jar com.xxx.Application 区别主要在于-classpath这一行,它不再是把所有jar的路径展示出来,而是“封装”到了一个jar文件里,这一下子让命令长度大幅减少,能够100%保证不会超长了,所以启动也就不会报错喽。 另外,在IDEA里你直接单击此jar路径是可以预览器内容的(真贴心):当然,你也可以在你磁盘里找到此jar文件,然后查看其内容(说明:请确保hold住线程了再去找对应文件,否则临时文件是线程结束后就删除了的):特别强调:我在实践过程中,使用此种方式出现过jar包没有被加载进来的情况,在此提醒各位,若你也有类似现象发生,请切换成使用classpath file方式吧。 毕竟官方也说了:这两种路径缩短方式,对某些框架可能存在不兼容情况,just可能而已哦~ classpath file方式 选择此种方式,运行测试程序,首行全部内容展示如下: D:\developer\jdks\1.8.0_241\bin\java.exe -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=5975:C:\Program Files\JetBrains\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\xxx\AppData\Local\Temp\idea_classpath921151059 com.xxx.Application 有了上面的描述,这个就不用A哥赘述了。 扩展知识:windows系统命令最大长度 这属于扩展知识,延伸阅读内容。 既然已经知道出现此问题的原因是命令超长了而“报错”,A哥就想那windows命令最长允许多少字符呢?带着这个问题,我开始了一番苦心寻找,最后终于在windows官网找到了我想要的答案。地址在这:https://docs.microsoft.com/zh-cn/windows/win32/api/processenv/nf-processenv-setenvironmentvariablea?redirectedfrom=MSDN 在Windows上,命令行长度最大为32767个字符(和shell长度、命令提示符长度的区别)。当提供足够大的类路径时,将违反此限制,并且Windows拒绝执行该命令并抛出错误代码87。推荐的解决方案有如下两种: 将所有jar复制到一个公共文件夹,例如c:\jars,然后将其包括在内。这样,每个jar都有一个短路径,即c:\jars(而不是长路径c:\program files\app\lib\app-jar1.jar),并且应该可以将这个路径们控制在38kb之内 如果步骤1不起作用,则可以将单个jar提取到一个文件夹中,并创建一个包含所有提取文件的新jar。这样就只需要引入这个新jar就可以了 这是两种解决问题的思想:短路径方式(简单高效)和打包方式(100%能解决问题) 别问A哥为毛只给出windows的最大长度,没有Mac的吗?我只能说,我很穷所以用的是windows本,Mac的我不关心 思考题 今日份思考题比较简单 为毛你的Spring Boot应用在生产环境下从来不用担心出现Command line is too long这种错误? 有哪些有效的方式可以避免你的开发环境出现此问题? 总结 IDEA踩坑系列第一篇到这就结束了,算不算精彩呢?我个人觉得还可以。此专栏后续将不定期的更新,除了我自己准备外,同时也非常欢迎各位小伙伴能把平时遇到的IDEA遇到的棘手问题反馈给我(最好有解决方案哦),咱们一起把这个事做好,也算造福于大家嘛,毕竟我一个人碰见的case实则有限,有建议的可以下方扫码加我好友私聊我。 关注A哥 Author A哥(YourBatman) 个人站点 www.yourbatman.cn E-mail yourbatman@qq.com 微 信 fsx641385712 活跃平台 公众号 BAT的乌托邦(ID:BAT-utopia) 知识星球 BAT的乌托邦 每日文章推荐 每日文章推荐
前言 各位小伙伴大家好,我是A哥。通过本专栏前两篇的学习,相信你对static关键字在Spring/Spring Boot里的应用有了全新的认识,能够解释工作中遇到的大多数问题/疑问了。本文继续来聊聊static关键字更为常见的一种case:使用@Autowired依赖注入静态成员(属性)。 在Java中,针对static静态成员,我们有一些最基本的常识:静态变量(成员)它是属于类的,而非属于实例对象的属性;同样的静态方法也是属于类的,普通方法(实例方法)才属于对象。而Spring容器管理的都是实例对象,包括它的@Autowired依赖注入的均是容器内的对象实例,所以对于static成员是不能直接使用@Autowired注入的。 这很容易理解:类成员的初始化较早,并不需要依赖实例的创建,所以这个时候Spring容器可能都还没“出生”,谈何依赖注入呢? 这个示例,你或许似曾相识: @Component public class SonHolder { @Autowired private static Son son; public static Son getSon() { return son; } } 然后“正常使用”这个组件: @Autowired private SonHolder sonHolder; @Transaction public void method1(){ ... sonHolder.getSon().toString(); } 运行程序,结果抛错: Exception in thread "main" java.lang.NullPointerException ... 很明显,getSon()得到的是一个null,所以给你扔了个NPE。 版本约定 本文内容若没做特殊说明,均基于以下版本: JDK:1.8 Spring Framework:5.2.2.RELEASE 正文 说起@Autowired注解的作用,没有人不熟悉,自动装配嘛。根据此注解的定义,它似乎能使用在很多地方: @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { boolean required() default true; } 本文我们重点关注它使用在FIELD成员属性上的case,标注在static静态属性上是本文讨论的中心。 说明:虽然Spring官方现在并不推荐字段/属性注入的方式,但它的便捷性仍无可取代,因此在做业务开发时它仍旧是主流的使用方式 场景描述 假如有这样一个场景需求:创建一个教室(Room),需要传入一批学生和一个老师,此时我需要对这些用户按照规则(如名字中含有test字样的示为测试帐号)进行数据合法性校验和过滤,然后才能正常走创建逻辑。此case还有以下特点: 用户名字/详细信息,需要远程调用(如FeignClient方式)从UC中心获取 因此很需要做桥接,提供防腐层 该过滤规则功能性很强,工程内很多地方都有用到 有点工具的意思有木有 阅读完“题目”感觉还是蛮简单的,很normal的一个业务需求case嘛,下面我来模拟一下它的实现。 从UC用户中心获取用户数据(使用本地数据模拟远程访问): /** * 模拟去远端用户中心,根据ids批量获取用户数据 * * @author yourbatman * @date 2020/6/5 7:16 */ @Component public class UCClient { /** * 模拟远程调用的结果返回(有正常的,也有测试数据) */ public List<User> getByIds(List<Long> userIds) { return userIds.stream().map(uId -> { User user = new User(); user.setId(uId); user.setName("YourBatman"); if (uId % 2 == 0) { user.setName(user.getName() + "_test"); } return user; }).collect(Collectors.toList()); } } 说明:实际情况这里可能只是一个@FeignClient接口而已,本例就使用它mock喽 因为过滤测试用户的功能过于通用,并且规则也需要收口,须对它进行封装,因此有了我们的内部帮助类UserHelper: /** * 工具方法:根据用户ids,按照一定的规则过滤掉测试用户后返回结果 * * @author yourbatman * @date 2020/6/5 7:43 */ @Component public class UserHelper { @Autowired UCClient ucClient; public List<User> getAndFilterTest(List<Long> userIds) { List<User> users = ucClient.getByIds(userIds); return users.stream().filter(u -> { Long id = u.getId(); String name = u.getName(); if (name.contains("test")) { System.out.printf("id=%s name=%s是测试用户,已过滤\n", id, name); return false; } return true; }).collect(Collectors.toList()); } } 很明显,它内部需依赖于UCClient这个远程调用的结果。封装好后,我们的业务Service层任何组件就可以尽情的“享用”该工具啦,形如这样: /** * 业务服务:教室服务 * * @author yourbatman * @date 2020/6/5 7:29 */ @Service public class RoomService { @Autowired UserHelper userHelper; public void create(List<Long> studentIds, Long teacherId) { // 因为学生和老师统称为user 所以可以放在一起校验 List<Long> userIds = new ArrayList<>(studentIds); userIds.add(teacherId); List<User> users = userHelper.getAndFilterTest(userIds); // ... 排除掉测试数据后,执行创建逻辑 System.out.println("教室创建成功"); } } 书写个测试程序来模拟Service业务调用: @ComponentScan public class DemoTest { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(DemoTest.class); // 模拟接口调用/单元测试 RoomService roomService = context.getBean(RoomService.class); roomService.create(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L), 101L); } } 运行程序,结果输出: id=2 name=YourBatman_test是测试用户,已过滤 id=4 name=YourBatman_test是测试用户,已过滤 id=6 name=YourBatman_test是测试用户,已过滤 教室创建成功 一切都这么美好,相安无事的,那为何还会有本文指出的问题存在呢?正所谓“不作死不会死”,总有那么一些“追求极致”的选手就喜欢玩花,下面姑且让我猜猜你为何想要依赖注入static成员属性呢? 帮你猜猜你为何有如此需求? 从上面示例类的命名中,我或许能猜出你的用意。UserHelper它被命名为一个工具类,而一般我们对工具类的理解是: 方法均为static工具方法 使用越便捷越好 很明显,static方法使用是最便捷的嘛 现状是:使用UserHelper去处理用户信息还得先@Autowired注入它的实例,实属不便。因此你想方设法的想把getAndFilterTest()这个方法变为静态方法,这样通过类名便可直接调用而并不再依赖于注入UserHelper实例了,so你想当然的这么“优化”: @Component public class UserHelper { @Autowired static UCClient ucClient; public static List<User> getAndFilterTest(List<Long> userIds) { ... // 处理逻辑完全同上 } } 属性和方法都添加上static修饰,这样使用方通过类名便可直接访问(无需注入): @Service public class RoomService { public void create(List<Long> studentIds, Long teacherId) { ... // 通过类名直接调用其静态方法 List<User> users = UserHelper.getAndFilterTest(userIds); ... } } 运行程序,结果输出: 07:22:49.359 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient 07:22:49.359 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient ... Exception in thread "main" java.lang.NullPointerException at cn.yourbatman.temp.component.UserHelper.getAndFilterTest(UserHelper.java:23) at cn.yourbatman.temp.component.RoomService.create(RoomService.java:26) at cn.yourbatman.temp.DemoTest.main(DemoTest.java:19) 以为天衣无缝,可结果并不完美,抛异常了。我特意多粘贴了两句info日志,它们告诉了你为何抛出NPE异常的原因:@Autowired不支持标注在static字段/属性上。 为什么@Autowired不能注入static成员属性 静态变量是属于类本身的信息,当类加载器加载静态变量时,Spring的上下文环境还没有被加载,所以不可能为静态变量绑定值(这只是最表象原因,并不准确)。同时,Spring也不鼓励为静态变量注入值(言外之意:并不是不能注入),因为它认为这会增加了耦合度,对测试不友好。 这些都是表象,那么实际上Spring是如何“操作”的呢?我们沿着AutowiredAnnotationBeanPostProcessor输出的这句info日志,倒着找原因,这句日志的输出在这: AutowiredAnnotationBeanPostProcessor: // 构建@Autowired注入元数据方法 // 简单的说就是找到该Class类下有哪些是需要做依赖注入的 private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) { ... // 循环递归,因为父类的也要管上 do { // 遍历所有的字段(包括静态字段) ReflectionUtils.doWithLocalFields(targetClass, field -> { if (Modifier.isStatic(field.getModifiers())) { logger.info("Autowired annotation is not supported on static fields: " + field); } return; ... }); // 遍历所有的方法(包括静态方法) ReflectionUtils.doWithLocalMethods(targetClass, method -> { if (Modifier.isStatic(method.getModifiers())) { logger.info("Autowired annotation is not supported on static methods: " + method); } return; ... }); ... targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); ... } 这几句代码道出了Spring为何不给static静态字段/静态方法执行@Autowired注入的最真实原因:扫描Class类需要注入的元数据的时候,直接选择忽略掉了static成员(包括属性和方法)。 那么这个处理的入口在哪儿呢?是否在这个阶段时Spring真的无法给static成员完成赋值而选择忽略掉它呢,我们继续最终此方法的调用处。此方法唯一调用处是findAutowiringMetadata()方法,而它被调用的地方有三个: 调用处一:执行时机较早,在MergedBeanDefinitionPostProcessor处理bd合并期间就会解析出需要注入的元数据,然后做check。它会作用于每个bd身上,所以上例中的2句info日志第一句就是从这输出的 AutowiredAnnotationBeanPostProcessor: @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition); } 调用处二:在InstantiationAwareBeanPostProcessor也就是实例创建好后,给属性赋值阶段(也就是populateBean()阶段)执行。所以它也是会作用于每个bd的,上例中2句info日志的第二句就是从这输出的 AutowiredAnnotationBeanPostProcessor: @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { metadata.inject(bean, beanName, pvs); } ... return pvs; } 调用处三:这个方法比较特殊,它表示对于带有任意目标实例(已经不仅是Class,而是实例本身)直接调用的“本地”处理方法实行注入。这是Spring提供给“外部”使用/注入的一个public公共方法,比如给容器外的实例注入属性,还是比较实用的,本文下面会介绍它的使用办法 说明:此方法Spring自己并不会主动调用,所以不会自动输出日志(这也是为何调用处有3处,但日志只有2条的原因) AutowiredAnnotationBeanPostProcessor: public void processInjection(Object bean) throws BeanCreationException { Class<?> clazz = bean.getClass(); InjectionMetadata metadata = findAutowiringMetadata(clazz.getName(), clazz, null); try { metadata.inject(bean, null, null); } ... } 通过这部分源码,从底层诠释了Spring为何不让你@Autowired注入static成员的原因。既然这样,难道就没有办法满足我的“诉求”了吗?答案是有的,接着往下看。 间接实现static成员注入的N种方式 虽然Spring会忽略掉你直接使用@Autowired + static成员注入,但还是有很多方法来绕过这些限制,实现对静态变量注入值。下面A哥介绍2种方式,供以参考: 方式一:以set方法作为跳板,在里面实现对static静态成员的赋值 @Component public class UserHelper { static UCClient ucClient; @Autowired public void setUcClient(UCClient ucClient) { UserHelper.ucClient = ucClient; } } 方式二:使用@PostConstruct注解,在里面为static静态成员赋值 @Component public class UserHelper { static UCClient ucClient; @Autowired ApplicationContext applicationContext; @PostConstruct public void init() { UserHelper.ucClient = applicationContext.getBean(UCClient.class); } } 虽然称作是2种方式,但其实我认为思想只是一个:延迟为static成员属性赋值。因此,基于此思想确切的说会有N种实现方案(只需要保证你在使用它之前给其赋值上即可),各位可自行思考,A哥就没必要一一举例了。 高级实现方式 作为福利,A哥在这里提供一种更为高(zhuang)级(bi)的实现方式供以你学习和参考: @Component public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { @Autowired private AutowireCapableBeanFactory beanFactory; /** * 当所有的单例Bena初始化完成后,对static静态成员进行赋值 */ @Override public void afterSingletonsInstantiated() { // 因为是给static静态属性赋值,因此这里new一个实例做注入是可行的 beanFactory.autowireBean(new UserHelper()); } } UserHelper类不再需要标注@Component注解,也就是说它不再需要被Spirng容器管理(static工具类确实不需要交给容器管理嘛,毕竟我们不需要用到它的实例),这从某种程度上也是节约开销的表现。 public class UserHelper { @Autowired static UCClient ucClient; ... } 运行程序,结果输出: 08:50:15.765 [main] INFO org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowired annotation is not supported on static fields: static cn.yourbatman.temp.component.UCClient cn.yourbatman.temp.component.UserHelper.ucClient Exception in thread "main" java.lang.NullPointerException at cn.yourbatman.temp.component.UserHelper.getAndFilterTest(UserHelper.java:26) at cn.yourbatman.temp.component.RoomService.create(RoomService.java:26) at cn.yourbatman.temp.DemoTest.main(DemoTest.java:19) 报错。当然喽,这是我故意的,虽然抛异常了,但是看到我们的进步了没:info日志只打印一句了(自行想想啥原因哈)。不卖关子了,正确的姿势还得这么写: public class UserHelper { static UCClient ucClient; @Autowired public void setUcClient(UCClient ucClient) { UserHelper.ucClient = ucClient; } } 再次运行程序,一切正常(info日志也不会输出喽)。这么处理的好处我觉得有如下三点: 手动管理这种case的依赖注入,更可控。而非交给Spring容器去自动处理 工具类本身并不需要加入到Spring容器内,这对于有大量这种case的话,是可以节约开销的 略显高级,装x神器(可别小看装x,这是个中意词,你的加薪往往来来自于装x成功) 当然,你也可以这么玩: @Component public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { @Autowired private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor; @Override public void afterSingletonsInstantiated() { autowiredAnnotationBeanPostProcessor.processInjection(new UserHelper()); } } 依旧可以正常work。这不正是上面介绍的调用处三麽,马上就学以致用了有木有,开心吧。 使用建议 有这种使用需求的小伙伴需要明晰什么才叫真正的util工具类?若你的工具类存在外部依赖,依赖于Spring容器内的实例,那么它就称不上是工具类,就请不要把它当做static来用,容易玩坏的。你现在能够这么用恰好是得益于Spring管理的实例默认都是单例,所以你赋值一次即可,倘若某天真变成多例了呢(即使可能性极小)? 强行这么撸,是有隐患的。同时也打破了优先级关系、生命周期关系,容易让“初学者”感到迷糊。当然若你坚持这么使用也未尝不可,那么请做好相关规范/归约,比如使用上面我推荐的高(zhuang)级(bi)使用方式是一种较好的选择,这个时候手动管理往往比自动来得更安全,降低后期可能的维护成本。 思考题 在解析类的@Autowired注入元数据的时候,Spring工厂/容器明明已经准备好了,理论上已经完全具备帮你完成注入/赋值的能力,既然这样,为何Spring还偏要“拒绝”这么干呢?可直接注入static成员不香吗? 既然@Autowired不能注入static属性,那么static方法呢?@Value注解呢? 总结 本文介绍了Spring依赖注入和static的关系,从使用背景到原因分析都做了相应的阐述,A哥觉得还是蛮香的,对你帮助应该不小吧。 最后,我想对小伙伴说:依赖注入的主要目的,是让容器去产生一个对象的实例然后管理它的生命周期,然后在生命周期中使用他们,这会让单元测试工作更加容易(什么?不写单元测试,那你应该关注我喽,下下下个专栏会专门讲单元测试)。而如果你使用静态变量/类变量就扩大了使用范围,使得不可控了。这种static field是隐含共享的,并且是一种global全局状态,Spring并不推荐你去这么做,因此使用起来务必当心~
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位小伙伴大家好,我是A哥。上篇文章了解了static关键字 + @Bean方法的使用,知晓了它能够提升Bean的优先级,在@Bean方法前标注static关键字,特定情况下可以避免一些烦人的“警告”日志的输出,排除隐患让工程变得更加安全。我们知道static关键字它不仅可使用在方法上,那么本文将继续挖掘static在Spring环境下的用处。 根据所学的JavaSE基础,static关键字除了能够修饰方法外,还能使用在这两个地方: 修饰类。确切的说,应该叫修饰内部类,所以它叫静态内部类 修饰成员变量 其实static还可以修饰代码块、static静态导包等,但很明显,这些与本文无关 接下来就以这为两条主线,分别研究static在对应场景下的作用,本文将聚焦在静态内部类上。 版本约定 本文内容若没做特殊说明,均基于以下版本: JDK:1.8 Spring Framework:5.2.2.RELEASE 正文 说到Java里的static关键字,这当属最基础的入门知识,是Java中常用的关键字之一。你平时用它来修饰变量和方法了,但是对它的了解,即使放在JavaSE情景下知道这些还是不够的,问题虽小但这往往反映了你对Java基础的了解程度。 当然喽,本文并不讨论它在JavaSE下使用,毕竟咱们还是有一定逼格的专栏,需要进阶一把,玩玩它在Spring环境下到底能够迸出怎么样的火花呢?比如静态内部类~ Spring下的静态内部类 static修饰类只有一种情况:那就是这个类属于内部类,这就是我们津津乐道的静态内部类,形如这样: public class Outer { private String name; private static Integer age; // 静态内部类 private static class Inner { private String innerName; private static Integer innerAge; public void fun1() { // 无法访问外部类的成员变量 //System.out.println(name); System.out.println(age); System.out.println(innerName); System.out.println(innerAge); } } public static void main(String[] args) { // 静态内部类的实例化并不需要依赖于外部类的实例 Inner inner = new Inner(); } } 在实际开发中,静态内部类的使用场景是非常之多的。 认识静态/普通内部类 由于一些小伙伴对普通内部类 vs 静态内部类傻傻分不清,为了方便后续讲解,本处把关键要素做简要对比说明: 静态内部类可以声明静态or实例成员(属性和方法);而普通内部类则不可以声明静态成员(属性和方法) 静态内部类实例的创建不依赖于外部类;而普通外部类实例创建必须先有外部类实例才行(绑定关系拿捏得死死的,不信你问郑凯) 静态内部类不能访问外部类的实例成员;而普通内部类可以随意访问(不管静态or非静态) --> 我理解这是普通内部类能 “存活” 下来的最大理由了吧 总之,普通内部类和外部类的关系属于强绑定,而静态内部类几乎不会受到外部类的限制,可以游离单独使用。既然如此,那为何还需要static静态内部类呢,直接单独写个Class类岂不就好了吗?存在即合理,这么使用的原因我个人觉得有如下两方面思考,供以你参考: 静态内部类是弱关系并不是没关系,比如它还是可以访问外部类的static的变量的不是(即便它是private的) 高内聚的体现 在传统Spirng Framework的配置类场景下,你可能鲜有接触到static关键字使用在类上的场景,但这在Spring Boot下使用非常频繁,比如属性配置类的典型应用: @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { // server.port = xxx // server.address = xxx private Integer port; private InetAddress address; ... // tomcat配置 public static class Tomcat { // server.tomcat.protocol-header = xxx private String protocolHeader; ... // tomcat内的log配置 public static class Accesslog { // server.tomcat.accesslog.enabled = xxx private boolean enabled = false; ... } } } 这种嵌套case使得代码(配置)的key 内聚性非常强,使用起来更加方便。试想一下,如果你不使用静态内部类去集中管理这些配置,每个配置都单独书写的话,像这样: @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { } @ConfigurationProperties(prefix = "server.tomcat", ignoreUnknownFields = true) public class TomcatProperties { } @ConfigurationProperties(prefix = "server.tomcat.accesslog", ignoreUnknownFields = true) public class AccesslogProperties { } 这代码,就问你,如果是你同事写的,你骂不骂吧!用臃肿来形容还是个中意词,层次结构体现得也非常的不直观嘛。因此,对于这种属性类里使用静态内部类是非常适合,内聚性一下子高很多~ 除了在内聚性上的作用,在Spring Boot中的@Configuration配置类下(特别常见于自动配置类)也能经常看到它的身影: @Configuration(proxyBeanMethods = false) public class WebMvcAutoConfiguration { // web MVC个性化定制配置 @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { ... } @Configuration(proxyBeanMethods = false) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { ... } } 利用静态内部类把相似配置类归并在一个 .java文件 内,这样多个static类还可公用外部类的属性、方法,也是一种高内聚的体现。同时static关键字提升了初始化的优先级,比如本例中的EnableWebMvcConfiguration它会优先于外部类加载~ 关于static静态内部类优先级相关是重点,静态内部类的优先级会更高吗?使用普通内部能达到同样效果吗?拍脑袋直接回答是没用的,带着这两个问题,接下来A哥举例领你一探究竟... static静态配置类提升配置优先级 自己先构造一个Demo,场景如下: @Configuration class OuterConfig { OuterConfig() { System.out.println("OuterConfig init..."); } @Bean static Parent parent() { return new Parent(); } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("InnerConfig init..."); } @Bean Daughter daughter() { return new Daughter(); } } } 测试程序: @ComponentScan public class TestSpring { public static void main(String[] args) { new AnnotationConfigApplicationContext(TestSpring.class); } } 启动程序,结果输出: InnerConfig init... OuterConfig init... Daughter init... Parent init... 结果细节:似乎都是按照字母表的顺序来执行的。I在前O在后;D在前P在后; 看到这个结果,如果你就过早的得出结论:静态内部类优先级高于外部类,那么就太随意了,图样图森破啊。大胆猜想,小心求证 应该是程序员应有的态度,那么继续往下看,在此基础上我新增加一个静态内部类: @Configuration class OuterConfig { OuterConfig() { System.out.println("OuterConfig init..."); } @Bean static Parent parent() { return new Parent(); } @Configuration private static class PInnerConfig { PInnerConfig() { System.out.println("PInnerConfig init..."); } @Bean Son son() { return new Son(); } } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("InnerConfig init..."); } @Bean Daughter daughter() { return new Daughter(); } } } 我先解释下我这么做的意图: 增加一个字母P开头的内部类,自然顺序P在O(外部类)后面,消除影响 P开头的内部类在源码摆放顺序上故意放在了I开头的内部类的上面,同样为了消除字母表顺序带来的影响 目的:看看是按照字节码顺序,还是字母表顺序呢? PInnerConfig里面的@Bean实例为Son,字母表顺序是三者中最为靠后的,但字节码却在中间,这样也能够消除影响 运行程序,结果输出: InnerConfig init... PInnerConfig init... OuterConfig init... Daughter init... son init... Parent init... 结果细节:外部类貌似总是滞后于内部类初始化;同一类的多个内部类之间顺序是按照字母表顺序(自然排序)初始化而非字节码顺序;@Bean方法的顺序依照了类的顺序 请留意本结果和上面结果是否有区别,你应该若有所思。 这是单.java文件的case(所有static类都在同一个.java文件内),接下来我在同目录下增加 2个.java文件(请自行留意类名第一个字母,我将不再赘述我的设计意图): // 文件一: @Configuration class A_OuterConfig { A_OuterConfig() { System.out.println("A_OuterConfig init..."); } @Bean String a_o_bean(){ System.out.println("A_OuterConfig a_o_bean init..."); return new String(); } @Configuration private static class PInnerConfig { PInnerConfig() { System.out.println("A_OuterConfig PInnerConfig init..."); } @Bean String a_p_bean(){ System.out.println("A_OuterConfig a_p_bean init..."); return new String(); } } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("A_OuterConfig InnerConfig init..."); } @Bean String a_i_bean(){ System.out.println("A_OuterConfig a_i_bean init..."); return new String(); } } } // 文件二: @Configuration class Z_OuterConfig { Z_OuterConfig() { System.out.println("Z_OuterConfig init..."); } @Bean String z_o_bean(){ System.out.println("Z_OuterConfig z_o_bean init..."); return new String(); } @Configuration private static class PInnerConfig { PInnerConfig() { System.out.println("Z_OuterConfig PInnerConfig init..."); } @Bean String z_p_bean(){ System.out.println("Z_OuterConfig z_p_bean init..."); return new String(); } } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("Z_OuterConfig InnerConfig init..."); } @Bean String z_i_bean(){ System.out.println("Z_OuterConfig z_i_bean init..."); return new String(); } } } 运行程序,结果输出: A_OuterConfig InnerConfig init... A_OuterConfig PInnerConfig init... A_OuterConfig init... InnerConfig init... PInnerConfig init... OuterConfig init... Z_OuterConfig InnerConfig init... Z_OuterConfig PInnerConfig init... Z_OuterConfig init... A_OuterConfig a_i_bean init... A_OuterConfig a_p_bean init... A_OuterConfig a_o_bean init... Daughter init... son init... Parent init... Z_OuterConfig z_i_bean init... Z_OuterConfig z_p_bean init... Z_OuterConfig z_o_bean init... 这个结果大而全,是有说服力的,通过这几个示例可以总结出如下结论: 垮.java文件 (垮配置类)之间的顺序,是由自然顺序来保证的(字母表顺序) 如上:下加载A打头的配置类(含静态内部类),再是O打头的,再是Z打头的 同一.java文件内部,static静态内部类优先于外部类初始化。若有多个静态内部类,那么按照类名自然排序初始化(并非按照定义顺序哦,请务必注意) 说明:一般内部类只可能与外部类“发生关系”,与兄弟之间不建议有任何联系,否则顺序控制上你就得当心了。毕竟靠自然顺序去保证是一种弱保证,容错性太低 同一.java文件内,不同类内的@Bean方法之间的执行顺序,保持同2一致(也就说你的@Bean所在的@Configuration配置类先加载,那你就优先被初始化喽) 同一Class内多个@Bean方法的执行顺序,上篇文章static关键字真能提高Bean的优先级吗?答:真能 就已经说过了哈,请移步参见 总的来说,当static标注在class类上时,在同.java文件内它是能够提升优先级的,这对于Spring Boot的自动配置非常有意义,主要体现在如下两个方法: static静态内部类配置优先于外部类加载,从而静态内部类里面的@Bean也优先于外部类的@Bean先加载 既然这样,那么Spring Boot自动配置就可以结合此特性,就可以进行具有优先级的@Conditional条件判断了。这里我举个官方的例子,你便能感受到它的魅力所在: @Configuration public class FeignClientsConfiguration { ... @Bean @Scope("prototype") @ConditionalOnMissingBean public Feign.Builder feignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); } @Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled") public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } } } 因为HystrixFeign.builder()它属于静态内部类,所以这个@Bean肯定是优先于外部的Feign.builder()先加载的。所以这段逻辑可解释为:优先使用HystrixFeign.builder()(若条件满足),否则使用Feign.builder().retryer(retryer)作为兜底。通过此例你应该再一次感受到Bean的加载顺序之于Spring应用的重要性,特别在Spring Boot/Cloud下此特性尤为凸显。 你以为记住这几个结论就完事了?不,这明显不符合A哥的逼格嘛,下面我们就来继续挖一挖吧。 源码分析 关于@Configuration配置类的顺序问题,事前需强调两点: 不同 .java文件 之间的加载顺序是不重要的,Spring官方也强烈建议使用者不要去依赖这种顺序 因为无状态性,因此你在使用过程中可以认为垮@Configuration文件之前的初始化顺序是不确定的 同一.javaw文件内也可能存在多个@Configuration配置类(比如静态内部类、普通内部类等),它们之间的顺序是我们需要关心的,并且需要强依赖于这个顺序编程(比如Spring Boot) @Configuration配置类只有是被@ComponentScan扫描进来(或者被Spring Boot自动配置加载进来)才需要讨论顺序(倘若是构建上下文时自己手动指好的,那顺序就已经定死了嘛),实际开发中的配置类也确实是酱紫的,一般都是通过扫描被加载。接下来我们看看@ComponentScan是如何扫描的,把此注解的解析步骤(伪代码)展示如下: 说明:本文并不会着重分析@ComponentScan它的解析原理,只关注本文“感兴趣”部分 1、解析配置类上的@ComponentScan注解(们):本例中TestSpring作为扫描入口,会扫描到A_OuterConfig/OuterConfig等配置类们 ConfigurationClassParser#doProcessConfigurationClass: // **最先判断** 该配置类是否有成员类(普通内部类) // 若存在普通内部类,最先把普通内部类给解析喽(注意,不是静态内部类) if (configClass.getMetadata().isAnnotated(Component.class.getName())) { processMemberClasses(configClass, sourceClass); } ... // 遍历该配置类上所有的@ComponentScan注解 // 使用ComponentScanAnnotationParser一个个解析 for (AnnotationAttributes componentScan : componentScans) { Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan,...); // 继续判断扫描到的bd是否是配置类,递归调用 ... } 细节说明:关于最先解析内部类时需要特别注意,Spring通过sourceClass.getMemberClasses()来获取内部类们:只有普通内部类属于这个,static静态内部类并不属于它,这点很重要哦 2、解析该注解上的basePackages/basePackageClasses等属性值得到一些扫描的基包,委托给ClassPathBeanDefinitionScanner去完成扫描 ComponentScanAnnotationParser#parse // 使用ClassPathBeanDefinitionScanner扫描,基于类路径哦 scanner.doScan(StringUtils.toStringArray(basePackages)); 3、遍历每个基包,从文件系统中定位到资源,把符合条件的Spring组件(强调:这里只指外部@Configuration配置类,还没涉及到里面的@Bean这些)注册到BeanDefinitionRegistry注册中心 ComponentScanAnnotationParser#doScan for (String basePackage : basePackages) { // 这个方法是本文最需要关注的方法 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { ... // 把该配置**类**(并非@Bean方法)注册到注册中心 registerBeanDefinition(definitionHolder, this.registry); } } 到这一步就完成了Bean定义的注册,此处可以验证一个结论:多个配置类之间,谁先被扫描到,就先注册谁,对应的就是谁最先被初始化。那么这个顺序到底是咋样界定的呢?那么就要来到这中间最为重要(本文最关心)的一步喽:findCandidateComponents(basePackage)。 说明:Spring 5.0开始增加了@Indexed注解为云原生做了准备,可以让scan扫描动作在编译期就完成,但这项技术还不成熟,暂时几乎无人使用,因此本文仍旧只关注经典模式的实现 ClassPathScanningCandidateComponentProvider#scanCandidateComponents // 最终返回的候选组件们 Set<BeanDefinition> candidates = new LinkedHashSet<>(); // 得到文件系统的路径,比如本例为classpath*:com/yourbatman/**/*.class String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; // 从文件系统去加载Resource资源文件进来 // 这里Resource代表的是一个本地资源:存在你硬盘上的.class文件 Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); for (Resource resource : resources) { if (isCandidateComponent(metadataReader)) { if (isCandidateComponent(sbd)) { candidates.add(sbd); } } } 这段代码的信息量是很大的,分解为如下两大步: 通过ResourcePatternResolver从磁盘里加载到所有的 .class资源Resource[]。这里面顺序信息就出现了,加载磁盘Resource资源的过程很复杂,总而言之它依赖于你os文件系统。所以关于资源的顺序可简单理解为:你磁盘文件里是啥顺序它就按啥顺序加载进来 注意:不是看.java源代码顺序,也不是看你target目录下的文件顺序(该目录是经过了IDEA反编译的结果,无法反应真实顺序),而是编译后看你的磁盘上的.class文件的文件顺序 遍历每一个Resource资源,并不是每个资源都会成为candidates候选,它有个双重过滤(对应两个isCandidateComponent()方法): 过滤一:使用TypeFilter执行过滤,看看是否被排除;再看看是否满足@Conditional条件 过滤二:它有两种case能满足条件(任意满足一个case即可) isIndependent()是独立类(top-level类 or 静态内部类属于独立类) 并且 isConcrete()是具体的(非接口非抽象类) isAbstract()是抽象类 并且 类内存在标注有@Lookup注解的方法 基于以上例子,磁盘中的.class文件情况如下:看着这个顺序,再结合上面的打印结果,是不是感觉得到了解释呢?既然@Configuration类(外部类和内部类)的顺序确定了,那么@Bean就跟着定了喽,因为毕竟配置类也得遍历一个一个去执行嘛(有依赖关系的case除外)。 特别说明:理论上不同的操作系统(如windows和Linux)它们的文件系统是有差异的,对文件存放的顺序是可能不同的(比如$xxx内部类可能放在后面),但现实状况它们是一样的,因此各位同学对此无需担心跨平台问题哈,这由JVM底层来给你保证。 什么,关于此解析步骤你想要张流程图?好吧,你知道的,这个A哥会放到本专栏的总结篇里统一供以你白嫖,关注我公众号吧~ 静态内部类在容器内的beanName是什么? 看到这个截图你就懂了:在不同.java文件内,静态内部类是不用担心重名问题的,这不也就是内聚性的一种体现麽。说明:beanName的生成其实和你注册Bean的方式有关,比如@Import、Scan方式是不一样的,这里就不展开讨论了,知道有这个差异就成。 进阶:Spring下普通内部类表现如何? 我们知道,从内聚性上来说,普通内部类似乎也可以达到目的。但是相较于静态内部类在Spring容器内对优先级的问题,它的表现可就没这么好喽。基于以上例子,把所有的static关键字去掉,就是本处需要的case。 reRun测试程序,结果输出: A_OuterConfig init... OuterConfig init... Z_OuterConfig init... A_OuterConfig InnerConfig init... A_OuterConfig a_i_bean init... A_OuterConfig PInnerConfig init... A_OuterConfig a_p_bean init... A_OuterConfig a_o_bean init... InnerConfig init... Daughter init... PInnerConfig init... son init... Parent init... Z_OuterConfig InnerConfig init... Z_OuterConfig z_i_bean init... Z_OuterConfig PInnerConfig init... Z_OuterConfig z_p_bean init... Z_OuterConfig z_o_bean init... 对于这个结果A哥不用再做详尽分析了,看似比较复杂其实有了上面的分析还是比较容易理解的。主要有如下两点需要注意: 普通内部类它不是一个独立的类(也就是说isIndependent() = false),所以它并不能像静态内部类那样预先就被扫描进去,如图结果展示: 普通内部类初始化之前,一定得先初始化外部类,所以类本身的优先级是低于外部类的(不包含@Bean方法哦) 普通内部类属于外部类的memberClasses,因此它会在解析当前外部类的第一步processMemberClasses()时被解析 普通内部类的beanName和静态内部类是有差异的,如下截图: 思考题: 请思考:为何使用普通内部类得到的是这个结果呢?建议copy我的demo,自行走一遍流程,多动手总是好的 总结 本文一如既往的很干哈。写本文的原动力是因为真的太多小伙伴在看Spring Boot自动配置类的时候,无法理解为毛它有些@Bean配置要单独写在一个static静态类里面,感觉挺费事;方法前直接价格static不香吗?通过这篇文章 + 上篇文章的解读,相信A哥已经给了你答案了。 static关键字在Spring中使用的这个专栏,下篇将进入到可能是你更关心的一个话题:为毛static字段不能使用@Autowired注入的分析,下篇见~ 关注A哥 Author A哥(YourBatman) 个人站点 www.yourbatman.cn E-mail yourbatman@qq.com 微 信 fsx641385712 活跃平台 公众号 BAT的乌托邦(ID:BAT-utopia) 知识星球 BAT的乌托邦 每日文章推荐 每日文章推荐
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位小伙伴大家好,我是A哥。关于Spring初始化Bean的顺序问题,是个老生常谈的话题了,结论可总结为一句话:全局无序,局部有序。Spring Bean整体上是无序的,而现实是大多数情况下我们真的无需关心,无序就无序呗,无所谓喽。但是(此处应该有但是哈),我有理由相信,对于有一定从业经验的Javaer来说,或多或少都经历过Bean初始化顺序带来的“困扰”,也许是因为没有对你的功能造成影响,也许可能是你全然“不知情”,所以最终就不了了之~ 隐患终归隐患,依照墨菲定律来讲,担心的事它总归是会发生的。A哥经常“教唆”程序员要面向工资编程,虽然这价值观有点扭曲,但不可否认很多小伙伴真是这么想的(命中你了没有),稍加粉饰了而已。话粗理不粗哦,almost所有的Javaer都在用Spring,你凭什么工资比你身边同事的高呢?Spring对Bean的(生命周期)管理是它最为核心的能力,同时也是很复杂、很难掌握的一个知识点。现在就可以启动你的工程,有木有这句日志: "Bean 'xxx' of type [xxxx] is not eligible for getting processed by all BeanPostProcessors" + "(for example: not eligible for auto-proxying)" 这是一个典型的Spring Bean过早初始化问题,搜搜看你日志里是否有此句喽。这句日志是由Spring的BeanPostProcessorChecker这个类负责输出,含义为:你的Bean xxx不能被所有的BeanPostProcessors处理到(有的生命周期触达不到),提醒你注意。此句日志在低些的版本里是warn警告级别,在本文约定的版本里官方把它改为了info级别。 绝大多数情况下,此句日志的输出不会对你的功能造成影响,因此无需搭理。这也是Spring官方为何把它从warn调低为info级别的原因 我在CSDN上写过一篇“Spring Bean过早初始化导致的误伤”的文章,访问量达近4w:从这个数据(访问量)上来看,这件事“并不简单”,遇到此麻烦的小伙伴不在少数且确实难倒了一众人。关于Spring Bean的顺序,全局是不可控的,但是局部上它提供了多种方式来方便使用者提高/降低优先级(比如前面的使用@AutoConfigureBefore调整配置顺序竟没生效?这篇文章),本文就聊聊static关键字对于提供Bean的优先级的功效。 版本约定 本文内容若没做特殊说明,均基于以下版本: JDK:1.8 Spring Framework:5.2.2.RELEASE 正文 本文采用从 问题提出-结果分析-解决方案-原理剖析 这4个步骤,层层递进的去感受static关键字在Spring Bean上的魅力~ 警告一:来自BeanPostProcessorChecker 这是最为常见的一种警告,特别当你的工程使用了shiro做鉴权框架的时候。在我记忆中这一年来有N多位小伙伴问过我此问题,可见一斑。 @Configuration class AppConfig { AppConfig() { System.out.println("AppConfig init..."); } @Bean BeanPostProcessor postProcessor() { return new MyBeanPostProcessor(); } } class MyBeanPostProcessor implements BeanPostProcessor { MyBeanPostProcessor() { System.out.println("MyBeanPostProcessor init..."); } } 运行程序,输出结果: AppConfig init... 2020-05-31 07:40:50.979 INFO 15740 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'appConfig' of type [com.yourbatman.config.AppConfig$$EnhancerBySpringCGLIB$$29b523c8] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) MyBeanPostProcessor init... ... 结果分析(问题点/冲突点): AppConfig优先于MyBeanPostProcessor进行实例化 常识是:MyBeanPostProcessor作为一个后置处理器理应是先被初始化的,而AppConfig仅仅是个普通Bean而已,初始化理应靠后 出现了BeanPostProcessorChecker日志:表示AppConfig这个Bena不能被所有的BeanPostProcessors处理,所以有可能会让它“错过”容器对Bean的某些生命周期管理,因此可能损失某些能力(比如不能被自动代理),存在隐患 但凡只要你工程里出现了BeanPostProcessorChecker输出日志,理应都得引起你的注意,因为这属于Spring的警告日志(虽然新版本已下调为了info级别) 说明:这是一个Info日志,并非warn/error级别。绝大多数情况下你确实无需关注,但是如果你是一个容器开发者,建议请务必解决此问题(毕竟貌似大多数中间件开发者都有一定代码洁癖) 解决方案:static关键字提升优先级 基于上例,我们仅需做如下小改动: AppConfig: //@Bean //BeanPostProcessor postProcessor() { // return new MyBeanPostProcessor(); //} // 方法前面加上static关键字 @Bean static BeanPostProcessor postProcessor() { return new MyBeanPostProcessor(); } 运行程序,结果输出: MyBeanPostProcessor init... ... AppConfig init... ... 那个烦人的BeanPostProcessorChecker日志就不见了,清爽了很多。同时亦可发现AppConfig是在MyBeanPostProcessor之后实例化的,这才符合我们所想的“正常”逻辑嘛。 警告二:Configuration配置类增强失败 这个“警告”就比上一个严重得多了,它有极大的可能导致你程序错误,并且你还很难定位问题所在。 @Configuration class AppConfig { AppConfig() { System.out.println("AppConfig init..."); } @Bean BeanDefinitionRegistryPostProcessor postProcessor() { return new MyBeanDefinitionRegistryPostProcessor(); } /////////////////////////////// @Bean Son son(){ return new Son(); } @Bean Parent parent(){ return new Parent(son()); } } class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { MyBeanDefinitionRegistryPostProcessor() { System.out.println("MyBeanDefinitionRegistryPostProcessor init..."); } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } } 运行程序,结果输出: AppConfig init... MyBeanDefinitionRegistryPostProcessor init... 2020-05-31 07:59:06.363 INFO 37512 --- [ main] o.s.c.a.ConfigurationClassPostProcessor : Cannot enhance @Configuration bean definition 'appConfig' since its singleton instance has been created too early. The typical cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring such methods as 'static'. ... son init...hashCode() = 1300528434 son init...hashCode() = 1598434875 Parent init... 结果分析(问题点/冲突点): AppConfig竟然比MyBeanDefinitionRegistryPostProcessor的初始化时机还早,这本就不合理 从ConfigurationClassPostProcessor 的日志中可看到:AppConfig配置类enhance增强失败 Son对象竟然被创建了两个不同的实例,这将会直接导致功能性错误 这三步结果环环相扣,因为1导致了2的增强失败,因为2的增强失败导致了3的创建多个实例,真可谓一步错,步步错。需要注意的是:这里ConfigurationClassPostProcessor输出的依旧是info日志(我个人认为,Spring把这个输出调整为warn级别是更为合理的,因为它影响较大)。 说明:对这个结果的理解基于对Spring配置类的理解,因此强烈建议你进我公众号参阅那个可能是写的最全、最好的Spring配置类专栏学习(文章不多,6篇足矣) 源码处解释: ConfigurationClassPostProcessor: // 对Full模式的配置类尝试使用CGLIB字节码提升 public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) { ... // 对Full模式的配置类有个判断/校验 if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) { if (!(beanDef instanceof AbstractBeanDefinition)) { throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" + beanName + "' since it is not stored in an AbstractBeanDefinition subclass"); } // 若判断发现此时该配置类已经是个单例Bean了(说明已初始化完成) // 那就不再做处理,并且输出警告日志告知使用者(虽然是info日志) else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) { logger.info("Cannot enhance @Configuration bean definition '" + beanName + "' since its singleton instance has been created too early. The typical cause " + "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + "return type: Consider declaring such methods as 'static'."); } configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef); } ... } 由于配置类增强是在BeanFactoryPostProcessor#postProcessBeanFactory()声明周期阶段去做的,而BeanDefinitionRegistryPostProcessor它会优先于该步骤完成实例化(其实主要是优先级比BeanFactoryPostProcessor高),从而间接带动 AppConfig提前初始化导致了问题,这便是根本原因所在。 提问点:本处使用了个自定义的BeanDefinitionRegistryPostProcessor模拟了效果,那如果你是使用的BeanFactoryPostProcessor能出来这个效果吗???答案是不能的,具体原因留给读者思考,可参考:PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors这段流程辅助理解。 解决方案:static关键字提升优先级 来吧,继续使用static关键字改造一下: AppConfig: //@Bean //BeanDefinitionRegistryPostProcessor postProcessor() { // return new MyBeanDefinitionRegistryPostProcessor(); //} @Bean static BeanDefinitionRegistryPostProcessor postProcessor() { return new MyBeanDefinitionRegistryPostProcessor(); } 运行程序,结果输出: MyBeanDefinitionRegistryPostProcessor init... ... AppConfig init... son init...hashCode() = 2090289474 Parent init... ... 完美。 警告三:非静态@Bean方法导致@Autowired等注解失效 @Configuration class AppConfig { @Autowired private Parent parent; @PostConstruct void init() { System.out.println("AppConfig.parent = " + parent); } AppConfig() { System.out.println("AppConfig init..."); } @Bean BeanFactoryPostProcessor postProcessor() { return new MyBeanFactoryPostProcessor(); } @Bean Son son() { return new Son(); } @Bean Parent parent() { return new Parent(son()); } } class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { MyBeanFactoryPostProcessor() { System.out.println("MyBeanFactoryPostProcessor init..."); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } } 运行程序,结果输出: AppConfig init... 2020-05-31 08:28:06.550 INFO 1464 --- [ main] o.s.c.a.ConfigurationClassEnhancer : @Bean method AppConfig.postProcessor is non-static and returns an object assignable to Spring's BeanFactoryPostProcessor interface. This will result in a failure to process annotations such as @Autowired, @Resource and @PostConstruct within the method's declaring @Configuration class. Add the 'static' modifier to this method to avoid these container lifecycle issues; see @Bean javadoc for complete details. MyBeanFactoryPostProcessor init... ... son init...hashCode() = 882706486 Parent init... 结果分析(问题点/冲突点): AppConfig提前于MyBeanFactoryPostProcessor初始化 @Autowired/@PostConstruct等注解没有生效,这个问题很大 需要强调的是:此时的AppConfig是被enhance增强成功了的,这样才有可能进入到BeanMethodInterceptor拦截里面,才有可能输出这句日志(该拦截器会拦截Full模式配置列的所有的@Bean方法的执行) 这句日志由ConfigurationClassEnhancer.BeanMethodInterceptor输出,含义为:你的@Bean标注的方法是非static的并且返回了一个BeanFactoryPostProcessor类型的实例,这就导致了配置类里面的@Autowired, @Resource,@PostConstruct等注解都将得不到解析,这是比较危险的(所以其实这个日志调整为warn级别也是阔仪的)。 小细节:为毛日志看起来是ConfigurationClassEnhancer这个类输出的呢?这是因为BeanMethodInterceptor是它的静态内部类,和它共用的一个logger 源码处解释: ConfigurationClassEnhancer.BeanMethodInterceptor: if (isCurrentlyInvokedFactoryMethod(beanMethod)) { if (logger.isInfoEnabled() && BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) { logger.info(String.format("@Bean method %s.%s is non-static and returns an object " + "assignable to Spring's BeanFactoryPostProcessor interface. This will " + "result in a failure to process annotations such as @Autowired, " + "@Resource and @PostConstruct within the method's declaring " + "@Configuration class. Add the 'static' modifier to this method to avoid " + "these container lifecycle issues; see @Bean javadoc for complete details.", beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName())); } return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } 解释为:如果当前正在执行的@Bean方法(铁定不是static,因为静态方法它也拦截不到嘛)返回类型是BeanFactoryPostProcessor类型,那就输出此警告日志来提醒使用者要当心。 解决方案:static关键字提升优先级 AppConfig: //@Bean //BeanFactoryPostProcessor postProcessor() { // return new MyBeanFactoryPostProcessor(); //} @Bean static BeanFactoryPostProcessor postProcessor() { return new MyBeanFactoryPostProcessor(); } 运行程序,结果输出: MyBeanFactoryPostProcessor init... AppConfig init... son init...hashCode() = 1906549136 Parent init... // @PostConstruct注解生效喽 AppConfig.parent = com.yourbatman.bean.Parent@baf1bb3 ... 世界一下子又清爽了有木有。 原因总结 以上三个case是有共同点的,粗略的讲导致它们的原因甚至是同一个:AppConfig这个Bean被过早初始化。然而我们的解决方案似乎也是同一个:使用static提升Bean的优先级。 那么为何AppConfig会被提前初始化呢?为何使用static关键字就没有问题了呢?根本原因可提前剧透:static静态方法属于类,执行静态方法时并不需要初始化所在类的实例;而实例方法属于实例,执行它时必须先初始化所在类的实例。听起来是不是非常的简单,JavaSE的东西嘛,当然只知晓到这个层次肯定是远远不够的,限于篇幅原因,关于Spring是如何处理的源码级别的分析我放在了下篇文章,请别走开哟~ static静态方法一定优先执行吗? 看完本文,有些小伙伴就忍不住跃跃欲试了,甚至很武断的得出结论:static标注的@Bean方法优先级更高,其实这是错误的,比如你看如下示例: @Configuration class AppConfig2 { AppConfig2(){ System.out.println("AppConfig2 init..."); } @Bean Son son() { return new Son(); } @Bean Daughter daughter() { return new Daughter(); } @Bean Parent Parent() { return new Parent(); } } 运行程序,结果输出: AppConfig2 init... son init... Daughter init... Parent init... 这时候你想让Parent在Son之前初始化,因此你想着在用static关键字来提升优先级,这么做: AppConfig2: //@Bean //Parent Parent() { // return new Parent(); //} @Bean static Parent Parent() { return new Parent(); } 结果:你徒劳了,static貌似并没有生效,怎么回事? 原因浅析 为了满足你的好奇心,这里给个浅析,道出关键因素。我们知道@Bean方法(不管是静态方法还是实例方法)最终都会被封装进ConfigurationClass实例里面,使用Set<BeanMethod> beanMethods存储着,关键点在于它是个LinkedHashSet所以是有序的(存放顺序),而存入的顺序底层是由clazz.getDeclaredMethods()来决定的,由此可知@Bean方法执行顺序和有无static没有半毛钱关系。 说明:clazz.getDeclaredMethods()得到的是Method[]数组,是有序的。这个顺序由字节码(定义顺序)来保证:先定义,先服务。 由此可见,static并不是真正意义上的提高Bean优先级,对于如上你的需求case,你可以使用@DependsOn注解来保证,它也是和Bean顺序息息相关的一个注解,在本专栏后续文章中将会详细讲到。 所以关于@Bean方法的执行顺序的正确结论应该是:在同一配置类内,在无其它“干扰”情况下(无@DependsOn、@Lazy等注解),@Bean方法的执行顺序遵从的是定义顺序(后置处理器类型除外)。 小提问:如果是垮@Configuration配置类的情况,顺序如何界定呢?那么这就不是同一层级的问题了,首先考虑的应该是@Configuration配置类的顺序问题,前面有文章提到过配置类是支持有限的的@Order注解排序的,具体分析请依旧保持关注A哥后续文章详解哈... static关键字使用注意事项 在同一个@Configuration配置类内,对static关键字的使用做出如下说明,供以参考: 对于普通类型(非后置处理器类型)的@Bean方法,使用static关键字并不能改变顺序(按照方法定义顺序执行),所以别指望它 static关键字一般有且仅用于@Bean方法返回为BeanPostProcessor、BeanFactoryPostProcessor等类型的方法,并且建议此种方法请务必使用static修饰,否则容易导致隐患,埋雷 static关键字不要滥用(其实任何关键字皆勿乱用),在同一配置类内,与其说它是提升了Bean的优先级,倒不如说它让@Bean方法静态化从而不再需要依赖所在类的实例即可独立运行。另外我们知道,static关键还可以修饰(内部)类,那么如果放在类上它又是什么表现呢?同样的,你先思考,下篇文章我们接着聊~ 说明:使用static修饰Class类在Spring Boot自动配置类里特别特别常见,所以掌握起来很具价值 思考题: 今天的思考题比较简单:为何文首三种case的警告信息都是info级别呢?是否有级别过低之嫌? 总结 本文还是蛮干的哈,不出意外它能够帮你解决你工程中的某些问题,排除掉一些隐患,毕竟墨菲定律被验证了你担心的事它总会发生,防患于未然才能把自己置于安全高地嘛。 你可能诧异,A哥竟能把static关键字在Spring中的应用都能写出个专栏出来,是的,这不是就是本公众号的定位么 ,小而美和拒绝浅尝辄止嘛。对于一些知识(比如本文的static关键字的使用)我并不推崇强行记忆,因为那真的很容易忘,快速使用可以简单记记,但真想记得牢(甚至成为永久记忆),那必须得去深水区看看。来吧,下文将授之以渔~ 很多小伙伴去强行记忆Spring Boot支持的那17种外部化配置,此时你应该问自己:现在你可能记得,一周以后呢?一个月以后呢?所以你需要另辟蹊径,那就持续关注我吧 关注A哥 Author A哥(YourBatman) 个人站点 www.yourbatman.cn E-mail yourbatman@qq.com 微 信 fsx641385712 活跃平台 公众号 BAT的乌托邦(ID:BAT-utopia) 知识星球 BAT的乌托邦 每日文章推荐 每日文章推荐
生命太短暂,不要去做一些根本没有人想要的东西。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。 前言 各位好,我是A哥。最近写了好几篇关于Spring @Configuration的文章,收录在Spring配置类专栏里,这是本公众号的第一个专栏(虽然CSDN里已有几百篇)。虽然写的过程很艰难,但从评价反馈来看都是正向的,聊以安慰呗,比如这个小伙的“三宗最”让我听了很开心啊:虽然每篇文章的阅读量堪忧,毕竟从第一篇文章我就对我自己的公众号定位了嘛:不求大量流行,只求小众共鸣。因为我知道愿意坚持看下去系列文章(强依赖于上下文)的小伙伴还是比较少的,但我相信一旦坚持下来我们的共同话题就多了,“臭味相投”嘛,是这样的吧~ 在这之前,CSDN里写过几百篇关于Spring的文章,但是总感觉体系性不够,东打一炮,西放一枪难免会有失连贯性。我一直认为大多时候技术的相关性、上下文上很必要的,仅靠一篇文章想把一个成型的知识点讲清楚几乎没可能,所以那种容易成为快餐,过眼即忘。这不这次我选择在公众号里做些成系列的专题,在CSDN的基础上,抽取精华,去其糟粕,以自身能力尽量的做好每一个专栏,普惠于有需要的小伙伴。过程很枯燥和很乏味,愿意看下去的人也不会很多,能坚持下来或许会被自己感动。 作为第一个专栏的总结篇,一般的都是少文字多流程图,旨在起到一个总览的作用,也为方便快速应付面试~ 版本约定 本文内容若没做特殊说明,均基于以下版本: JDK:1.8 Spring Framework:5.2.2.RELEASE 正文 本文以绘制流程图为主,特点是快,缺点是不详,辅以该专栏(关注公众号,进入该专栏。或点击顶部相关推荐直达)前几篇文章可以达到很好的效果。 相关类 @Configuration:标注在类上,表示该类是个Full模式的配置类 自Spring 5.2.0版本后它加了个proxyBeanMethods属性来显示控制Full模式还是Lite模式,默认是true表示Full模式 @Bean:标注在方法上,表示方法生成一个由Spring容器管理的Bean ConfigurationClassPostProcessor:用于引导处理@Configuration配置类的后置处理器。注意:它只是引导处理,并不是实际处理 ConfigurationClassUtils:内部工具类。用于判断组件是否是配置类,又或是Full模式/Lite模式,然后在bd元数据里打上标记 它还会处理一件小事:获取@Configuration配置类上标注的@Order排序值并放进bd里 BeanMethod:内部使用的类。用于封装标注有@Bean注解的方法 ConfigurationClass:内部使用的类。每一个@Configuration配置类都会被封装为它,内部会包含多个@Bean方法(BeanMethod) ConfigurationClassParser:解析@Configuration配置类,最终以ConfigurationClass对象的形式展示,并且填充它:因为一个配置类可以@Import导入另外一个(或者N多个)其它配置类,所以需要填充 ConfigurationClassBeanDefinitionReader:内部使用的类。读取给定的已经解析好的Set<ConfigurationClass>集合,把里面的bd信息注册到BeanDefinitionRegistry里去(这里决定了bd的有序和无序相关问题) ConfigurationClassEnhancer:内部使用的类。配置类增强器,用于对@Configuration类(Full模式)使用CGLIB增强,生成一个代理子类字节码Class对象 EnhancedConfiguration:被增强器增强过的配置类,都会自动的让实现此接口(实际是个BeanFactoryAware)接口 SpringNamingPolicy:使用CGLIB生成字节码类名名称生成策略 -> 名称中会有BySpringCGLIB字样 BeanFactoryAwareMethodInterceptor:CGLIB代理对象拦截器。作用:拦截代理类的setBeanFactory()方法,给对应属性赋值 BeanMethodInterceptor:CGLIB代理对象拦截器。作用:拦截所有@Bean方法的执行,以支持可以通过直接调用@Bean方法来管理依赖关系(当然也支持FactoryBean模式) 配置类解析流程图 配置类的解析均是交由ConfigurationClassPostProcessor来引导。在Spring Framework里(非Spring Boot)里,它是BeanDefinitionRegistryPostProcessor处理器的唯一实现类,用于引导处理@Configuration配置类。解析入口是postProcessBeanDefinitionRegistry()方法,实际处理委托给了processConfigBeanDefinitions()方法。 配置类增强流程图 如果一个配置类是Full模式,那么它就需要被CGLIB字节码提升。增强动作委托给enhanceConfigurationClasses(beanFactory)去完成。 以上是引导/调度的流程图,下面对字节码增强、实际拦截实现流程进行细分描述。 生成增强子类字节码流程图 针对于Full模式配置类的字节码生成,委托给ConfigurationClassEnhancer增强器去完成,最终得到一个CGLIB提升过的子类Class字节码对象。字节码实际是由Enhancer生成,就不用再深入了,那属于CGLIB(甚至ASM)的范畴,很容易头晕,也并无必要。 拦截器执行流程图 拦截器是完成增强实际逻辑的核心部件,因此它的执行流程需要引起重视。一共有两个“有用”的拦截器,分别画出。 BeanFactoryAwareMethodInterceptor拦截流程图 拦截setBeanFactory()方法的执行 BeanMethodInterceptor拦截流程图 拦截@Bean方法的执行 总结 本文作为公众号首个专栏Spring配置类的总结篇,主要是对核心处理流程画图阐述,适合需要快速理解的白嫖党,毕竟面试最喜欢问的就是让你说说执行流程之类的,因此实用性还是蛮高的,以后的专栏均会仿造此套路来玩。 关于Spring配置类这个专栏到这就全部结束了,在此也多谢各位在这期间给我的反馈,让我确定以及肯定了这么坚持下去是有意义的,是被支持的,是能够帮助到同仁们的。我公众号定位为专栏式学习,拒绝浅尝遏止,诚邀你的关注,一起进步。 Tips:有小伙伴私信我说有没有入门级别的?答案是没有的。主要是觉得入门级文章网上太多了,趋同性很强,所以我这一般会篇进阶,有点工作经验/基础再看效果更佳 关注A哥 Author A哥(YourBatman) 个人站点 www.yourbatman.cn E-mail yourbatman@qq.com 微 信 fsx641385712 活跃平台 公众号 BAT的乌托邦(ID:BAT-utopia) 知识星球 BAT的乌托邦 每日文章推荐 每日文章推荐
当大潮退去,才知道谁在裸泳。。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。 前言 各位小伙伴大家好,我是A哥。北京时间2020-05-15,Spring Boot 2.3.0版本正式发布了,次版本号的升级,一般会有些新特性出来。作为Java Coder的我们有必要一览它的新new Feature,keep下技术节奏嘛。 A哥“第二时间”知道了这个消息,然后在自己本机(请注意:非生产环境)体验了一把,然后再结合Spring Boot官方的Release Notes,在这里给你絮叨絮叨。 关于版本号 Spring Boot代码库的版本好采用“国际通用”(我自己yy的)的命名方式:主版本号.次版本号.修订号,所以通过版本号就能感受到它的变化到底大不大,你升级时是否需要倍加注意等等。那么此处我就对这种命名方式版本号的各段进行科普一波: 主版本号:完全不兼容。产品定位变化、核心API大规模不兼容(比如包名变了)、架构方式升级不能向下兼容...... 举例:Configuration1.x -> 2.x;Zuul1.x -> 2.x;Spring Boot1.x -> 2.x;Netty4.x -> 5.x 次版本号:相对兼容。一般是增加新特新,删除掉废弃的API,修改某些API不兼容。总的来说是影响比较小,在可控范围内的,但升级时不可掉以轻心,必须做前期调研 修订号:100%兼容。一般是修复bug、新增无伤大雅的一些特性等,一般想升就升 这次Spring Boot升级到2.3.0版本,属于次版本号的升级,因此会带有些新特性,还是值得一看的。 正文 Spring Boot v2.2依然是活跃的维护的版本,Spring Boot遵循的是Pivotal OSS支持策略,从发布日期起支持主要版本3年。但是呢,一般来说在主要/次要版本发布时,将会对上个主要版本至少提供12个月的支持(即使超过了3年),以解决关键的bug或者安全问题。 关于其它版本的维护活跃状态和已经EOL的日期,做出如下说明: 2.2.x:支持的版本。2019.10发布,是现在的活跃的主干 2.1.x:支持的版本。2018.10发布,会支持到2020.10月底 2.0.x:生命已终止的版本。2018.3发布,2019.4.3停止维护 1.5.x:生命已终止的版本。2017.1发布,是最后一个1.x分支,2019.8.1停止维护 从官网页面也可以看出,只有支持的版本才会被列出来,对使用者是有一定的引导作用的: 简单回忆2.2版本的新特性 很明显,Spring Boot2.2版本不是本文关心的重点,但为了起到衔接作用,本处把它的核心新特性列一下: Spring Framework 5.2:重大升级,可以看到它为Cloud Native的努力 JUnit 5:从此版本开始,spring-boot-starter-test默认使用JUnit 5作为单元测试框架 支持Java13 性能提升:表现在对所有的自动配置类改为了@Configuration的Lite模式,提升性能。 新增@ConfigurationPropertiesScan注解,自动扫描@ConfigurationProperties配置类 支持RSocket 下面我们来了解下本次升级(2.3.0版本)的新特性,分为主要新特性和其它新特性分开阐述。 主要新特性 优雅停机 这个新特性深入人心,是开发者、运维的福音啊。据我了解,很多中小型公司/团队都是使用kill -9(当然有些比较“温柔”的团队也用kill -2)来停服的,这样暴力“停机”很容易造成业务逻辑执行失败,导致在一些业务场景下出现数据不一致现象。虽然我们可以通过一些手段(自研)来避免这个问题,但并不是每个公司/团队都去做了。这不Spring Boot2.3.0版本就内置了这个功能:优雅停机。 小知识:kill -2类似于你的Ctrl + C,会触发shutDownHook事件(从而关闭Spring容器);kill -9就没啥好说的,杀杀杀 SB所有四个嵌入式web服务器(Jetty、Reactor Netty、Tomcat和Undertow)以及响应性和基于servlet的web应用都支持优雅的关闭。在关闭时,web服务器将不再允许新的请求,并将等待完成的请求给个宽限期让它完成。当然这个宽限期是可以设置的:可以使用spring.lifecycle.timeout-per-shutdown-phase=xxx来配置,默认值是30s。 注意,注意,注意:默认情况下,优雅关机并没有开启(还是立即关机),你仅需添加server.shutdown=graceful配置即可开启优雅关机(取值参见2.3.0新增的Shutdown枚举类,默认值参见AbstractConfigurableWebServerFactory.shutdown属性值)。 配置属性的调整 这个版本中,一些配置属性已被重命名或弃用(这会导致不向下兼容,需要特别引起注意),需要你做出调整。 那么如何知道我现在用的哪些属性存在不兼容情况呢???官方给了一个很好的解决方案,这里我用个使用示例教你可以这么处理: 现状:在Spring Boot2.2.x环境中你有很多配置,痛点是不知道哪些配置需要配替换成2.3.x中新的。此时你可以在工程下加入这个jar: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope> </dependency> 然后升级你的Spring Boot版本号为2.3.0,重新启动工程。本处以你配置文件里的spring.http.encoding.enabled=true为例,由于使用了SB的最新版本,因此可以在控制台看到如下日志输出: Property source 'applicationConfig: [classpath:/application.properties]': Key: spring.http.encoding.enabled Line: 3 Replacement: server.servlet.encoding.enabled 日志说够明确了吧。有了这个好帮手,妈妈就不用再担心辣么多的配置项需要自己一个个去核对喽,按照指示一个个的修改即可。 官方说明:完成迁移后,请确保从项目的依赖项中删除properties-migrator这个模块。 顺道说一下:升级到2.3.0版本号,属性变化主要是这个:spring.http. -> server.servlet.encoding.、spring.mvc.、spring.codec. 删除不推荐使用的类/方法/属性 在该版本中,Spring Boot删除了2.2版本中不推荐使用的大多数类,方法和属性。请确保升级之前没有再调用不推荐使用的方法。针对于此,下面我举例那些在2.2版本中还“活着”但被弃用(标记有@Deprecated注解),但在2.3版本中已完全删除的类、方法、属性: 方法BindResult#orElseCreate 属性LoggingApplicationListener#LOGFILE_BEAN_NAME 类JodaDateTimeJacksonConfiguration 类JestAutoConfiguration 即使如此,有些虽然在2.2就已被弃用,但在2.3.0还存在的,如:ConfigurationBeanFactoryMetadata、CompositeHealthIndicator 配置文件位置支持通配符 Spring Boot现在在加载配置文件时支持通配符位置。默认情况下,jar外部的config/*/位置是被支持的。当配置属性有多个源时,比如在Kubernetes这样的环境中非常有用。 特点说明:jar包外,jar包外,jar包外,放在内部(比如resource目录下是没有此特针的),下面有示例证明 简单的说,如果你有MySql的配置和Redis配置的话,你就可以把他们分开来放置,隔离性更好目录也更加清晰了: mysql:/config/mysql/application.properties redis:/config/redis/application.properties 工程目录如下截图:运行程序: public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(Boot23Demo1Application.class, args); ConfigurableEnvironment environment = context.getEnvironment(); System.out.println(environment.getProperty("mysql.name")); System.out.println(environment.getProperty("redis.name")); context.close(); } 结果输出: mysql redis 但如果你把文件放在jar包内,形如这样,是没有效果的:输出为null null,因此使用时需要稍加注意哈~ web下的日期转换支持配置 现在时间/日期的转换现在可以通过属性进行配置了,这补充了对格式化日期值的现有支持。比如对于MVC和WebFlux来说,它们的配置项分别如下: spring.mvc.format.date spring.mvc.format.date-time spring.mvc.format.time spring.webflux.format.date spring.webflux.format.date-time spring.webflux.format.time 这个怎么用,相信大家都会,一看就知道什么含义。但是,但是,但是:请一定做好充分测试,并且充分考虑兼容性,因为你这动的是接口层的东西~ 其它新特性 更改某些依赖最低版本要求 主要体现在如下两处: 如果你使用Gradle构建,支持Gradle 6.3+ 。当然喽5.6.x也支持,只是标记为@Deprecated不推荐使用了 如果你使用Jetty嵌入式容器,版本要求是Jetty 9.4.22+ 核心依赖升级 Spring Boot 2.3迁移到几个Spring项目的新版本: Spring Data Neumann:你可以理解为它就是之前的Spirng Data工程的升级版 Spring HATEOAS 1.1 Spring Integration 5.3 Spring Kafka 2.5 Spring Security 5.3 Spring Session Dragonfruit Spring Boot 2.3的构建与Spring Boot 2.2基于 相同的 Spring Framework和Reactor。 说明:spirng-core么有升级,还是5.2.6版本(SB的2.2.7版本依赖的spring-core也是这个版本) 三方库依赖升级 AssertJ 3.16 Cassandra Driver 4.6 Elasticsearch 7.6 Hibernate Validator 6.1 JUnit Jupiter 5.6 Kafka 2.5 Lettuce 5.3 Micrometer 1.5 MongoDB 4.0 Spring Data Neumann升级带来的变化 Cassandra:升级到v4版本,带来了一些变化,如ClusterBuilderCustomizer就木有了~ Couchbase:升级到v3版本 Elasticsearch:已废弃的原生Elasticsearch transport直接被删除了,并且还移除了对Jest的支持。从此版本开始,默认支持Elasticsearch7.5+ MongoDB:升级到v4版本 关于Validation 从此版本开始,spring-boot-starter-web不会再把validation带进来,所以若使用到,你需要自己添加这个spring-boot-starter-validation依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> 关于spring-boot-starter-web启动器新、老版本的区别截图: 移除一些maven插件 移除了对exec-maven-plugin和maven-site-plugin这两个插件的管理,所以如果你的工程依赖于这两个插件,那么你得显示的导入(指定版本号)。 支持Java14 Spring Boot 2.3增加了对Java 14的支持。当然Java 8和Java 11也还是被支持的。 Docker支持 在Maven和Gradle插件中添加了对构建包含分层内容的jar文件的支持。分层根据jar内容的更改频率来分隔它们。这种分离允许更有效地构建Docker映像。未更改的现有层可以与已更改的层一起放在顶部进行重用。 根据您的应用程序,您可能需要调整层的创建方式并添加新层。这可以通过描述如何将jar分成层以及这些层的顺序的配置来完成。 Fat Jar支持优化 用Maven和Gradle构建的Fat jar现在包括一个索引文件。当jar被分解时,这个索引文件用于确保类路径的顺序与直接执行jar时相同。 嵌入式Servlet Web Server线程配置 用于配置嵌入式Servlet web服务器使用的线程的配置属性(包括Jetty, Tomcat, 和Undertow)别移动到了专注于threads的组:erver.jetty.threads,server.tomcat.threads,server.undertow.threads。当然喽,旧的配置属性目前依然保留着,但被标记为@Deprecated不再推荐使用了~ WebFlux基础路径配置 现在可以配置WebFlux应用程序的所有web处理程序的基本路径。使用pring.webflux.base-path = xxx配置。 活性探测器 Spring Boot现在内置了关于应用程序可用性的探测的能力,可以跟踪应用程序是否处于活动状态以及是否准备好处理流量。如果你配置了management.health.probes.enabled=true,那么健康检查端点就可以查看你应用的活性和就绪列表,这在在Kubernetes上运行时,这是自动完成的。 Actuator增强 主要是对端点做了些输出、显示上的优化。如: /actuator/metrics/:按字母顺序排列,这样你找起来就更方便了 DataSource的HealthIndicator健康指示器,现在进行无查询判断,而Connection仅做连接可用性验证而已 ... 好基友Spring Cloud什么时候跟上? 作为Spring Boot的好基友,按照以往的惯例,他俩的步调不一般都保持基本一致。戒指到当前,Spring Cloud的最新版本是Hoxton SR4,那它是否支持最新的Spring Boot2.3.0呢???答案是:不支持,不支持,不支持。对于Spring Boot这种跨版本升级,一般是有阻断性变化,所以它的机油SC适配上还需要时间。 这不,官方就公布了Spring Cloud支持Spring Boot 2.3.x的里程碑时间点,也就是它的Hoxton.SR5版本发布时间点:Spring Cloud里程碑地址:https://github.com/spring-cloud/spring-cloud-release/milestones 升级建议:等等 至少要等到2020-5-26号发布后嘛,至少要等到Spring Boot2.3.x跑一段时间之后嘛,坐在第二排看戏,才是最舒服最稳妥的。 总结 这是A哥奉给大家的,对Spring Boot2.3.0版本新特性的介绍,希望对你有些帮助。有些人可能会这么说:反正我现在也不用这个版本,没有必要去了解它。其实非也,如果你2.3.0不去了解,2.4.0不去了解,倘若某一天你突然要从2.0.0版本过度过来使用2.5.x版本了,你会“浑身不舒服”的。你品下,是不是这么个道理呢? 关注A哥 Author A哥(YourBatman) 个人站点 www.yourbatman.cn E-mail yourbatman@qq.com 微 信 fsx641385712 活跃平台 公众号 BAT的乌托邦(ID:BAT-utopia) 知识星球 BAT的乌托邦 每日文章推荐 每日文章推荐
当大潮退去,才知道谁在裸泳。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。 前言 各位小伙伴大家好,我是A哥。本文对Spring @Configuration配置类继续进阶,虽然有点烧脑,但目的只有一个:为拿高薪备好弹药。如果说上篇文章已经脑力有点“不适”了,那这里得先给你个下马威:本篇文章内容将更加的让你“感觉不适”。 读本文之前,为确保连贯性,建议你移步先阅读上篇文章内容,直达电梯:你自我介绍说很懂Spring配置类,那你怎么解释这个现象? 为什么有些时候我会建议先阅读上篇文章,这确实是无奈之举。技术的内容一般都具有很强相关性,它是需要有Context上下文支撑的,所以花几分钟先了解相关内容效果更佳,磨刀不误砍柴工的道理大家都懂。同时呢,这也是写深度分析类的技术文章的尴尬之处:吃力反而不讨好,需要坚持。 版本约定 本文内容若没做特殊说明,均基于以下版本: JDK:1.8 Spring Framework:5.2.2.RELEASE 正文 上篇文章介绍了代理对象两个拦截器其中的前者,即BeanFactoryAwareMethodInterceptor,它会拦截setBeanFactory()方法从而完成给代理类指定属性赋值。通过第一个拦截器的讲解,你能够成功“忽悠”很多面试官了,但仍旧不能够解释我们最常使用中的这个疑惑:为何通过调用@Bean方法最终指向的仍旧是同一个Bean呢? 带着这个疑问,开始本文的陈诉。请系好安全带,准备发车了... Spring配置类的使用误区 根据不同的配置方式,展示不同情况。从Lite模式的使用产生误区,到使用Full模式解决问题,最后引出解释为何有此效果的原因分析/源码解析。 Lite模式:错误姿势 配置类: public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } } 运行程序: public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); AppConfig appConfig = context.getBean(AppConfig.class); System.out.println(appConfig); // bean情况 Son son = context.getBean(Son.class); Parent parent = context.getBean(Parent.class); System.out.println("容器内的Son实例:" + son.hashCode()); System.out.println("容器内Person持有的Son实例:" + parent.getSon().hashCode()); System.out.println(parent.getSon() == son); } 运行结果: son created...624271064 son created...564742142 parent created...持有的Son是:564742142 com.yourbatman.fullliteconfig.config.AppConfig@1a38c59b 容器内的Son实例:624271064 容器内Person持有的Son实例:564742142 false 结果分析: Son实例被创建了2次。很明显这两个不是同一个实例 第一次是由Spring创建并放进容器里(624271064这个) 第二次是由构造parent时创建,只放进了parent里,并没放进容器里(564742142这个) 这样的话,就出问题了。问题表现在这两个方面: Son对象被创建了两次,单例模式被打破 对Parent实例而言,它依赖的Son不再是IoC容器内的那个Bean,而是一个非常普通的POJO对象而已。所以这个Son对象将不会享有Spring带来的任何“好处”,这在实际场景中一般都是会有问题的 这种情况在生产上是一定需要避免,那怎么破呢?下面给出Lite模式下使用的正确姿势。 Lite模式:正确姿势 其实这个问题,现在这么智能的IDE(如IDEA)已经能教你怎么做了:按照“指示”,可以使用依赖注入的方式代替从而避免这种问题,如下: // @Bean // public Parent parent() { // Son son = son(); // System.out.println("parent created...持有的Son是:" + son.hashCode()); // return new Parent(son); // } @Bean public Parent parent(Son son){ System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } 再次运行程序,结果为: son created...624271064 parent created...持有的Son是:624271064 com.yourbatman.fullliteconfig.config.AppConfig@667a738 容器内的Son实例:624271064 容器内Person持有的Son实例:624271064 true bingo,完美解决了问题。如果你坚持使用Lite模式,那么请注意它的优缺点哦(Full模式和Lite模式的优缺点见这篇文章)。 没有仔细看的同学可能会问:我明明就是按照第一种方式写的,也正常work没问题呀。说你是不细心吧还真是,不信你再回去瞅瞅对比对比。如果你用第一种方式并且能够“正常work”,那请你查查类头上是不是标注有@Configuration注解? Full模式: Full模式是容错性最强的一种方式,你乱造都行,没啥顾虑。 当然喽,方法不能是private/final。但一般情况下谁会在配置里final掉一个方法呢?你说对吧~ @Configuration public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } } 运行程序,结果输出: son created...1797712197 parent created...持有的Son是:1797712197 com.yourbatman.fullliteconfig.config.AppConfig$$EnhancerBySpringCGLIB$$8ef51461@be64738 容器内的Son实例:1797712197 容器内Person持有的Son实例:1797712197 true 结果是完美的。它能够保证你通过调用标注有@Bean的方法得到的是IoC容器里面的实例对象,而非重新创建一个。相比较于Lite模式,它还有另外一个区别:它会为配置类生成一个CGLIB的代理子类对象放进容器,而Lite模式放进容器的是原生对象。 凡事皆有代价,一切皆在取舍。原生的才是效率最高的,是对Cloud Native最为友好的方式。但在实际“推荐使用”上,业务端开发一般只会使用Full模式,毕竟业务开发的同学水平是残参差不齐的,容错性就显得至关重要了。 如果你是容器开发者、中间件开发者...推荐使用Lite模式配置,为容器化、Cloud Native做好准备嘛~ Full模式既然是面向使用侧为常用的方式,那么接下来就趴一趴Spring到底是施了什么“魔法”,让调用@Bean方法竟然可以不进入方法体内而指向同一个实例。 BeanMethodInterceptor拦截器 终于到了今天的主菜。关于前面的流程分析本文就一步跳过,单刀直入分析BeanMethodInterceptor这个拦截器,也也就是所谓的两个拦截器的后者。 温馨提示:亲务必确保已经了解过了上篇文章的流程分析哈,不然下面内容很容易造成你脑力不适的 相较于上个拦截器,这个拦截器不可为不复杂。官方解释它的作用为:拦截任何标注有@Bean注解的方法的调用,以确保正确处理Bean语义,例如作用域(请别忽略它)和AOP代理。 复杂归复杂,但没啥好怕的,一步一步来呗。同样的,我会按如下两步去了解它:执行时机 + 做了何事。 执行时机 废话不多说,直接结合源码解释。 BeanMethodInterceptor: @Override public boolean isMatch(Method candidateMethod) { return (candidateMethod.getDeclaringClass() != Object.class && !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) && BeanAnnotationHelper.isBeanAnnotated(candidateMethod)); } 三行代码,三个条件: 该方法不能是Object的方法(即使你Object的方法标注了@Bean,我也不认) 不能是setBeanFactory()方法。这很容易理解,它交给上个拦截器搞定即可 方法必须标注标注有@Bean注解 简而言之,标注有@Bean注解方法执行时会被拦截。 所以下面例子中的son()和parent()这两个,以及parent()里面调用的son()方法的执行它都会拦截(一共拦截3次)~ 小细节:方法只要是个Method即可,无论是static方法还是普通方法,都会“参与”此判断逻辑哦 做了何事 这里是具体拦截逻辑,会比第一个拦截器复杂很多。源码不算非常的多,但牵扯到的东西还真不少,比如AOP、比如Scope、比如Bean的创建等等,理解起来还蛮费劲的。 本处以拦截到parent()方法的执行为例,结合源码进行跟踪讲解: BeanMethodInterceptor: // enhancedConfigInstance:被拦截的对象实例,也是代理对象 // beanMethod:parent()方法 // beanMethodArgs:空 // cglibMethodProxy:代理。用于调用其invoke/invokeSuper()来执行对应的方法 @Override @Nullable public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, MethodProxy cglibMethodProxy) throws Throwable { // 通过反射,获取到Bean工厂。也就是$$beanFactory这个属性的值~ ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance); // 拿到Bean的名称 String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); // 判断这个方法是否是Scoped代理对象 很明显本利里是没有标注的 暂先略过 // 简答的说:parent()方法头上是否标注有@Scoped注解~~~ if (BeanAnnotationHelper.isScopedProxy(beanMethod)) { String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); if (beanFactory.isCurrentlyInCreation(scopedBeanName)) { beanName = scopedBeanName; } } // ========下面要处理bean间方法引用的情况了======== // 首先:检查所请求的Bean是否是FactoryBean。也就是bean名称为`&parent`的Bean是否存在 // 如果是的话,就创建一个代理子类,拦截它的getObject()方法以返回容器里的实例 // 这样做保证了方法返回一个FactoryBean和@Bean的语义是效果一样的,确保了不会重复创建多个Bean if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) && factoryContainsBean(beanFactory, beanName)) { // 先得到这个工厂Bean Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); if (factoryBean instanceof ScopedProxyFactoryBean) { // Scoped proxy factory beans are a special case and should not be further proxied // 如果工厂Bean已经是一个Scope代理Bean,则不需要再增强 // 因为它已经能够满足FactoryBean延迟初始化Bean了~ } // 继续增强 else { return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName); } } // 检查给定的方法是否与当前调用的容器相对应工厂方法。 // 比较方法名称和参数列表来确定是否是同一个方法 // 怎么理解这句话,参照下面详解吧 if (isCurrentlyInvokedFactoryMethod(beanMethod)) { // 这是个小细节:若你@Bean返回的是BeanFactoryPostProcessor类型 // 请你使用static静态方法,否则会打印这句日志的~~~~ // 因为如果是非静态方法,部分后置处理失效处理不到你,可能对你程序有影像 // 当然也可能没影响,所以官方也只是建议而已~~~ if (logger.isInfoEnabled() && BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) { ... // 输出info日志 } // 这表示:当前parent()方法,就是这个被拦截的方法,那就没啥好说的 // 相当于在代理代理类里执行了super(xxx); // 但是,但是,但是,此时的this依旧是代理类 return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } // parent()方法里调用的son()方法会交给这里来执行 return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); } 步骤总结: 拿到当前BeanFactory工厂对象。该工厂对象通过第一个拦截器BeanFactoryAwareMethodInterceptor已经完成了设值 确定Bean名称。默认是方法名,若通过@Bean指定了以指定的为准,若指定了多个值以第一个值为准,后面的值当作Bean的alias别名 判断当前方法(以parent()方法为例)是否是个Scope域代理。也就是方法上是否标注有@Scope注解 若是域代理类,那旧以它的方式来处理喽。beanName的变化变化为scopedTarget.parent 判断scopedTarget.parent这个Bean是否正在创建中...若是的,那就把当前beanName替换为scopedTarget.parent,以后就关注这个名称的Bean了~ 试想一下,如果不来这个判断的话,那最终可能的结果是:容器内一个名为parent的Bean,一个名字为scopedTarget.parent的Bean,那岂不又出问题了麽~ 判断请求的Bean是否是个FactoryBean工厂Bean。 若是工厂Bean,那么就需要enhance增强这个Bean,以拦截它的getObject()方法 拦截getObject()的做法是:当执行getObject()方法时转为 -> getBean()方法 为什么需要这么做:是为了确保FactoryBean产生的实例是通过getBean()容器去获取的,而非又自己创建一个出来了 这种case先打个❓,下面会结合代码示例加以说明 判断这个beanMethod是否是当前正在被调用的工厂方法。 若是正在创建的方法,那就好说了,直接super(xxx)执行父类方法体完事~ 若不是正在创建的方法,那就需要代理喽,以确保实际调用的仍旧是实际调用getBean方法而保证是同一个Bean 这种case先打个❓,下面会结合代码示例加以说明。因为这个case是最常见的主线case,所以先把它搞定 这是该拦截器的执行步骤,留下两个打❓下面我来一一解释(按照倒序)。 多次调用@Bean方法为何不会产生新实例? 这是最为常见的case。示例代码: @Configuration public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { notBeanMethod(); Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } public void notBeanMethod(){ System.out.println("notBeanMethod invoked by 【" + this + "】"); } } 本配置类一共有三个方法: son():标注有@Bean。 因此它最终交给cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);方法直接执行父类(也就是目标类)的方法体:值得注意的是:此时所处的对象仍旧是代理对象内,这个方法体只是通过代理类调用了super(xxx)方法进来的而已嘛~ parent():标注有@Bean。它内部会还会调用notBeanMethod()和son()两个方法 同上,会走到目标类的方法体里,开始调用 notBeanMethod()和son() 这两个方法,这个时候处理的方式就不一样了: 调用notBeanMethod()方法,因为它没有标注@Bean注解,所以不会被拦截 -> 直接执行方法体 调用son()方法,因为它标注有@Bean注解,所以会继续进入到拦截器里。但请注意和上面 直接调用 son()方法不一样的是:此时当前正在被invoked的方法是parent()方法,而并非son()方法,所以他会被交给resolveBeanReference()方法来处理: BeanMethodInterceptor: private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, ConfigurableBeanFactory beanFactory, String beanName) { // 当前bean(son这个Bean)是否正在创建中... 本处为false嘛 // 这个判断主要是为了防止后面getBean报错~~~ boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName); try { // 如果该Bean确实正在创建中,先把它标记下,放置后面getBean报错~ if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, false); } // 更具该方法的入参,决定后面使用getBean(beanName)还是getBean(beanName,args) // 基本原则是:但凡只要有一个入参为null,就调用getBean(beanName) boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs); if (useArgs && beanFactory.isSingleton(beanName)) { for (Object arg : beanMethodArgs) { if (arg == null) { useArgs = false; break; } } } // 通过getBean从容器中拿到这个实例 本处拿出的就是Son实例喽 Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) : beanFactory.getBean(beanName)); // 方法返回类型和Bean实际类型做个比较,因为有可能类型不一样 // 什么时候会出现类型不一样呢?当BeanDefinition定义信息类型被覆盖的时候,就可能出现此现象 if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) { if (beanInstance.equals(null)) { beanInstance = null; } else { ... throw new IllegalStateException(msg); } } // 当前被调用的方法,是parent()方法 Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); if (currentlyInvoked != null) { String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked); // 这一步是注册依赖关系,告诉容器: // parent实例的初始化依赖于son实例 beanFactory.registerDependentBean(beanName, outerBeanName); } // 返回实例 return beanInstance; } // 归还标记:笔记实际确实还在创建中嘛~~~~ finally { if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, true); } } } 这么一来,执行完parent()方法体里的son()方法后,实际得到的是容器内的实例,从而保证了我们这么写是不会有问题的。 notBeanMethod():因为没有标注@Bean,所以它并不会被容器调用,而只能是被上面的parent()方法调用到,并且也不会被拦截(值得注意的是:因为此方法不需要被代理,所以此方法可以是private final的哦~) 以上程序的运行结果是: son created...347978868 notBeanMethod invoked by 【com.yourbatman.fullliteconfig.config.AppConfig$$EnhancerBySpringCGLIB$$ec611337@12591ac8】 parent created...持有的Son是:347978868 com.yourbatman.fullliteconfig.config.AppConfig$$EnhancerBySpringCGLIB$$ec611337@12591ac8 容器内的Son实例:347978868 容器内Person持有的Son实例:347978868 true 可以看到,Son自始至终都只存在一个实例,这是符合我们的预期的。 Lite模式下表现如何? 同样的代码,在Lite模式下(去掉@Configuration注解即可),不存在“如此复杂”的代理逻辑,所以上例的运行结果是: son created...624271064 notBeanMethod invoked by 【com.yourbatman.fullliteconfig.config.AppConfig@21a947fe】 son created...90205195 parent created...持有的Son是:90205195 com.yourbatman.fullliteconfig.config.AppConfig@21a947fe 容器内的Son实例:624271064 容器内Person持有的Son实例:90205195 false 这个结果很好理解,这里我就不再啰嗦了。总之就不能这么用就对了~ FactoryBean模式剖析 FactoryBean也是向容器提供Bean的一种方式,如最常见的SqlSessionFactoryBean就是这么一个大代表,因为它比较常用,并且这里也作为此拦截器一个单独的执行分支,所以很有必要研究一番。 执行此分支逻辑的条件是:容器内已经存在&beanName和beanName两个Bean。执行的方式是:使用enhanceFactoryBean()方法对FactoryBean进行增强。 ConfigurationClassEnhancer: // 创建一个子类代理,拦截对getObject()的调用,委托给当前的BeanFactory // 而不是创建一个新的实例。这些代理仅在调用FactoryBean时创建 // factoryBean:从容器内拿出来的那个已经存在的工厂Bean实例(是工厂Bean实例) // exposedType:@Bean标注的方法的返回值类型 private Object enhanceFactoryBean(Object factoryBean, Class<?> exposedType, ConfigurableBeanFactory beanFactory, String beanName) { try { // 看看Spring容器内已经存在的这个工厂Bean的情况,看看是否有final Class<?> clazz = factoryBean.getClass(); boolean finalClass = Modifier.isFinal(clazz.getModifiers()); boolean finalMethod = Modifier.isFinal(clazz.getMethod("getObject").getModifiers()); // 类和方法其中有一个是final,那就只能看看能不能走接口代理喽 if (finalClass || finalMethod) { // @Bean标注的方法返回值若是接口类型 尝试走基于接口的JDK动态代理 if (exposedType.isInterface()) { // 基于JDK的动态代理 return createInterfaceProxyForFactoryBean(factoryBean, exposedType, beanFactory, beanName); } else { // 类或方法存在final情况,但是呢返回类型又不是 return factoryBean; } } } catch (NoSuchMethodException ex) { // 没有getObject()方法 很明显,一般不会走到这里 } // 到这,说明以上条件不满足:存在final且还不是接口类型 // 类和方法都不是final,生成一个CGLIB的动态代理 return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName); } 步骤总结: 拿到容器内已经存在的这个工厂Bean的类型,看看类上、getObject()方法是否用final修饰了 但凡只需有一个被final修饰了,那注定不能使用CGLIB代理了喽,那么就尝试使用基于接口的JDK动态代理: 若你标注的@Bean返回的是接口类型(也就是FactoryBean类型),那就ok,使用JDK创建个代理对象返回 若不是接口(有final又还不是接口),那老衲无能为力了:原样return返回 若以上条件不满足,表示一个final都木有,那就统一使用CGLIB去生成一个代理子类。大多数情况下,都会走到这个分支上,代理是通过CGLIB生成的 说明:无论是JDK动态代理还是CGLIB的代理实现均非常简单,就是把getObject()方法代理为使用beanFactory.getBean(beanName)去获取实例(要不代理掉的话,每次不就执行你getObject()里面的逻辑了麽,就又会创建新实例啦~) 需要明确,此拦截器对FactoryBean逻辑处理分支的目的是:确保你通过方法调用拿到FactoryBean后,再调用其getObject()方法(哪怕调用多次)得到的都是同一个示例(容器内的单例)。因此需要对getObject()方法做拦截嘛,让该方法指向到getBean(),永远从容器里面拿即可。 这个拦截处理逻辑只有在@Bean方法调用时才有意义,比如parent()里调用了son()这样子才会起到作用,否则你就忽略它吧~ 针对于此,下面给出不同case下的代码示例,加强理解。 代码示例(重要) 准备一个SonFactoryBean用于产生Son实例: public class SonFactoryBean implements FactoryBean<Son> { @Override public Son getObject() throws Exception { return new Son(); } @Override public Class<?> getObjectType() { return Son.class; } } 并且在配置类里把它放好: @Configuration public class AppConfig { @Bean public FactoryBean<Son> son() { SonFactoryBean sonFactoryBean = new SonFactoryBean(); System.out.println("我使用@Bean定义sonFactoryBean:" + sonFactoryBean.hashCode()); System.out.println("我使用@Bean定义sonFactoryBean identityHashCode:" + System.identityHashCode(sonFactoryBean)); return sonFactoryBean; } @Bean public Parent parent(Son son) throws Exception { // 根据前面所学,sonFactoryBean肯定是去容器拿 FactoryBean<Son> sonFactoryBean = son(); System.out.println("parent流程使用的sonFactoryBean:" + sonFactoryBean.hashCode()); System.out.println("parent流程使用的sonFactoryBean identityHashCode:" + System.identityHashCode(sonFactoryBean)); System.out.println("parent流程使用的sonFactoryBean:" + sonFactoryBean.getClass()); // 虽然sonFactoryBean是从容器拿的,但是getObject()你可不能保证每次都返回单例哦~ Son sonFromFactory1 = sonFactoryBean.getObject(); Son sonFromFactory2 = sonFactoryBean.getObject(); System.out.println("parent流程使用的sonFromFactory1:" + sonFromFactory1.hashCode()); System.out.println("parent流程使用的sonFromFactory1:" + sonFromFactory2.hashCode()); System.out.println("parent流程使用的son和容器内的son是否相等:" + (son == sonFromFactory1)); return new Parent(sonFromFactory1); } } 运行程序: @Bean public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); SonFactoryBean sonFactoryBean = context.getBean("&son", SonFactoryBean.class); System.out.println("Spring容器内的SonFactoryBean:" + sonFactoryBean.hashCode()); System.out.println("Spring容器内的SonFactoryBean:" + System.identityHashCode(sonFactoryBean)); System.out.println("Spring容器内的SonFactoryBean:" + sonFactoryBean.getClass()); System.out.println("Spring容器内的Son:" + context.getBean("son").hashCode()); } 输出结果: 我使用@Bean定义sonFactoryBean:313540687 我使用@Bean定义sonFactoryBean identityHashCode:313540687 parent流程使用的sonFactoryBean:313540687 parent流程使用的sonFactoryBean identityHashCode:70807318 parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean$$EnhancerBySpringCGLIB$$1ccec41d parent流程使用的sonFromFactory1:910091170 parent流程使用的sonFromFactory1:910091170 parent流程使用的son和容器内的son是否相等:true Spring容器内的SonFactoryBean:313540687 Spring容器内的SonFactoryBean:313540687 Spring容器内的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean Spring容器内的Son:910091170 结果分析:达到了预期的效果:parent在调用son()方法时,得到的是在容器内已经存在的SonFactoryBean基础上CGLIB字节码提升过的实例,拦截成功,从而getObject()也就实际是去容器里拿对象的。 通过本例有如下小细节需要指出: 原始对象和代理/增强后(不管是CGLIB还是JDK动态代理)的实例的.hashCode()以及.equals()方法是一毛一样的,但是identityHashCode()值(实际内存值)不一样哦,因为是不同类型、不同实例,这点请务必注意 最终存在于容器内的仍旧是原生工厂Bean对象,而非代理后的工厂Bean实例。毕竟拦截器只是拦截了@Bean方法的调用来了个“偷天换日”而已~ 若SonFactoryBean上加个final关键字修饰,根据上面讲述的逻辑,那代理对象会使用JDK动态代理生成喽,形如这样(本处仅作为示例,实际使用中请别这么干): public final class SonFactoryBean implements FactoryBean<Son> { ... } 再次运行程序,结果输出为:执行的结果一样,只是代理方式不一样而已。从这个小细节你也能看出来Spring对代理实现上的偏向:优先选择CGLIB代理方式,JDK动态代理方式用于兜底。 ... // 使用了JDK的动态代理 parent流程使用的sonFactoryBean:class com.sun.proxy.$Proxy11 ... 提示:若你标注了final关键字了,那么请保证@Bean方法返回的是FactoryBean接口,而不能是SonFactoryBean实现类,否则最终无法代理了,原样输出。因为JDK动态代理和CGLIB都搞不定了嘛~ 在以上例子的基础上,我给它“加点料”,再看看效果呢: 使用BeanDefinitionRegistryPostProcessor提前就放进去一个名为son的实例: // 这两种方式向容器扔bd or singleton bean都行 我就选择第二种喽 // 注意:此处放进去的是BeanFactory工厂,名称是son哦~~~ 不要写成了&son @Component public class SonBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // registry.registerBeanDefinition("son", BeanDefinitionBuilder.rootBeanDefinition(SonFactoryBean.class).getBeanDefinition()); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { SonFactoryBean sonFactoryBean = new SonFactoryBean(); System.out.println("初始化时,注册进容器的sonFactoryBean:" + sonFactoryBean); beanFactory.registerSingleton("son", sonFactoryBean); } } 再次运行程序,输出结果: 初始化时最早进容器的sonFactoryBean:2027775614 初始化时最早进容器的sonFactoryBean identityHashCode:2027775614 parent流程使用的sonFactoryBean:2027775614 parent流程使用的sonFactoryBean identityHashCode:1183888521 parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean$$EnhancerBySpringCGLIB$$1ccec41d parent流程使用的sonFromFactory1:2041605291 parent流程使用的sonFromFactory1:2041605291 parent流程使用的son和容器内的son是否相等:true Spring容器内的SonFactoryBean:2027775614 Spring容器内的SonFactoryBean:2027775614 Spring容器内的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean Spring容器内的Son:2041605291 效果上并不差异,从日志上可以看到:你配置类上使用@Bean标注的son()方法体并没执行了,而是使用的最开始注册进去的实例,差异仅此而已。 为何是这样的现象?这就不属于本文的内容了,是Spring容器对Bean的实例化、初始化逻辑,本公众号后面依旧会采用专栏式讲解,让你彻底弄懂它。当前有兴趣的可以先自行参考DefaultListableBeanFactory#preInstantiateSingletons的内容~ Lite模式下表现如何? Lite模式下可没这些“加强特性”,所以在Lite模式下(拿掉@Configuration这个注解便可)运行以上程序,结果输出为: 我使用@Bean定义sonFactoryBean:477289012 我使用@Bean定义sonFactoryBean identityHashCode:477289012 我使用@Bean定义sonFactoryBean:2008966511 我使用@Bean定义sonFactoryBean identityHashCode:2008966511 parent流程使用的sonFactoryBean:2008966511 parent流程使用的sonFactoryBean identityHashCode:2008966511 parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean parent流程使用的sonFromFactory1:433874882 parent流程使用的sonFromFactory1:572191680 parent流程使用的son和容器内的son是否相等:false Spring容器内的SonFactoryBean:477289012 Spring容器内的SonFactoryBean:477289012 Spring容器内的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean Spring容器内的Son:211968962 结果解释我就不再啰嗦,有了前面的基础就太容易理解了。 为何是@Scope域代理就不用处理? 要解释好这个原因,和@Scope代理方式的原理知识强相关。限于篇幅,本文就先卖个关子~ 关于@Scope我个人觉得足够用5篇以上文章专题讲解,虽然在Spring Framework里使用得比较少,但是在理解Spirng Cloud的自定义扩展实现上显得非常非常有必要,所以你可关注我公众号,会近期推出相关专栏的。 总结 关于Spring配置类这个专栏内容,讲解到这就完成99%了,毫不客气的说关于此部分知识真正可以实现“横扫千军”,据我了解没有解决不了的问题了。 当然还剩下1%,那自然是缺少一篇总结篇喽:在下一篇总结篇里,我会用图文并茂的方式对Spring配置类相关内容的执行流程进行总结,目的是让你快速掌握,应付面试嘛。 本文将近2万字,手真的很累,如果对你有帮助,帮点个在看哈。最主要的是:关注我的公众号,后期推出的专栏都会很精彩...... 关注A哥 Author A哥(YourBatman) 个人站点 www.yourbatman.cn E-mail yourbatman@qq.com 微 信 fsx641385712 活跃平台 公众号 BAT的乌托邦(ID:BAT-utopia) 知识星球 BAT的乌托邦 每日文章推荐 每日文章推荐
专注Java领域分享、成长,拒绝浅尝辄止。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。 前言 各位小伙伴大家好,我是A哥。这是继上篇文章:真懂Spring的@Configuration配置类?你可能自我感觉太良好 的原理/源码解释篇。按照本公众号的定位,原理一般跑不了,虽然很枯燥,但还得做,毕竟做难事必有所得,真的掌握了才有底气谈涨薪嘛。 Tips:鉴于经常有些同学无法区分某个功能/某项能力属于Spring Framework的还是Spring Boot,你可以参考文章里的【版本约定】目录,那里会说明本文的版本依赖,也就是功能所属喽。比如本文内容它就属于Spring Framework,和Spring Boot木有关系。 版本约定 本文内容若没做特殊说明,均基于以下版本: JDK:1.8 Spring Framework:5.2.2.RELEASE 正文 Spring的IoC就像个“大熔炉”,什么都当作Bean放在里面。然而,虽然它们都放在了一起,但是实际在功能上是有区别的,比如我们熟悉的BeanPostProcessor就属于后置处理器功能的Bean,还有本文要讨论的@Configuration配置Bean也属于一种特殊的组件。 判断一个Bean是否是Bean的后置处理器很方便,只需看它是否实现了BeanPostProcessor接口即可;那么如何去确定一个Bean是否是@Configuration配置Bean呢?若是,如何区分是Full模式还是Lite模式呢?这便就是本文将要讨论的内容。 如何判断一个组件是否是@Configuration配置? 首先需要明确:@Configuration配置前提必须是IoC管理的一个组件(也就是常说的Bean)。Spring使用BeanDefinitionRegistry注册中心管理着所有的Bean定义信息,那么对于这些Bean信息哪些属于@Configuration配置呢,这是需要甄选出来的。 判断一个Bean是否是@Configuration配置类这个逻辑统一交由ConfigurationClassUtils这个工具类去完成。 ConfigurationClassUtils工具类 见名之意,它是和配置有关的一个工具类,提供几个静态工具方法供以使用。它是Spring 3.1新增,对于它的作用,官方给的解释是:用于标识@Configuration类的实用程序(Utilities)。它主要提供了一个方法:checkConfigurationClassCandidate()用于检查给定的Bean定义是否是配置类的候选对象(或者在配置/组件类中声明的嵌套组件类),并做相应的标记。 checkConfigurationClassCandidate() 它是一个public static工具方法,用于判断某个Bean定义是否是@Configuration配置。 ConfigurationClassUtils: public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { ... // 根据Bean定义信息,拿到器对应的注解元数据 AnnotationMetadata metadata = xxx; ... // 根据注解元数据判断该Bean定义是否是配置类。若是:那是Full模式还是Lite模式 Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } ... // 到这。它肯定是一个完整配置(Full or Lite) 这里进一步把@Order排序值放上去 Integer order = getOrder(metadata); if (order != null) { beanDef.setAttribute(ORDER_ATTRIBUTE, order); } return true; } 步骤总结: 根据Bean定义信息解析成为一个注解元数据对象AnnotationMetadata metadata 可能是个AnnotatedBeanDefinition,也可能是个StandardAnnotationMetadata 根据注解元数据metadata判断是否是个@Configuration配置类,有如下三种可能case: 标注有@Configuration注解并且该注解的proxyBeanMethods = false,那么mark一下它是Full模式的配置。否则进入下一步判断 标注有@Configuration注解或者符合Lite模式的条件(上文有说一共有5种可能是Lite模式,源码处在isConfigurationCandidate(metadata)这个方法里表述),那么mark一下它是Lite模式的配置。否则进入下一步判断 不是配置类,并且返回结果return false 能进行到这一步,说明该Bean肯定是个配置类了(Full模式或者Lite模式),那就取出其@Order值(若有的话),然后mark进Bean定义里面去 这个mark动作很有意义:后面判断一个配置类是Full模式还是Lite模式,甚至判断它是否是个配置类均可通过beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE)这样完成判断。 方法使用处 知晓了checkConfigurationClassCandidate()能够判断一个Bean(定义)是否是一个配置类,那么它在什么时候会被使用呢?通过查找可以发现它被如下两处使用到: 使用处:ConfigurationClassPostProcessor.processConfigBeanDefinitions()处理配置Bean定义阶段。 ConfigurationClassPostProcessor: public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { // 拿出当前所有的Bean定义信息,一个个的检查是否是配置类 String[] candidateNames = registry.getBeanDefinitionNames(); for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } // 如果该Bean定义不是配置类,那就继续判断一次它是否是配置类,若是就加入结果集合里 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } ... } ConfigurationClassPostProcessor是个BeanDefinitionRegistryPostProcessor,会在BeanFactory 准备好后执行生命周期方法。因此自然而然的,checkConfigurationClassCandidate()会在此阶段调用,用于区分出哪些是配置Bean。 值得注意的是:ConfigurationClassPostProcessor的执行时期是非常早期的(BeanFactory准备好后就执行嘛),这个时候容器内的Bean定义很少。这个时候只有主配置类才被注册了进来,那些想通过@ComponentScan扫进来的配置类都还没到“时间”,这个时间节点很重要,请注意区分。为了方便你理解,我分别把Spring和Spring Boot在此阶段的Bean定义信息截图展示如下: 以上是Spring环境,对应代码为: new AnnotationConfigApplicationContext(AppConfig.class); 以上是Spring Boot环境,对应代码为: @SpringBootApplication public class Boot2Demo1Application { public static void main(String[] args) { SpringApplication.run(Boot2Demo1Application.class, args); } } 相比之下,Spring Boot里多了internalCachingMetadataReaderFactory这个Bean定义。原因是SB定义了一个CachingMetadataReaderFactoryPostProcessor把它放进去的,由于此Processor也是个BeanDefinitionRegistryPostProcessor并且order值为Ordered.HIGHEST_PRECEDENCE,所以它会优先于ConfigurationClassPostProcessor执行把它注册进去~ 使用处:ConfigurationClassParser.doProcessConfigurationClass() 解析 @Configuration配置类阶段。所处的大阶段同上使用处,仍旧是ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry()阶段 ConfigurationClassParser: @Nullable protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { ... // 先解析nested内部类(内部类会存在@Bean方法嘛~) ... // 解析@PropertySource资源,加入到environment环境 ... // 解析@ComponentScan注解,把组件扫描进来 scannedBeanDefinitions = ComponentScanAnnotationParser.parse(componentScan, ...); // 把扫描到的Bean定义信息依旧需要一个个的判断,是否是配置类 // 若是配置类,就继续当作一个@Configuration配置类来解析parse() 递归嘛 for (BeanDefinitionHolder holder : scannedBeanDefinitions) { ... if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } ... // 解析@Import注解 ... // 解析@ImportResource注解 ... // 解析当前配置里配置的@Bean方法 ... // 解析接口默认方法(因为配置类可能实现接口,然后接口默认方法可能标注有@Bean ) ... // 处理父类(递归,直到父类为java.打头的为止) } 这个方法是Spring对配置类解析的最核心步骤,通过它顺带也能够解答你的疑惑了吧:为何你仅需在类上标注一个@Configuration注解即可让它成为一个配置类?因为被Scan扫描进去了嘛~ 通过以上两个使用处的分析和对比,对于@Configuration配置类的理解,你至少应该掌握了如下讯息: @Configuration配置类肯定是个组件,存在于IoC容器里 @Configuration配置类是有主次之分的,主配置类是驱动整个程序的入口,可以是一个,也可以是多个(若存在多个,支持使用@Order排序) 我们平时一般只书写次配置类(而且一般写多个),它一般是借助主配置类的@ComponentScan能力完成加载进而解析的(当然也可能是@Import、又或是被其它次配置类驱动的) 配置类可以存在嵌套(如内部类),继承,实现接口等特性 聊完了最为重要的checkConfigurationClassCandidate()方法,当然还有必要看看ConfigurationClassUtils的另一个工具方法isConfigurationCandidate()。 isConfigurationCandidate() 它是一个public static工具方法,通过给定的注解元数据信息来判断它是否是一个Configuration。 ConfigurationClassUtils: static { candidateIndicators.add(Component.class.getName()); candidateIndicators.add(ComponentScan.class.getName()); candidateIndicators.add(Import.class.getName()); candidateIndicators.add(ImportResource.class.getName()); } public static boolean isConfigurationCandidate(AnnotationMetadata metadata) { // 不考虑接口 or 注解 说明:注解的话也是一种“特殊”的接口哦 if (metadata.isInterface()) { return false; } // 只要该类上标注有以上4个注解任意一个,都算配置类 for (String indicator : candidateIndicators) { if (metadata.isAnnotated(indicator)) { return true; } } // 若一个注解都没标注,那就看有木有@Bean方法 若有那也算配置类 return metadata.hasAnnotatedMethods(Bean.class.getName()); } 步骤总结: 若是接口类型(含注解类型),直接不予考虑,返回false。否则继续判断 若此类上标注有@Component、@ComponentScan、@Import、@ImportResource任意一个注解,就判断成功返回true。否则继续判断 到此步,就说明此类上没有标注任何注解。若存在@Bean方法,返回true,否则返回false。 需要特别特别特别注意的是:此方法它的并不考虑@Configuration注解,是“轻量级”判断,这是它和checkConfigurationClassCandidate()方法的最主要区别。当然,后者依赖于前者,依赖它来根据注解元数据判断是否是Lite模式的配置。 Spring 5.2.0版本变化说明 因为本文的讲解和代码均是基于Spring 5.2.2.RELEASE的,而并不是所有小伙伴都会用到这么新的版本。关于此部分的实现,以Spring 5.2.0版本为分界线实现上有些许差异,所以在此处做出说明。 proxyBeanMethods属性的作用 proxyBeanMethods属性是Spring 5.2.0版本为@Configuration注解新增加的一个属性: @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor(annotation = Component.class) String value() default ""; // @since 5.2 boolean proxyBeanMethods() default true; } 它的作用是:是否允许代理@Bean方法。说白了:决定此配置使用Full模式还是Lite模式。为了保持向下兼容,proxyBeanMethods的默认值是true,使用Full模式配置。 Spring 5.2提出了这个属性项,是期望你在已经了解了它的作用之后,显示的把它置为false的,因为在云原生将要到来的今天,启动速度方面Spring一直在做着努力,也希望你能配合嘛。这不Spring Boot就“配合”得很好,它在2.2.0版本(依赖于Spring 5.2.0)起就把它的所有的自动配置类的此属性改为了false,即@Configuration(proxyBeanMethods = false)。 Full模式/Lite模式实现上的差异 由于Spring 5.2.0新增了proxyBeanMethods属性来控制模式,因此实现上也有些许诧异,请各位注意甄别: Spring 5.2.0+版本判断实现: ConfigurationClassUtils: Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } Spring 5.2.0-版本判断实现: ConfigurationClassUtils: if (isFullConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (isLiteConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } 思考题? 既然isConfigurationCandidate()判断方法是为checkConfigurationClassCandidate()服务,那Spring为何也把它设计为public static呢? ConfigurationClassUtils里还存在对@Order顺序的解析方法,不是说Spring的Bean是无序的吗?这又如何理解呢? 总结 本文作为上篇文章的续篇,解释了@Configuration配置的Full模式和Lite模式的判断原理,同时顺带的也介绍了什么叫主配置配和次配置类,这个概念(虽然官方并不这么叫)对你理解Spring Framework是非常有帮助的。如果你使用是基于Spring 5.2.0+的版本,在了解了这两篇文章内容的基础上,建议你的配置类均采用Lite模式去做,即显示设置proxyBeanMethods = false。 另外关于此部分内容,有些更为感兴趣的小伙伴问到:为什么Full模式下通过方法调用指向的仍旧是原来的Bean,保证了只会执行一次呢?开启的是Full模式这只是表象原因,想要回答此问题需要涉及到CGLIB增强实现的深水区内容,为了满足这些好奇(好学)的娃子,计划会在下篇文章继续再拿一篇专程讲解(预计篇幅不短,万字以上),你可订阅我的公众号持续保持关注。 关注A哥 原创不易,码字更不易。关注A哥的公众号【BAT的乌托邦】,开启有深度的专栏式学习,拒绝浅尝辄止 专栏式学习,我们是认真的(关注公众号回复“知识星球”领券后再轻装入驻) 加A哥好友(fsx641385712),备注“Java入群”邀你进入【Java高工、架构师】系列纯纯纯技术群
当大潮退去,才知道谁在裸泳。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。 前言 各位小伙伴大家好,我是A哥。这是一篇“插队”进来的文章,源于我公众号下面的这句评论:官方管这两种模式分别叫:Full @Configuration和lite @Bean mode,口语上我习惯把它称为Spring配置的Full模式和Lite模式更易沟通。 的确,我很简单的“调研”了一下,知晓Spring配置中Lite模式和Full模式的几乎没有(或者说真的很少吧)。按照我之前的理论,大多人都不知道的技术(知识点)那肯定是不流行的。但是:不流行不代表不重要,不流行不代表不值钱,毕竟高薪往往只有少数人才能拥有。 什么OPP、OOP、AOP编程,其实我最喜欢的和推崇的是面向工资编程。当然前提是够硬(收回你邪恶的笑容),没有金刚钻,不揽瓷器活。 听我这么一忽悠,是不是对这块内容还饶有兴味了,这不它来了嘛。 版本约定 本文内容若没做特殊说明,均基于以下版本: JDK:1.8 Spring Framework:5.2.2.RELEASE 正文 最初的Spring只支持xml方式配置Bean,从Spring 3.0起支持了一种更优的方式:基于Java类的配置方式,这一下子让我们Javaer可以从标签语法里解放了出来。毕竟作为Java程序员,我们擅长的是写Java类,而非用标签语言去写xml文件。 我对Spring配置的Full/Lite模式的关注和记忆深刻,源自于一个小小故事:某一年我在看公司的项目时发现,数据源配置类里有如下一段配置代码: @Configuration public class DataSourceConfig { ... @Bean public DataSource dataSource() { ... return dataSource; } @Bean(name = "transactionManager") public DataSourceTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } ... } 作为当时还是Java萌新的我,非常的费解。自然的对此段代码产生了较大的好奇(其实是质疑):在准备DataSourceTransactionManager这个Bean时调用了dataSource()方法,根据我“非常扎实”的JavaSE基础知识,它肯定会重新走一遍dataSource()方法,从而产生一个新的数据源实例,那么你的事务管理器管理的不就是一个“全新数据源”麽?谈何事务呢? 为了验证我的猜想,我把断点打到dataSource()方法内部开始调试,但让我“失望”的是:此方法并没有执行两次。这在当时是震惊了我的,甚至一度怀疑自己引以为豪的Java基础了。所以我四处询问,希望得到一个“解释”,但奈何,问了好几圈,那会没有一人能给我一个合理的说法,只知道那么用是没有问题的。 很明显,现在再回头来看当时的这个质疑是显得有些“无知”的,这个“难题”困扰了我很久,直到我前2年开始深度研究Spring源码才让此难题迎刃而解,当时那种豁然开朗的感觉真好呀。 基本概念 关于配置类的核心概念,在这里先予以解释。 @Configuration和@Bean Spring新的配置体系中最为重要的构件是:@Configuration标注的类,@Bean标注的方法。 // @since 3.0 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor(annotation = Component.class) String value() default ""; // @since 5.2 boolean proxyBeanMethods() default true; } 用@Configuration注解标注的类表明其主要目的是作为bean定义的源。此外,@Configuration类允许通过调用同一类中的其他@Bean method方法来定义bean之间的依赖关系(下有详解)。 // @since 3.0 @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Bean { @AliasFor("name") String[] value() default {}; @AliasFor("value") String[] name() default {}; @Deprecated Autowire autowire() default Autowire.NO; // @since 5.1 boolean autowireCandidate() default true; String initMethod() default ""; String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; } @Bean注解标注在方法上,用于指示方法实例化、配置和初始化要由Spring IoC容器管理的新对象。对于熟悉Spring的<beans/> XML配置的人来说,@Bean注解的作用与<bean/>元素相同。您可以对任何Spring的@Component组件使用@Bean注释的方法代替(注意:这是理论上,实际上比如使用@Controller标注的组件就不能直接使用它代替)。 需要注意的是,通常来说,我们均会把@Bean标注的方法写在@Configuration标注的类里面来配合使用。 简单粗暴理解:@Configuration标注的类等同于一个xml文件,@Bean标注的方法等同于xml文件里的一个<bean/> 标签 使用举例 @Configuration public class AppConfig { @Bean public User user(){ User user = new User(); user.setName("A哥"); user.setAge(18); return user; } } public class Application { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); User user = context.getBean(User.class); System.out.println(user.getClass()); System.out.println(user); } } 输出: class com.yourbatman.fullliteconfig.User User{name='A哥', age=18} Full模式和Lite模式 Full模式和Lite模式均是针对于Spring配置类而言的,和xml配置文件无关。值得注意的是:判断是Full模式 or Lite模式的前提是,首先你得是个容器组件。至于一个实例是如何“晋升”成为容器组件的,可以用注解也可以没有注解,本文就不展开讨论了,这属于Spring的基础知识。 Lite模式 当@Bean方法在没有使用@Configuration注释的类中声明时,它们被称为在Lite模式下处理。它包括:在@Component中声明的@Bean方法,甚至只是在一个非常普通的类中声明的Bean方法,都被认为是Lite版的配置类。@Bean方法是一种通用的工厂方法(factory-method)机制。 和Full模式的@Configuration不同,Lite模式的@Bean方法不能声明Bean之间的依赖关系。因此,这样的@Bean方法不应该调用其他@Bean方法。每个这样的方法实际上只是一个特定Bean引用的工厂方法(factory-method),没有任何特殊的运行时语义。 何时为Lite模式 官方定义为:在没有标注@Configuration的类里面有@Bean方法就称为Lite模式的配置。透过源码再看这个定义是不完全正确的,而应该是有如下case均认为是Lite模式的配置类: 类上标注有@Component注解 类上标注有@ComponentScan注解 类上标注有@Import注解 类上标注有@ImportResource注解 若类上没有任何注解,但类内存在@Bean方法 以上case的前提均是类上没有被标注@Configuration,在Spring 5.2之后新增了一种case也算作Lite模式: 标注有@Configuration(proxyBeanMethods = false),注意:此值默认是true哦,需要显示改为false才算是Lite模式 细心的你会发现,自Spring5.2(对应Spring Boot 2.2.0)开始,内置的几乎所有的@Configuration配置类都被修改为了@Configuration(proxyBeanMethods = false),目的何为?答:以此来降低启动时间,为Cloud Native继续做准备。 优缺点 优点: 运行时不再需要给对应类生成CGLIB子类,提高了运行性能,降低了启动时间 可以该配置类当作一个普通类使用喽:也就是说@Bean方法 可以是private、可以是final 缺点: 不能声明@Bean之间的依赖,也就是说不能通过方法调用来依赖其它Bean (其实这个缺点还好,很容易用其它方式“弥补”,比如:把依赖Bean放进方法入参里即可) 代码示例 主配置类: @ComponentScan("com.yourbatman.fullliteconfig.liteconfig") @Configuration public class AppConfig { } 准备一个Lite模式的配置: @Component // @Configuration(proxyBeanMethods = false) // 这样也是Lite模式 public class LiteConfig { @Bean public User user() { User user = new User(); user.setName("A哥-lite"); user.setAge(18); return user; } @Bean private final User user2() { User user = new User(); user.setName("A哥-lite2"); user.setAge(18); // 模拟依赖于user实例 看看是否是同一实例 System.out.println(System.identityHashCode(user())); System.out.println(System.identityHashCode(user())); return user; } public static class InnerConfig { @Bean // private final User userInner() { // 只在lite模式下才好使 public User userInner() { User user = new User(); user.setName("A哥-lite-inner"); user.setAge(18); return user; } } } 测试用例: public class Application { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // 配置类情况 System.out.println(context.getBean(LiteConfig.class).getClass()); System.out.println(context.getBean(LiteConfig.InnerConfig.class).getClass()); String[] beanNames = context.getBeanNamesForType(User.class); for (String beanName : beanNames) { User user = context.getBean(beanName, User.class); System.out.println("beanName:" + beanName); System.out.println(user.getClass()); System.out.println(user); System.out.println("------------------------"); } } } 结果输出: 1100767002 313540687 class com.yourbatman.fullliteconfig.liteconfig.LiteConfig class com.yourbatman.fullliteconfig.liteconfig.LiteConfig$InnerConfig beanName:userInner class com.yourbatman.fullliteconfig.User User{name='A哥-lite-inner', age=18} ------------------------ beanName:user class com.yourbatman.fullliteconfig.User User{name='A哥-lite', age=18} ------------------------ beanName:user2 class com.yourbatman.fullliteconfig.User User{name='A哥-lite2', age=18} ------------------------ 小总结 该模式下,配置类本身不会被CGLIB增强,放进IoC容器内的就是本尊 该模式下,对于内部类是没有限制的:可以是Full模式或者Lite模式 该模式下,配置类内部不能通过方法调用来处理依赖,否则每次生成的都是一个新实例而并非IoC容器内的单例 该模式下,配置类就是一普通类嘛,所以@Bean方法可以使用private/final等进行修饰(static自然也是阔仪的) Full模式 在常见的场景中,@Bean方法都会在标注有@Configuration的类中声明,以确保总是使用“Full模式”,这么一来,交叉方法引用会被重定向到容器的生命周期管理,所以就可以更方便的管理Bean依赖。 何时为Full模式 标注有@Configuration注解的类被称为full模式的配置类。自Spring5.2后这句话改为下面这样我觉得更为精确些: 标注有@Configuration或者@Configuration(proxyBeanMethods = true)的类被称为Full模式的配置类 (当然喽,proxyBeanMethods属性的默认值是true,所以一般需要Full模式我们只需要标个注解即可) 优缺点 优点: 可以支持通过常规Java调用相同类的@Bean方法而保证是容器内的Bean,这有效规避了在“Lite模式”下操作时难以跟踪的细微错误。特别对于萌新程序员,这个特点很有意义 缺点: 运行时会给该类生成一个CGLIB子类放进容器,有一定的性能、时间开销(这个开销在Spring Boot这种拥有大量配置类的情况下是不容忽视的,这也是为何Spring 5.2新增了proxyBeanMethods属性的最直接原因) 正因为被代理了,所以@Bean方法 不可以是private、不可以是final 代码示例 主配置: @ComponentScan("com.yourbatman.fullliteconfig.fullconfig") @Configuration public class AppConfig { } 准备一个Full模式的配置: @Configuration public class FullConfig { @Bean public User user() { User user = new User(); user.setName("A哥-lite"); user.setAge(18); return user; } @Bean protected User user2() { User user = new User(); user.setName("A哥-lite2"); user.setAge(18); // 模拟依赖于user实例 看看是否是同一实例 System.out.println(System.identityHashCode(user())); System.out.println(System.identityHashCode(user())); return user; } public static class InnerConfig { @Bean // private final User userInner() { // 只在lite模式下才好使 public User userInner() { User user = new User(); user.setName("A哥-lite-inner"); user.setAge(18); return user; } } } 测试用例: public class Application { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // 配置类情况 System.out.println(context.getBean(FullConfig.class).getClass()); System.out.println(context.getBean(FullConfig.InnerConfig.class).getClass()); String[] beanNames = context.getBeanNamesForType(User.class); for (String beanName : beanNames) { User user = context.getBean(beanName, User.class); System.out.println("beanName:" + beanName); System.out.println(user.getClass()); System.out.println(user); System.out.println("------------------------"); } } } 结果输出: 550668305 550668305 class com.yourbatman.fullliteconfig.fullconfig.FullConfig$$EnhancerBySpringCGLIB$$70a94a63 class com.yourbatman.fullliteconfig.fullconfig.FullConfig$InnerConfig beanName:userInner class com.yourbatman.fullliteconfig.User User{name='A哥-lite-inner', age=18} ------------------------ beanName:user class com.yourbatman.fullliteconfig.User User{name='A哥-lite', age=18} ------------------------ beanName:user2 class com.yourbatman.fullliteconfig.User User{name='A哥-lite2', age=18} ------------------------ 小总结 该模式下,配置类会被CGLIB增强(生成代理对象),放进IoC容器内的是代理 该模式下,对于内部类是没有限制的:可以是Full模式或者Lite模式 该模式下,配置类内部可以通过方法调用来处理依赖,并且能够保证是同一个实例,都指向IoC内的那个单例 该模式下,@Bean方法不能被private/final等进行修饰(很简单,因为方法需要被复写嘛,所以不能私有和final。defualt/protected/public都可以哦),否则启动报错(其实IDEA编译器在编译器就提示可以提示你了): Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Bean method 'user2' must not be private or final; change the method's modifiers to continue Offending resource: class path resource [com/yourbatman/fullliteconfig/fullconfig/FullConfig.class] at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:72) at org.springframework.context.annotation.BeanMethod.validate(BeanMethod.java:50) at org.springframework.context.annotation.ConfigurationClass.validate(ConfigurationClass.java:220) at org.springframework.context.annotation.ConfigurationClassParser.validate(ConfigurationClassParser.java:211) at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:326) at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:242) at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275) at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95) at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532) at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89) at com.yourbatman.fullliteconfig.Application.main(Application.java:11) 使用建议 了解了Spring配置类的Full模式和Lite模式,那么在工作中我该如何使用呢?这里A哥给出使用建议,仅供参考: 如果是在公司的业务功能/服务上做开发,使用Full模式 如果你是个容器开发者,或者你在开发中间件、通用组件等,那么使用Lite模式是一种更被推荐的方式,它对Cloud Native更为友好 思考题? 通过new AnnotationConfigApplicationContext(AppConfig.class)直接放进去的类,它会成为一个IoC的组件吗?若会,那么它是Full模式 or Lite模式呢?是个固定的结果还是也和其标注的注解有关呢? 本思考题不难,自己试验一把便知,建议多动手~ 总结 本文结合代码示例阐述了Spring配置中Full模式和Lite模式,以及各自的定义和优缺点。对于一般的小伙伴,掌握本文就够用了,并且足够你面试中吹x。但A哥系列文章一般不止于“表面”嘛,下篇文章将从原理层面告诉你Spring是如何来巧妙的处理这两种模式的,特别是会结合Spring 5.2.0新特性,以及对比Spring 5.2.0的实现和之前版本有何不同,你课订阅我的公众号保持关注。 关注A哥 原创不易,码字更不易。关注A哥的公众号【BAT的乌托邦】,开启有深度的专栏式学习,拒绝浅尝辄止 专栏式学习,我们是认真的(关注公众号回复“知识星球”领券后再轻装入驻) 加A哥好友(fsx641385712),备注“Java入群”邀你进入【Java高工、架构师】系列纯纯纯技术群
一个人的价值体现在能够帮助多少人。自己编码好,价值能得到很好的体现。若你做出来的东西能够帮助别人开发,大大减少开发的时间,那就功德无量。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。 @TOC 前言 各位小伙伴大家好,我是A哥。Spring Boot是Spring家族具有划时代意义的一款产品,它发展自Spring Framework却又高于它,这种高于主要表现在其最重要的三大特性,而相较于这三大特性中更为重要的便是Spring Boot的自动配置(AutoConfiguration)。与其说是自动,倒不如说是“智能”,该框架看起来好像“更聪明”了。因此它也顺理成章的成为了构建微服务的基础设施,稳坐第一宝座。 生活之道,在于取舍。编程何尝不是,任何决定都会是一把双刃剑,Spring Boot的自动配置解决了Spring Framework使用起来的众多痛点,让开发效率可以得到指数级提升(想一想,这不就是功德无量吗?)。成也萧何败也萧何,也正是因为它的太智能,倘若出了问题就会让程序员两眼一抹黑,无从下手。 瑕不掩瑜,Spring Boot前进的步伐浩浩荡荡,学就完了 这不,我就在前几天收到一个“求助”,希望使用@AutoConfigureBefore来控制配置的顺序,但并未能如愿。本文就针对这个场景case稍作展开,讨论下使用@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder三大注解控制自动配置执行顺序的正确姿势。 提示:Spring Boot的自动配置是通过@EnableAutoConfiguration注解驱动的,默认是开启状态。你也可以通过spring.boot.enableautoconfiguration = false来关闭它,回退到Spring Framework时代。显然这不是本文需要讨论的内容~ 正文 本文将要聊的重点是Spring Boot自动配置 + 顺序控制,自动配置大家都耳熟能详,那么“首当其冲”就是知晓这个问题:配置类的执行为何需要控制顺序? 配置类为何需要顺序? 我们已经知道Spring容器它对Bean的初始化是无序的,我们并不能想当然的通过@Order注解来控制其执行顺序。一般来说,对于容器内普通的Bean我们只需要关注依赖关系即可,而并不需要关心其绝对的顺序,而依赖关系的管理Spring的是做得很好的,这不连循环依赖它都可以搞定麽。 @Configuration配置类它也是一个Bean,但对于配置类来说,某些场景下的执行顺序是必须的,是需要得到保证的。比如很典型的一个非A即B的case:若容器内已经存在A了,就不要再把B放进来。这种case即使用中文理解,就能知道对A的“判断”必须要放在B的前面,否则可能导致程序出问题。 那么针对于配置的执行顺序,传统Spring和Spring Boot下各自是如何处理的,表现如何呢? Spring下控制配置执行顺序 在传统的Spring Framework里,一个@Configuration注解标注的类就代表一个配置类,当存在多个@Configuration时,他们的执行顺序是由使用者靠手动指定的,就像这样: // 手动控制Config1 Config2的顺序 ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class, Config2.class); 当然,你可能就疑问了说:即使在传统Spirng里,我也从没有自己使用过AnnotationConfigApplicationContext来显示加载配置啊,都是使用@Configuration定义好配置类后,点击Run一把唆的。没错,那是因为你是在web环境下使用Spring,IoC容器是借助web容器(如Tomcat等)来驱动的,Spring对此部分封装得非常好,所以做到了对使用者几乎无感知。 关于这部分的内容,此处就不深究了,毕竟本文重点不在这嘛。但可以给出给小结论:@Configuration配置被加载进容器的方式大体上可分为两种: 手动。构建ApplicationContext 时由构建者手动传入,可手动控制顺序 自动。被@ComponentScan自动扫描进去,无法控制顺序 绝大多数情况下我们都是使用自动的方式,所以在Spring下对配置的顺序并无感知。其实这也是需求驱使,因为在传统Spring下我们并无此需求,所以对它无感是合乎逻辑的。另说一句,虽然我们并不能控制Bean的顺序,但是我们是可以干涉它的,比如:控制依赖关系、提升优先级、“间接”控制执行顺序...当然喽这是后面文章的内容,敬请关注。 Spring Boot下控制配置执行顺序 Spring Boot下对自动配置的管理对比于Spring它就是黑盒,它会根据当前容器内的情况来动态的判断自动配置类的加载与否、以及加载的顺序,所以可以说:Spring Boot的自动配置它对顺序是有强要求的。需求驱使,Spring Boot给我们提供了@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder(下面统称这三个注解为“三大注解”)这三个注解来帮我们解决这种诉求。 需要注意的是:三大注解是Spring Boot提供的而非Spring Framework。其中前两个是1.0.0就有了,@AutoConfigureOrder属于1.3.0版本新增,表示绝对顺序(数字越小,优先级越高)。另外,这几个注解并不互斥,可以同时标注在同一个@Configuration自动配置类上。 Spring Boot内置的控制配置顺序举例 为方便大家理解,我列出一个Spring Boot它自己的使用作为示例学一学。以大家最为熟悉的WebMvc的自动配置场景为例: @Configuration(proxyBeanMethods = false) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { ... } @Configuration(proxyBeanMethods = false) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { ... } @Configuration(proxyBeanMethods = false) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) public class ServletWebServerFactoryAutoConfiguration { ... } 这几个配置是WebMVC的核心配置,他们之间是有顺序关系的: WebMvcAutoConfiguration被加载的前提是:DispatcherServletAutoConfiguration、TaskExecutionAutoConfiguration、ValidationAutoConfiguration这三个哥们都已经完成初始化 DispatcherServletAutoConfiguration被加载的前提是:ServletWebServerFactoryAutoConfiguration已经完成初始化 ServletWebServerFactoryAutoConfiguration被加载的前提是:@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)最高优先级,也就是说它无其它依赖,希望自己是最先被初始化的 当碰到多个配置都是最高优先级的时候,且互相之前没有关系的话,顺序也是不定的。但若互相之间存在依赖关系(如本利的DispatcherServletAutoConfiguration和ServletWebServerFactoryAutoConfiguration),那就按照相对顺序走 在WebMvcAutoConfiguration加载后,在它之后其实还有很多配置会尝试执行,例如: @AutoConfigureAfter(WebMvcAutoConfiguration.class) class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration { ... } @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class GroovyTemplateAutoConfiguration { ... } @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { ... } @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class LifecycleMvcEndpointAutoConfiguration { ... } 这些都很容易理解:如果都不是Web环境,加载一些模版引擎的并无必要嘛。 三大注解使用的误区(重要) 根据我的切身体会,针对这三大注解,实在有太多人把它误用了,想用但是用了却又不生效,于是就容易触发一波“骂街”操作,其实这也是我书写本文的最大动力所在:纠正你的错误使用,告诉你正确姿势。 错误使用示例 我见到的非常多的小伙伴这么来使用三大注解:我这里使用“伪代码”进行模拟 @Configuration public class B_ParentConfig { B_ParentConfig() { System.out.println("配置类ParentConfig构造器被执行..."); } } @Configuration public class A_SonConfig { A_SonConfig() { System.out.println("配置类SonConfig构造器被执行..."); } } @Configuration public class C_DemoConfig { public C_DemoConfig(){ System.out.println("我是被自动扫描的配置,初始化啦...."); } } @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args).close(); } } 通过名称能知道我想要的达到的效果是:ParentConfig先加载,SonConfig后加载。(DemoConfig作为一个参考配置,作为日志参考使用即可) 启动应用,控制台打印: 配置类SonConfig构造器被执行... 配置类ParentConfig构造器被执行... 我是被自动扫描的配置,初始化啦.... Son优先于Parent被加载了,这明显不符合要求。因此,我看到很多小伙伴就这么干: @AutoConfigureBefore(A_SonConfig.class) @Configuration public class B_ParentConfig { B_ParentConfig() { System.out.println("配置类ParentConfig构造器被执行..."); } } 通过@AutoConfigureBefore控制,表示在A_SonConfig之前执行此配置。语义层面上看,貌似没有任何问题,再次启动应用: 配置类SonConfig构造器被执行... 配置类ParentConfig构造器被执行... 我是被自动扫描的配置,初始化啦.... what a fuck。看到没,我没骗你吧,骂街了骂街了 竟然没生效?代码不会骗人,@AutoConfigureBefore的语义也没有问题,而是你使用的姿势不对,下面我会给你正确姿势。 三大注解使用的正确姿势 针对以上case,要想达到预期效果,正确姿势只需要下面两步: 把A_SonConfig和B_ParentConfig挪动到Application扫描不到的包内,切记:一定且必须是扫描不到的包内 当前工程里增加配置META-INF/spring.factories,内容为(配置里Son和Parent前后顺序对结果无影响): org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.fsx.autoconfig.A_SonConfig,com.fsx.autoconfig.B_ParentConfig 再次启动应用看看,打印输出: 我是被自动扫描的配置,初始化啦.... 配置类ParentConfig构造器被执行... 配置类SonConfig构造器被执行... 完美。符合预期,Parent终于在Son之前完成了初始化,也就是说我们的@AutoConfigureBefore注解生效了。 使用细节注意事项 针对此使用姿势,虽然很正确,并不是完全没有“副作用”的,有如下细节平时也需要引起注意: 若你不用@AutoConfigureBefore这个注解,单单就想依赖于spring.factories里的先后顺序的来控制实际的加载顺序,答案是不可以,控制不了 例子中有个小细节:我每次都故意输出了我是被自动扫描的配置,初始化啦....这句话,可以发现被扫描进去配置实例化是在它前面(见错误示例),而通过spring.factories方式进去是在它的后面(见正确姿势) 从这个小细节可以衍生得到结论:Spring Boot的自动配置均是通过spring.factories来指定的,它的优先级最低(执行时机是最晚的);通过扫描进来的一般都是你自己自定义的配置类,所以优先级是最高的,肯定在自动配置之前加载 从这你应该学到:若你要指定扫描的包名,请千万不要扫描到形如org.springframework这种包名,否则“天下大乱”(当然喽为了防止这种情况出现,Spring Boot做了容错的。它有一个类专门检测这个case防止你配置错了,具体参见ComponentScanPackageCheck默认实现) 请尽量不要让自动配置类既被扫描到了,又放在spring.factories配置了,否则后者会覆盖前者,很容易造成莫名其妙的错误 小总结,对于三大注解的正确使用姿势是应该是:请使用在你的自动配置里(一般是你自定义starter时使用),而不是使用在你业务工程中的@Configuration里,因为那会毫无效果。 三大注解解析时机浅析 为了更好的辅助理解,加强记忆,本文将这三大注解解析时机简要的絮叨一下,知道了它被解析的时机,自然就很好解释为何你那么写是无效的喽。 这三个注解的解析都是交给AutoConfigurationSorter来排序、处理的,做法类似于AnnotationAwareOrderComparator去解析排序@Order注解。核心代码如下: class AutoConfigurationSorter { // 唯一给外部调用的方法:返回排序好的Names,因此返回的是个List嘛(ArrayList) List<String> getInPriorityOrder(Collection<String> classNames) { ... // 先按照自然顺序排一波 Collections.sort(orderedClassNames); // 在按照@AutoConfigureBefore这三个注解排一波 orderedClassNames = sortByAnnotation(classes, orderedClassNames); return orderedClassNames; } ... } 此排序器被两个地方使用到: AutoConfigurationImportSelector:Spring自动配置处理器,用于加载所有的自动配置类。它实现了DeferredImportSelector接口:这也顺便解释了为何自动配置是最后执行的原因~ AutoConfigurations:表示自动配置@Configuration类。 这个排序的“解析/排序”过程还是比较复杂的,本文点到为止,观其大意即可。你可以简单粗暴的记住结论:@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder这三个注解只能作用于自动配置类,而不能是自定义的@Configuration配置类。 总结 关于Spring Boot自动配置顺序相关的三大注解@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder就先介绍到这了,本文主要用意是为了帮助大家规范此些“常用注解”的使用,规避一些误区,端正使用姿势,避免犯错时又丈二和尚。 我看到不少文章、生产上的代码都使用错了(估计有没有效果自己的都不知道,又或者刚好歪打正着确实是在xxx后面执行而以为生效了),希望本文能帮助到你。 关注A哥 原创不易,码字更不易。关注A哥的公众号【BAT的乌托邦】,开启有深度的专栏式学习,拒绝浅尝辄止 专栏式学习,我们是认真的(关注公众号回复“知识星球”领券后再轻装入驻) 加A哥好友(fsx641385712),备注“Java入群”邀你进入【Java高工、架构师】系列纯纯纯技术群 Netflix OSS套件一站式学习驿站(Eureka、Hystrix、Ribbon、Feign、Zuul...)
学习方法之少废话:吹牛、装逼、叫大哥。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。 前言 各位小伙伴大家好,我是A哥。本文属总结性文章,对总览Spring Boot生命周期很是重要,建议点在看、转发“造福”更多小伙伴。 我最近不是在写Spring Cloud深度剖析的相关专栏麽,最近有收到小伙伴发过来一些问题,通过这段时间收集到的反馈,总结了一下有一个问题非常集中:那便是对Spring Boot应用SpringApplication的生命周期、事件的理解。有句话我不是经常挂嘴边说的麽,你对Spring Framework有多了解决定了你对Spring Boot有多了解,你对Spring Boot的了解深度又会制约你去了解Spring Cloud,一环扣一环。因此此问题反馈比较集中是在清理之中的~ 为何在Spring Boot中生命周期事件机制如此重要?缘由很简单:Spring Cloud父容器是由该生命周期事件机制来驱动的,而它仅仅是一个典型代表。Spring Cloud构建在Spring Boot之上,它在此基础上构建并添加了一些“Cloud”功能。应用程序事件ApplicationEvent以及监听ApplicationListener是Spring Framework提供的扩展点,Spring Boot对此扩展点利用得非常充分和深入,并且还衍生出了非常多“子”事件类型,甚至自成体系。从ApplicationEvent衍生出来的子事件类型非常多,例如JobExecutionEvent、RSocketServerInitializedEvent、AuditApplicationEvent... 本文并不会对每个子事件分别介绍(也并无必要),而是集中火力主攻Spring Boot最为重要的一套事件机制:SpringApplication生命周期的事件体系。 正文 本文将以SpringApplication的启动流程/生命周期各时期发出的Event事件为主线,结合每个生命周期内完成的大事记介绍,真正实现一文让你总览Spring Boot的全貌,这对你深入理解Spring Boot,以及整合进Spring Cloud都将非常重要。 为表诚意,本文一开始便把SpringApplication生命周期事件流程图附上,然后再精细化讲解各个事件的详情。 话外音:赶时间的小伙伴可以拿图走人,但不建议白嫖哟 生命周期事件流程图 版本说明: 由于不同版本、类路径下存在不同包时结果会存在差异,不指明版本的文章都是不够负责任的。因此对导包/版本情况作出如下说明: Spring Boot:2.2.2.RELEASE。有且仅导入spring-boot-starter-web和spring-boot-starter-actuator Spring Cloud:Hoxton.SR1。有且仅导入spring-cloud-context(注意:并非spring-cloud-starter,并不含有spring-cloud-commons哦) 总的来说:本例导包是非常非常“干净”的,这样在流程上才更有说服力嘛~ SpringApplicationEvent 它是和SpringApplication生命周期有关的所有事件的父类,@since 1.0.0。 public abstract class SpringApplicationEvent extends ApplicationEvent { private final String[] args; public SpringApplicationEvent(SpringApplication application, String[] args) { super(application); this.args = args; } public SpringApplication getSpringApplication() { return (SpringApplication) getSource(); } public final String[] getArgs() { return this.args; } } 它是抽象类,扩展自Spring Framwork的ApplicationEvent,确保了事件和应用实体SpringApplication产生关联(当然还有String[] args)。它有如下实现子类(7个): 每个事件都代表着SpringApplication不同生命周期所处的位置,下面分别进行讲解。 ApplicationStartingEvent:开始启动中 @since 1.5.0,并非1.0.0就有的哦。不过现在几乎没有人用1.5以下的版本了,所以可当它是标准事件。 完成的大事记 SpringApplication实例已实例化:new SpringApplication(primarySources) 它在实例化阶段完成了如下几件“大”事: 推断出应用类型webApplicationType、main方法所在类 给字段initializers赋值:拿到SPI方式配置的ApplicationContextInitializer上下文初始化器 给字段listeners赋值:拿到SPI方式配置的ApplicationListener应用监听器 - 注意:在此阶段(早期阶段)不要过多地使用它的内部状态,因为它可能在生命周期的后期被修改(话外音:使用时需谨慎) 此时,SpringApplicationRunListener已实例化:它通过SPI方式指定org.springframework.boot.SpringApplicationRunListener=org.springframework.boot.context.event.EventPublishingRunListener。 若你有自己的运行时应用监听器,使用相同方式配置上即可,均会生效 由于EventPublishingRunListener已经实例化了,因此在后续的事件发送中,均能够触发对应的监听器的执行 ==发送ApplicationStartingEvent事件,触发对应的监听器的执行== 监听此事件的监听器们 默认情况下,有4个监听器监听ApplicationStartingEvent事件: LoggingApplicationListener:@since 2.0.0。对日志系统抽象LoggingSystem执行实例化以及初始化之前的操作,默认使用的是基于Logback的LogbackLoggingSystem BackgroundPreinitializer:启动一个后台进行对一些类进行预热。如ValidationInitializer、JacksonInitializer...,因为这些组件有第一次惩罚的特点(并且首次初始化均还比较耗时),所以使用后台线程先预热效果更佳 DelegatingApplicationListener:它监听的是ApplicationEvent,而实际上只会ApplicationEnvironmentPreparedEvent到达时生效,所以此处忽略 LiquibaseServiceLocatorApplicationListener:略 总结:此事件节点结束时,SpringApplication完成了一些实例化相关的动作:本实例实例化、本实例属性赋值、日志系统实例化等。 ApplicationEnvironmentPreparedEvent:环境已准备好 @since 1.0.0。该事件节点是最为重要的一个节点之一,因为对于Spring应用来说,环境抽象Enviroment简直太重要了,它是最为基础的元数据,决定着程序的构建和走向,所以构建的时机是比较早的。 完成的大事记 封装命令行参数(main方法的args)到ApplicationArguments里面 创建出一个环境抽象实例ConfigurableEnvironment的实现类,并且填入值:Profiles配置和Properties属性,默认内容如下(注意,这只是初始状态,后面还会改变、添加属性源,实际见最后的截图): ==发送ApplicationEnvironmentPreparedEvent事件,触发对应的监听器的执行== 对环境抽象Enviroment的填值,均是由监听此事件的监听器去完成,见下面的监听器详解 bindToSpringApplication(environment):把环境属性中spring.main.xxx = xxx绑定到当前的SpringApplication实例属性上,如常用的spring.main.allow-bean-definition-overriding=true会被绑定到当前SpringApplication实例的对应属性上 监听此事件的监听器们 默认情况下,有9个监听器监听ApplicationEnvironmentPreparedEvent事件: BootstrapApplicationListener:来自SC。优先级最高,用于启动/创建Spring Cloud的应用上下文。需要注意的是:到此时SB的上下文ApplicationContext还并没有创建哦。这个流程“嵌套”特别像Bean初始化流程:初始化Bean A时,遇到了Bean B,就需要先去完成Bean B的初始化,再回头来继续完成Bean A的步骤。 说明:在创建SC的应用的时候,使用的也是SpringApplication#run()完成的(非web),因此也会走下一整套SpringApplication的生命周期逻辑,所以请你务必区分。 特别是这种case会让“绝大多数”初始化器、监听器等执行多次,若你有那种只需要执行一次的需求(比如只想让SB容器生命周期内执行,SC生命周期不执行),请务必自行处理,否则会被执行多次而带来不可预知的结果 SC应用上下文读取的外部化配置文件名默认是:bootstrap,使用的也是ConfigFileApplicationListener完成的加载/解析 LoggingSystemShutdownListener:来自SC。对LogbackLoggingSystem先清理,再重新初始化一次,效果同上个事件,相当于重新来了一次,毕竟现在有Enviroment环境里嘛 ConfigFileApplicationListener:@since 1.0.0。它也许是最重要的一个监听器。做了如下事情: 加载SPI配置的所有的EnvironmentPostProcessor实例,并且排好序。需要注意的是:ConfigFileApplicationListener也是个EnvironmentPostProcessor,会参与排序哦 排好序后,分别一个个的执行EnvironmentPostProcessor(@since 1.3.0,并非一开始就有),介绍如下: SystemEnvironmentPropertySourceEnvironmentPostProcessor:@since 2.0.0。把SystemEnvironmentPropertySource替换为其子类OriginAwareSystemEnvironmentPropertySource(属性值带有Origin来源),仅此而已 SpringApplicationJsonEnvironmentPostProcessor:@since 1.3.0。把环境中spring.application.json=xxx值解析成为一个MapPropertySource属性源,然后放进环境里面去(属性源的位置是做了处理的,一般不用太关心) 可以看到,SB是直接支持JSON串配置的哦。Json解析参见:JsonParser CloudFoundryVcapEnvironmentPostProcessor:@since 1.3.0。略 ConfigFileApplicationListener:@since 1.0.0(它比EnvironmentPostProcessor先出现的哦)。加载application.properties/yaml等外部化配置,解析好后放进环境里(这应该是最为重要的)。 外部化配置默认的优先级为:"classpath:/,classpath:/config/,file:./,file:./config/"。当前工程下的config目录里的application.properties优先级最高,当前工程类路径下的application.properties优先级最低 值得强调的是:bootstrap.xxx也是由它负责加载的,处理规则一样 DebugAgentEnvironmentPostProcessor:@since 2.2.0。处理和reactor测试相关,略 AnsiOutputApplicationListener:@since 1.2.0。让你的终端(可以是控制台、可以是日志文件)支持Ansi彩色输出,使其更具可读性。当然前提是你的终端支持ANSI显示。参考类:AnsiOutput。你可通过spring.output.ansi.enabled = xxx配置,可选值是:DETECT/ALWAYS/NEVER,一般不动即可。另外,针对控制台可以单独配置:spring.output.ansi.console-available = true/false LoggingApplicationListener:@since 2.0.0。根据Enviroment环境完成initialize()初始化动作:日志等级、日志格式模版等 值得注意的是:它这步相当于在ApplicationStartingEvent事件基础上进一步完成了初始化(上一步只是实例化) ClasspathLoggingApplicationListener:@since 2.0.0。用于把classpath路径以log.debug()输出,略 值得注意的是:classpath类路径是有N多个的Arrays.toString(((URLClassLoader) classLoader).getURLs()),也就是说每个.jar里都属于classpath的范畴 当然喽,你需要注意Spring在处理类路径时:classpath和classpath*的区别~,这属于基础知识 BackgroundPreinitializer:本事件达到时无动作 DelegatingApplicationListener:执行通过外部化配置context.listener.classes = xxx,xxx的监听器们,然后把该事件广播给他们,关心此事件的监听器执行 这麽做的好处:可以通过属性文件外部化配置监听器,而不一定必须写在spring.factories里,更具弹性 外部化配置的执行优先级,还是相对较低的,到这里才给与执行嘛 FileEncodingApplicationListener:检测当前系统环境的file.encoding和spring.mandatory-file-encoding设置的值是否一样,如果不一样则抛出异常如果不配置spring.mandatory-file-encoding则不检查 总结:此事件节点结束时,Spring Boot的环境抽象Enviroment已经准备完毕,但此时其上下文ApplicationContext还没有创建,但是Spring Cloud的应用上下文(引导上下文)已经全部初始化完毕哦,所以SC管理的外部化配置也应该都进入到了SB里面。如下图所示(这是基本上算是Enviroment的最终态了): 小提示:SC配置的优先级是高于SB管理的外部化配置的。例如针对spring.application.name这个属性,若bootstrap里已配置了值,再在application.yaml里配置其实就无效了,因此生产上建议不要写两处。 ApplicationContextInitializedEvent:上下文已实例化 @since 2.1.0,非常新的一个事件。当SpringApplication的上下文ApplicationContext准备好后,对单例Bean们实例化之前,发送此事件。所以此事件又可称为:contextPrepared事件。 完成的大事记 printBanner(environment):打印Banner图,默认打印的是Spring Boot字样 spring.main.banner-mode = xxx来控制Banner的输出,可选值为CONSOLE/LOG/OFF,一般默认就好 默认在类路径下放置一个banner.txt文件,可实现自定义Banner。关于更多自定义方式,如使用图片、gif等,本处不做过多介绍 小建议:别花里胡哨搞个佛祖在那。让它能自动打印输出当前应用名,这样才是最为实用,最高级的(但需要你定制化开发,并且支持可配置,最好对使用者无感,属于一个common组件) 根据是否是web环境、是否是REACTIVE等,用空构造器创建出一个ConfigurableApplicationContext上下文实例(因为使用的是空构造器,所以不会立马“启动”上下文) SERVLET -> AnnotationConfigServletWebServerApplicationContext REACTIVE -> AnnotationConfigReactiveWebServerApplicationContext 非web环境 -> AnnotationConfigApplicationContext(SC应用的容器就是使用的它) 既然上下文实例已经有了,那么就开始对它进行一些参数的设置: 首先最重要的便是把已经准备好的环境Enviroment环境设置给它 设置些beanNameGenerator、resourceLoader、ConversionService等组件 实例化所有的ApplicationContextInitializer上下文初始化器,并且排序好后挨个执行它(这个很重要),默认有如下截图这些初始化器此时要执行: 下面对这些初始化器分别做出简单介绍: 1. `BootstrapApplicationListener.AncestorInitializer`:来自SC。用于把SC容器设置为SB容器的父容器。当然实际操作委托给了此方法:`new ParentContextApplicationContextInitializer(this.parent).initialize(context)`去完成 2. `BootstrapApplicationListener.DelegatingEnvironmentDecryptApplicationInitializer`:来自SC。代理了下面会提到的`EnvironmentDecryptApplicationInitializer`,也就是说在此处就会先执行,用于提前解密Enviroment环境里面的属性,如相关URL等 3. `PropertySourceBootstrapConfiguration`:来自SC。重要,和配置中心相关,**若想自定义配置中心必须了解它**。主要作用是`PropertySourceLocator`属性源定位器,我会把它放在配置中心章节详解 4. `EnvironmentDecryptApplicationInitializer`:来自SC。属性源头通过上面加载回来了,通过它来实现解密 - 值得注意的是:它被执行了两次哦~ 5. `DelegatingApplicationContextInitializer`:和上面的`DelegatingApplicationListener`功能类似,支持外部化配置`context.initializer.classes = xxx,xxx` 6. `SharedMetadataReaderFactoryContextInitializer`:略 7. `ContextIdApplicationContextInitializer`:@since 1.0.0。设置应用ID -> `applicationContext.setId()`。默认取值为`spring.application.name`,再为application,再为自动生成 8. `ConfigurationWarningsApplicationContextInitializer`:@since 1.2.0。对错误的配置进行**警告**(不会终止程序),以warn()日志输出在控制台。默认内置的只有对包名的检查:若你扫包含有`"org.springframework"/"org"`这种包名就警告 - 若你想自定义检查规则,请实现`Check`接口,然后... 9. `RSocketPortInfoApplicationContextInitializer`:@since 2.2.0。暂略 10. `ServerPortInfoApplicationContextInitializer`:@since 2.0.0。将自己作为一个监听器注册到上下文`ConfigurableApplicationContext`里,专门用于监听`WebServerInitializedEvent`事件(非SpringApplication的生命周期事件) - 该事件有两个实现类:`ServletWebServerInitializedEvent`和`ReactiveWebServerInitializedEvent`。发送此事件的时机是`WebServer`已启动完成,所以已经有了监听的端口号 - 该监听器做的事有两个: - `"local." + getName(context.getServerNamespace()) + ".port"`作为key(默认值是`local.server.port`),value是端口值。这样可以通过@Value来获取到本机端口了(但貌似端口写0的时候,SB在显示上有个小bug) - 作为一个属性源`MapPropertySource`放进环境里,属性源名称为:`server.ports`(因为一个server是可以监听多个端口的,所以这里用复数) - `ConditionEvaluationReportLoggingListener`:将`ConditionEvaluationReport`报告(自动配置中哪些匹配了,哪些没匹配上)写入日志,当然只有`LogLevel#DEBUG`时才会输出(注意:这不是日志级别哦,应该叫报告级别)。如你配置`debug=true`就开启了此自动配置类报告 - 槽点:它明明是个初始化器,为毛命名为Listener? ==发送ApplicationContextInitializedEvent事件,触发对应的监听器的执行== 监听此事件的监听器们 默认情况下,有2个监听器监听ApplicationContextInitializedEvent事件: BackgroundPreinitializer:本事件达到时无动作 DelegatingApplicationListener:本事件达到时无动作 总结:此事件节点结束时,完成了应用上下文ApplicationContext的准备工作,并且执行所有注册的上下文初始化器ApplicationContextInitializer。但是此时,单例Bean是仍旧还没有初始化,并且WebServer也还没有启动 ApplicationPreparedEvent:上下文已准备好 @since 1.0.0。截止到上个事件ApplicationContextInitializedEvent,应用上下文ApplicationContext充其量叫实例化好了,但是还剩下很重要的事没做,这便是本周期的内容。 完成的大事记 把applicationArguments、printedBanner等都作为一个Bean放进Bean工厂里(因此你就可以@Autowired注入的哦) 比如:有了Banner这个Bean,你可以在你任何想要输出的地方输出一个Banner,而不仅仅是启动时只会输出一次了 若lazyInitialization = true延迟初始化,那就向Bean工厂放一个:new LazyInitializationBeanFactoryPostProcessor() 该处理器@since 2.2.0。该处理器的作用是:对所有的Bean(通过LazyInitializationExcludeFilter接口指定的排除在外)全部.setLazyInit(true);延迟初始化 根据primarySources和allSources,交给BeanDefinitionLoader(SB提供的实现)实现加载Bean的定义信息,它支持4种加载方式(4种源): AnnotatedBeanDefinitionReader -> 基于注解 XmlBeanDefinitionReader -> 基于xml配置 GroovyBeanDefinitionReader -> Groovy文件 ClassPathBeanDefinitionScanner -> classpath中加载 (不同的源使用了不同的load加载方式) ==发送ApplicationPreparedEvent事件,触发对应的监听器的执行== 监听此事件的监听器们 默认情况下,有6个监听器监听ApplicationContextInitializedEvent事件: CloudFoundryVcapEnvironmentPostProcessor:略 ConfigFileApplicationListener:向上下文注册一个new PropertySourceOrderingPostProcessor(context)。它的作用是:Bean工厂结束后对环境里的属性源进行重排序 -> 把名字叫defaultProperties的属性源放在最末位 该属性源是通过SpringApplication#setDefaultProperties API方式放进来的,一般不会使用到,留个印象即可 LoggingApplicationListener:因为这时已经有Bean工厂了嘛,所以它做的事是:向工厂内放入Bean "springBootLoggingSystem" -> loggingSystem "springBootLogFile" -> logFile "springBootLoggerGroups" -> loggerGroups BackgroundPreinitializer:本事件达到时无动作 RestartListener:SC提供。把当前最新的上下文缓存起来而已,目前并未发现有实质性作用,可忽略 DelegatingApplicationListener:本事件达到时无动作 总结:此事件节点结束时,应用上下文ApplicationContext初始化完成,该赋值的赋值了,Bean定义信息也已全部加载完成。但是,单例Bean还没有被实例化,web容器依旧还没启动。 ApplicationStartedEvent:应用成功启动 @since 2.0.0。截止到此,应用已经准备就绪,并且通过监听器、初始化器等完成了非常多的工作了,但仍旧剩下被认为最为重要的初始化单例Bean动作还没做、web容器(如Tomcat)还没启动,这便是这个周期所要做的事。 完成的大事记 启动Spring容器:AbstractApplicationContext#refresh(),这个步骤会做很多事,比如会实例化单例Bean 该步骤属于Spring Framework的核心内容范畴,做了很多事,请参考Spring核心技术内容章节 在Spring容器refresh()启动完成后,WebServer也随之完成启动,成功监听到对应端口(们) 输出启动成功的日志:Started Application in xxx seconds (JVM running for xxx) ==发送ApplicationStartedEvent事件,触发对应的监听器的执行== callRunners():依次执行容器内配置的ApplicationRunner/CommandLineRunner的Bean实现类,支持sort排序 ApplicationRunner:@since 1.3.0,入参是ApplicationArguments,先执行(推荐使用) CommandLineRunner:@since 1.0.0,入参是String... args,后执行(不推荐使用) 监听此事件的监听器们 默认情况下,有3个监听器监听ApplicationStartedEvent事件: 前两个不用再解释了吧:本事件达到时无动作 TomcatMetricsBinder:@since 2.1.0。和监控相关:将你的tomcat指标信息TomcatMetrics绑定到MeterRegistry,从而就能收集到相关指标了 总结:此事件节点结束时,SpringApplication的生命周期到这一步,正常的启动流程就全部完成了。也就说Spring Boot应用可以正常对对外提供服务了。 ApplicationReadyEvent:应用已准备好 @since 1.3.0。该事件所处的生命周期可认为基本同ApplicationStartedEvent,仅是在其后执行而已,两者中间并无其它特别的动作,但是监听此事件的监听器们还是蛮重要的。 完成的大事记 同上。 监听此事件的监听器们 默认情况下,有4个监听器监听ApplicationStartedEvent事件: SpringApplicationAdminMXBeanRegistrar:当此事件到达时,告诉Admin Spring应用已经ready,可以使用啦。 中间这两个不用再解释了吧:本事件达到时无动作 RefreshEventListener:当此事件到达时,告诉Spring应用已经ready了,接下来便可以执行ContextRefresher.refresh()喽 总结:此事件节点结束时,应用已经完完全全的准备好了,并且也已经完成了相关组件的周知工作。 异常情况 SpringApplication是有可能在启动的时候失败(如端口号已被占用),当然任何一步骤遇到异常时交给SpringApplication#handleRunFailure()方法来处理,这时候也会有对应的事件发出。 ApplicationFailedEvent:应用启动失败 当SpringApplication在启动时抛出异常:可能是端口绑定、也可能是你自定义的监听器你写了个bug等,就会“可能”发送此事件。 完成的大事记 得到异常的退出码ExitCode,然后发送ExitCodeEvent事件(非生命周期事件) 发送ApplicationFailedEvent事件,触发对应的监听器的执行 监听此事件的监听器们 默认情况下,有6个监听器监听ApplicationStartedEvent事件: LoggingApplicationListener:执行loggingSystem.cleanUp()清理资源 ClasspathLoggingApplicationListener:输出一句debug日志:Application failed to start with classpath: ... 中间这两个不用再解释了吧:本事件达到时无动作 ConditionEvaluationReportLoggingListener:自动配置输出报告,输出错误日志呗:特别方便你查看和错误定位 不得不夸:SB对错误定位这块才真叫智能,比Spring Framework好用太多了 BootstrapApplicationListener.CloseContextOnFailureApplicationListener:执行context.close() 总结:此事件节点结束时,会做一些释放资源的操作。一般情况下:我们并不需要监听到此事件 总结 关于SpringApplication的生命周期体系的介绍就到这了,相信通过此“万字长文”你能体会到A哥的用心。翻了翻市面上的相关文章,本文Almost可以保证是总结得最到位的,让你通过一文便可从大的方面基本掌握Spring Boot,这不管是你使用SB,还是后续自行扩展、精雕细琢SB,以及去深入了解Spring Cloud均由非常重要的意义,希望对你有帮助,谢谢你的三连。 关注A哥 原创不易,码字更不易。关注A哥的公众号【BAT的乌托邦】,开启有深度的专栏式学习,拒绝浅尝辄止 专栏式学习,我们是认真的(关注公众号回复“知识星球”领券后再轻装入驻) 加A哥好友(fsx641385712),备注“Java入群”邀你进入【Java高工、架构师】系列纯纯纯技术群
当大潮退去,才知道谁在裸泳。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。 前言 各位小伙伴大家好,我是A哥,一个前25年还不会写Hallo World的半残程序猿。也许你看到这个介绍心里一阵美滋滋:卧槽,终于有一个不是大佬(话外音:并不优秀)的人可以关注了,一下子阳光了起来有木有。 啊,问我多大了?反正是大龄程序员一枚没跑了 近期,在我朋友圈看了不下5篇的“个人介绍”文章,看完之后我每每只能附上本就匮乏的赞美之词:666、牛逼牛逼、大佬带带我......每看完一篇,我的心是这样的: 24岁买房,25岁年薪50万+,26岁孩子可以帮忙打酱油......有些人过着开挂式人生,而有些人也过着“开挂式人生”~ 画外音:我们同样都有腰间盘,为何大佬们如此突出呢?得看看医生了呀 这就是强者的世界,大多数人(那必须包括我啊)所向往的世界。人生是一个漫长的过程,我们怀有太多的期望,亦会遇到太多的失望,但请记住:烧不死的鸟才是火凤凰,烧死的都成烤乳鸽了。爱因斯坦告诉我们:成功 = 99%汗水 + 1%天赋,但是你活到现在或许才发现,那1%的天赋才是至关重要的。所以不要一味地模仿,因为大多数的“成功”并不可复制。 我曲折的人生经验告诉我:听马云如何解决问题,如何应对困难对你不会有任何帮助,因为级别差得太多,不具备效仿的意义。只有身边的人和事,说白了就是和你Level等级相差无几的人才能促使你进步。如果你觉得那些开挂人生的大佬给你压力,那么就关注我吧,看完我的个人介绍,会发现我是来给你送安慰的,因为你大概率比我优秀得多得多。 人生已经很艰难,对自己好点,别压力过大,毕竟活得长,胜算才最大,比如想想司马懿。所以呀,咱们一起做个技术人,产生共鸣;一起做个俗人,贪财好色。 正文 我的人生比较曲折,总体上分为三个阶段介绍: 1、贫苦大众 游戏人生 2、迷茫大学 似梦年华 3、凤凰涅槃 程序生涯 1、贫苦大众 游戏人生 1.1 家徒四壁 呱呱坠地 这是我2019年春节爬到山顶拍摄的我家乡县城全景图:湖北省通山县。对你没看错,属于这次全国疫情最严重的地区湖北省的一个县。通山二字含义为:通通是山。环视整个中华大地,凡是这种地貌的省市/地区,一般都会这么形容他:远离闹市喧嚣,尽享静谧人生。 话外音:你那山多树木多,空气非常的好呀,适合养身。经济嘛,不好意思,我有事先走了 层层叠叠的山峰自然地“阻断”了交通,所以同样的一瓶可乐,在武汉市(平原地区)卖3块钱,但在我们县买就是要贵5毛钱,你说气不气。好在去年(2019年),我们县脱贫啦,在政策帮助下顺利“摘帽”(此处应有掌声): 至于我的出生嘛,建议携带纸巾阅读(皮一下~)。我出生在通山县一个地地道道的农村家庭,怎么形容家境呢?当时也不可能有照片嘛,费好大力气找来一张2004年的老照片(分辨率低,不清晰),便可大概逆推出来当时喽: 说到了这,我把一个深藏已久的小秘密也公布了哈:我是一个早产儿,还不满8个月便因为我妈妈挺着大肚子还坚持下田干农活,然后,然后,我就意外的提前出来了。 你可能会好奇:怀孕7个多月还下农田干粗活???如果不是生活所迫,谁愿默默承受! 早产其实并不可怕,只是在当时的条件下,提前这么长时间早产的,只会有一个结果:孩子没了。可别想着说去医院生,没有的事,因为那里只属于有钱人。最终呢,在几乎没人看好的情况下,我以小强般顽强的生命力挺了过来,这不,现在还坐在电脑前写公众号着嘛。 这么算来,我也是度过生死劫的人哈,会不会大难不死必有后福呢?这我不知道,但我知道没什么能比健康活着更重要的事了,若用这种心态去生活,岂不处处皆阳光麽。 Tips:若后福已至,那当属我已娶得娇妻一枚喽 1.2 被耽误的高中 回不去的青春 听着《长寿村》这个背乐敲打这段文字,单曲循环,泪水打湿了眼眶,像青春致敬,还有几个人会像我这样怀念梦幻吗? 上高中,来到了县城上学,开始了住校生活。第一次接触电脑,第一次知道网吧长啥样,第一次知道了上网和电网是有区别的......可能和你们不一样,我的上网不是从学着使用word、excel、ppt开始的,而是一切起源于看片(懂的自然懂,不懂的不用懂),所以上高中后我也就有了第一次(别想歪了)...... 到县城上学因为是住校,所以每周有60元生活费(人生首次有如此多自由支配的钱)。但因为迷恋上了网络游戏,所以高中生活很长一段时间每天只吃1-2餐。原因很简单嘛,钱是固定的,上网多了,吃饭自然就少了。高中阶段是我长身体的最关键时期,但由于经常“废寝忘食”,所以导致了我现在身高并不高,成了我最痛的回忆。 别光顾吃瓜哈,我还是没拉男性平均身高的后退滴 很快,接触到梦幻西游这款游戏:申请一个号,从0开始。人物角色:龙太子,门派:龙宫。没错,就是他: 首次接触游戏并无经验,所以昵称取名为光能英雄,在那个非主流文化盛行的年代,这个名字简直土到掉渣。正因为这个名字,被玩伴们硬生生的“嘲笑”好几年,哪怕现在聊起来还不忘调侃我的这个取名处女秀。 其实我本意是想取名为:光能使者(发现没,这个名字还是蛮高大上的嘛)。但是使者二字属于固定NPC,官方不让用,所以...... 还记得当时游戏没点了就偷家里钱去充点卡,明知会挨打的,还偏要去偷。花了30多块买了个将军令,别在裤腰带里每天带着,感觉非常吊的样子。梦幻西游可以窗口化、双开,所以每次通宵都是一边游戏一边看片(别想歪了,是正常电影),偶尔也挂机,挂着挂着就睡着了。如今再来回忆过去,时间真的过得太快了,好想停留在当初,不想长大! 记忆很深是当时拜了一个师傅,我15级,她75级。那会没有微信更没有手机,每次只能中午偷跑出去网吧通过QQ跟师傅约个晚上通宵的时间,好让她带我做剧情任务、副本任务、整宿整宿的扫大雁塔、抓鬼......她带我的话自己经验是非常少的,基本属于纯帮忙的那种,那时候的人真好,真纯粹。印象特别深的是我过40级剧情的时候,师傅她刚满90级,带我过了3次都还没过去,并且她自己因打不赢还死了2次(死亡是有掉装备风险的),本以为当晚是没戏了。但让我特别惊喜的是:她半夜打电话把朋友叫醒,把朋友125级的登上来帮我过了,这件事现在回想起来好暖 后来,我满60级了,师傅领着我去长安城国子监祭酒处,我出师了.......再后来,我90级了,我也收了徒弟。师傅125级了,但是我俩一起做任务的机会越来越少了,偶尔在龙宫里能碰见就唠几句......再再后来,师傅把她的号送给了我,而我也已经没有再继续玩下去的欲望了...... 虽十几年过去了,现在回味起来别有滋味。今天听着《长寿村》,泡了一会吧,看着吧友们的回复,感触良多。作为曾经国内最为流行的游戏,相信感同身受的人是可以蛮多的,因此顺道也分享一些吧友话给你吧: 每周日:12门派闯关、守军之争、英雄大会、长安保卫战、挑战首席大弟子,这些活动你都还熟悉吗? 十几年前,那时候刚入梦幻,0级,到处跑,看到发光的圈圈(传送点),觉得好神奇,不知不觉就走到了沉船底下,进去后,竟不知道怎么走出来,后来是被海毛虫和巨蛙打死了,才回到建邺城的,因为等级太低,那时候不会自动升级,要手动升级。爱上梦幻,就是从那一刻开始的,但是现在太商业了,已经失去了当年的味道 感觉好多回忆,现在已经是孩子妈了,还是很怀念 长寿的配乐是最缠绵伤感的,十年前那时经常通宵抓鬼或者押镖累了过来长寿听着背乐站一站,遐想、发呆 2012年之后就再也没玩过梦幻。之前玩了近八年。现在听着这音乐心里有种说不出的感觉 ...... 被耽误的高中,回不去的青春。当初介绍我玩梦幻西游的小伙伴,我是该谢谢你呢,还是谢谢你呢? 2、迷茫大学 似梦年华 2.1 疲于奔命的大学四年 还记得当时轰动中国、获得2009感动中国人物特别奖荣誉的“长江大学1024人链”感人事件吗?是的,它就出自这所英雄大学。 整个高中阶段都被梦幻西游“耽误”了,自然就没有个好大学上喽。从全国排名上看,它绝对算不上一个很好的大学。和大学时期的你一样,在校时候没少骂过母校,但现在不会了,因为人生最美好的四年,都在这里度过。母校是什么?母校是我可以一天骂他一百次,但是不允许别人骂他一次的地方。 话外音:耳边可容不得别人骂它,否则就拔刀吧 我的大学生涯,不可谓不“丰富多彩”: 协会、社团、学生会 创意设计大赛 校园销售团队 商学院招聘 开餐馆 全市最大圣诞节校园苹果鲜花销售 全市最大暑期家教团(名校高分) ...... 因为有了一群志同道合的伙伴,实现了很多学生时代的“梦想”:在大桥底下过夜、下雪天光膀子沿操场跑50圈、通宵KTV、以及在学校隔壁的四星级酒店开party......再多各自牛逼的时光,也比不上一起傻逼的岁月。 2.2 跌落深渊的毕业答辩 上面列出的很多条目,没有一条与学习/专业有关。我似乎是一个最分不清主次的学生,忘乎了学生最主要的职责其实是学业,大学期间我做了几乎你所有想干的事,除了学习。补考成为了我每年必做的事,60元一个学分,补考费在当时来说也算天价,每次缴得让我心痛。 没有补考过的大学,不算完整的大学。我tm现在想问了:这是谁说的哪门子歪理论呢?尽霍霍我了 但这还不是最痛的,和我的毕业比起来,补考只是小儿科。大四下学期准备毕业论文,我在某宝花了120块钱买了一篇,包售后的那种,结果首次答辩没有通过,参加二次答辩,结果。。。还是没给过 面临着延迟毕业的危险,眼瞅着其他同学陆陆续续离开学校参加工作去了,心里暗自神伤,悔不当初。炎炎夏日,所有寝室里只剩下我和同班的另外一位“难友”。拿不到毕业证不能正常毕业,对人生的影响是极大的,那会承受的心里负担、压力乃至到现在拿来比较,都还没有什么事情可与之比拟。作为好学生的你,可能是永远无法体会到我当时的那种恐慌,这种gap就像:法海与爱。 当然喽,庆幸的是结局是好的:没有延迟毕业,我如期毕业了。这底下的一波操作实在感人,这段黑历史这里就不公开了哈,有兴趣可以底部给我留言咱们促膝单聊~ 2.3 百无一成的初出茅庐 这段路程,是我毕业之后的两年经历,最难的两年。期间大小换过5次工作,且都是那种被认为门槛最低,无技术含量的,如你所见: 大智慧炒股:北京。是的,你当时如果正在使用它看股票,看涨跌,或许你就接听过我给你打的电话 兴麟房产:宁夏银川。是的,就是你现在常常骂的“黑中介”中的一员。这份工作是我做的时间最长的,干了有近一年的时间。最让我值得回味的并非那些对大叔大妈的“忽悠”,也并非工作本身。而是在宁夏银川结识的一些朋友,那种豪气,那种爽快,那种喝大酒,非我们一般南方人所能比,期待我找个机会回去银川时再相聚。 奶茶店:湖北荆州。和朋友一起在荆州呆了一个多月,目的是在学校旁边盘个奶茶店,谈了几个并且考察了相关流程,但最后也搁浅了。这段时间一直蜗居在宾馆里,好在学校附近嘛,房费并不算贵 百度糯米:湖北武汉。公司听起来还不错,但干的就是最底层最累的LBS的活。说白了:打电话、陌拜、地推邀请商户入驻百度糯米提供团购。在这里我知道了餐饮利润没过50%就不要干餐饮、各行各业的毛利率水平、当然也见证了Robbin李老板当时说投资200亿到百度糯米到现在的啪啪啪打脸...... 豪大大香鸡排:湖北武汉。加盟豪大大,我的鸡排梦 以上,就是我毕业后整整2年的艰辛之路,用一事无成来形容都高估了自己。人在江湖飘,哪有不挨刀,这个阶段的我真可谓满目疮痍,被压死仅需最后一根稻草。 3、凤凰涅槃 程序生涯 时间到了毕业已2年了,审视现在的自己:何止是“三无产品”,竟连一件可以拿出来絮叨的事儿都没有。最为致命的是,自己仍旧啥技能都没不会。想回去“继承父业”(家里有农田可耕嘛),但自己却又肩不能扛手不能提,有着一堆“公子病”。如何破局?在这个节骨眼上,相信作为读者的你都能感受到我当时是有多么的迷茫,无力感是有多么的强,此时,没有人比我更需要改变。 3.1 欲练此功 必先闭关 当自己没有退路可选的时候,只能毕其功于一役。经过了很长时间挣扎和重择业,最终决定选择回到我曾经在大学期间最畏惧的行业:程序员。依稀记得,大四时我多次在公开场合说到:毕业后一定以及肯定不会从事编程行业。现在回头再看,这脸打得......挺痛 人性的弱点:越缺什么,就越强调什么。我本人其实十分羡慕IT行业,只奈何当初完全不上道...... 所以喽,痛定思痛,我开始了我的100天闭关学习计划:切断一切外界联系,从0开始,每天保证至少12小时+的高效学习时间,足不出户。这期间陪伴我时间最长的就是这两套视频:现在依旧记得风清扬老师那磁性的声音和他心目中的女神:林青霞(东方不败);崔希凡老师操着一口东北话和赐予的三尺白绫。您们幽默风趣的教学风格,给我闭关枯燥的学习增添了不少乐趣,这才让我坚持了下来,真心的感谢二位老师。 感谢二位老师,后来我也联系到了二位老师加微信表示了感谢,祝福传智播客越来越好 说句题外话:我收到过一些小伙伴咨询问我要不要报名去培训机构锻造,对此我一直持中立态度。中立是因为它收费(一般价格还不菲),如果撇开收费这个关键要素(比如收费没有那么的贵),是非常有价值的,因为不吹不黑,大多数机构的老师都是非常有水平的。 伴随着这100个日日夜夜,我完成了基础的学习,但实在还是手生,打代码还得看键盘。还好当时的互联网环境欣欣向荣,所以我才敢硬着头皮上:海投。 这100天的意外收获:把驾驶证拿到手了(可见我大学是多么的“忙”,在校期间竟连驾照都没考)。 3.2 崭新篇章 程序人生 100天的闭关结束后,自然而然的开始了我的程序员生涯。但这条路开始走得并不顺利...... 为了第一份工作,我连续的天天面试,每天1-2家,持续足足两周共计面了20+家公司,才在最后一个周末拿到了唯一一个offer:外包公司,月薪试用期3500。伙计,北京耶,我还得吃饭还得住宿呀,那会可都5102年了,回忆一下你那会大学生活费是不是都破2000了? 虽然非常的窘迫,开的条件也很“简陋”,但我心里还是高兴的。因为终于有公司愿意接收我了,从此成为了一个真正的技术人,有一技之长啦。进入这个公司后的我丝毫不敢懈怠,那会给自己定的唯一目标是:努力转正,不要被开除。毕竟作为一个打代码还得靠一指禅、看键盘的我,真的很担心露馅。 科班出生vs非科班的明显差异:其它条件都一致情况下,科班4年大学学习折算成实际经验值至少能高出非科班2年甚至更多。也就说1年经验的科班生,是远比1年非科班生的基础、技术熟练度要好非常多的。 好在自己小有“天赋”(主要靠努力、靠死撑),在不断的努力下,得到的回馈也比较乐观,小开挂了一波:提前转正、年度优秀、职位升级、破格涨薪。仅7个月时间,我的薪水从3.5k涨到了12k,这种“速度”公司里是没有前例可寻的,所以需要特批~ 跟第一个东家没过太久就分手了,虽然让领导觉得可惜甚至失望(毕竟对我这么好嘛),但分手还算和平。我离开的原因也很冰冷:外包公司嘛,呆久了对职业生涯没多大好处,俺对技术是有追求滴,追求可持续发展。 从此,我不再是空中飘着的云,随风而逝。脚踏实地的感觉真好,有一件事可以让自己当作事业来做,全情投入的感觉是很妙的。我的人生前半部分算是正式宣告翻篇了,开启了崭新的篇章。 笔耕不辍 钟情翰墨 眼瞅着当码农快3年了,3年是一个典型从业年限数字。看看那些招聘信息,大都写着工作经验要求3-5年,并且这一条似乎是HR筛选简历的硬核条件,因此需要好好把握这个时间节点。 话外音:倘若你从业满了3年,可视区域可就大了很多 一路走来,默默的把自己和身边(普通)同事compare以自省,渐渐的自己把逝去的那些时间补回来了,挺起腰板正式入编到“正规科班”行列。 积累了知识,多了些经验,但总感觉还是不够扎实,知识不成体系,而且很容易捡了芝麻忘了西瓜,因此我选择采用写下来的方式,把它持久化掉。起初,我仅仅只想找一个云笔记本记录下我关心的技术关键点,以便日后自己“复习”使用,所以我最初使用的是有道云笔记(很low有木有,不过够用就行嘛): 2018年:CSDN 用有道云笔记持续记录了有一段时间,直到有一次我给团队做分享时,有个同事看到我收录了不少技术内容,然后他就建议我可以把这些内容整理到博客里,显得更专业些。这不,我选择了CSDN平台,并在2018-5-17发了第一篇文章: 虽然期间也接触过其它技术社区平台,但文章首发均在CSDN。时至现在,坚持了有近2年之久,可以为自己鼓个掌喽。 这2年间以CSDN作为创作平台,坚持只输出技术文章(确切的说只输出Java相关技术文),收获到了一些志同道合的粉丝,这些给与了我程序生涯非常大的鼓励,坚定了信心。同时,也得到过CSDN官方的认可:CSDN官网首页顶部轮播图首位、CSDN官方公众号推文首文、2019年度博客之星评选......当然还有我这些花花绿绿的勋章,这是对自己“成绩”的最好官方证明: 整整两排勋章,有点收集癖哈哈。有木有比我多的?亮出来呗 相较于官方给与的肯定,对于我而言,这些匿名好友的评价和鼓励更能给我带来刺激感,赋予动力。让我觉得当初的再择业时选择程序员这条路并没有错,谢谢你们: 此处顺便再声明一下:A哥跟享学课堂、享学科技什么的真的没有半毛钱关系,纯名字撞了而已: 通过CSDN平台能收获到这些好友的共鸣声,属于我意外的收获。起初仅仅只想把它作为自己的一个markdown笔记本来用,无心插柳柳成荫,后来慢慢的有读者给我反馈,给我打气加油,这才决定坚持写下去,并且更加用心了。 关于我的CSDN阅读量说句题外话:纯技术文章的阅读量一般都不会很高(当然不排除也有高手能把技术讲得生动有趣的),况且我还不会标题党(我只会直抒胸臆),所以即使写了2年,我的博客总阅读量也并不算高,粉丝量也不很多(但我相信都是爱技术的真爱粉),关键我还没有渠道导流不是。“水文”相对来说较容易抓人眼球获得阅读量,因为读起来确实有意思,我也蛮喜欢读的。我曾经尝试过写了几篇,但发现自己真不是那块料,告辞了。所以痛定思痛,还是把精力花在力所能及的事上吧:我只会写纯技术文,找到属于自己的一片小众空间心满意足,至于流量文那些,我玩不来。 话外音:我内心深处其实很想写出流量文,但奈何文笔确实不行,所以还是乖乖的做个技术人吧,别瞎折腾了 开始写公众号并不代表不再维护CSDN了,because我的伙食费还需要它的支援勒。仍旧会同现在一样,持续保持更新哒~ CSDN平台永不停更,但更新的速度一般会滞后于公众号1-2天,欢迎CSDN的粉丝关注我的公众号哟 2020年:微信公众号 都2020年了你才开始搞公众号?是的,这还是我的第一篇文章呢。有些人做公众号是出于兴趣,有些人做公众号是为了发展副业,而我又不一样了:我是为了把技术分享切换到一个大家更易接受、更喜欢的平台上,仅此而已。我的粉丝是小众的,我写不出流量文章,所以流量红利我是享受不到喽,依旧保持CSDN平台一样的初心就好:不忘初心,砥砺前行。 其实早在2018年末,就有大佬建议我切换到公众号平台去分享,我当时也听进去了建议,申请了帐号。只是后来我觉得在公众号里发技术文太麻烦了(不支持Markdown还得自己转换),并且手机里看示例代码效果实在太差,所以公号仅运营了一周就彻底搁浅了。因为我当时对于技术分享,心里只有一个想法:同样是内容,放哪都一样,所以我更倾向于可以大屏观看的CSDN平台。 但为何今年我却动摇了呢?并不是初心变了,而是不止一个大佬建议我搞个公众号,因为有些很现实的问题:CSDN已是过去时了,如果你还在用CSDN推文,很多技术好友他们是心存抵触心里的,而且使用体验上也不如公众号。我品,我细品一番确实是这么个道理:大家都使用IPhone X用微信语音聊天了,而你还在使用诺基亚打电话吗? 总结 过去的这些年,我的人生是非常曲折的,是具有教育意义的人生轨迹,简直就是负面教材嘛。这不现在过年我回去,还有村民让我给他家孩子上思想政治课呢,让我教育教育他孩子要好好读书少玩游戏,不要走弯路。咋地,我这“名声”都传村里去了?用脚丫子想都知道这是我妈的“功劳”~ 截止到现在,我的漫漫人生中,即使只从高中算起,我有小10年的路是完全走歪的,偏离了主航道。不敢说这些路对我一丁点帮助都没有,但从客观上来说错就是错,是无法回避的事实。现在回味起来,一声叹息,毕竟人生有几个10年呢? 人生就像一场马拉松,获胜的关键不在于瞬间的爆发,而在于途中的坚持。你纵有千百个理由放弃,也要给自己一个坚持下去的理由。很多时候,成功就是多坚持一分钟,这一分钟不放弃,下一分钟就会有希望。漫漫人生途中,有的人跑的是上半场,有的人会在下半场爆发,每个人都是24h,充满希望的生活才会更有意义。 截止到今天,我还差几个月就在北京缴纳社保满5年了,祝愿自己在小汽车摇号时,一发中第。 文末彩蛋 作为“开业”第一篇文章,于情于理都得有个彩蛋来热闹一下嘛。A哥虽然没啥钱,但学着从公众号底部留言抽几个人寄几本书还是送得起的嘛。这里我就不这么干啦,毕竟我的粉丝本来就小众,万一没人留言岂不尴尬死。 既然不送书,那就玩比送书福利稍微大点的呗。怎么搞?我不是写有付费专栏嘛,我打算在目前拥有的所有付费专栏中(见下截图),选出2个付费专栏(所有文章),在接下来的公众号文章中免费推送给大家学习和交流(是不是比送书来得更有诚意些呀)。 当然喽,至于具体选择哪2个专栏作为免费发送的福利,权利当然是交给你,你可以文章留言or私聊我都行,我会筛选出出排名前2的专栏的~ 说明:为了避免付费版权问题,在公众号发出的文章时我会重新排版、改变叙述方式、重新调整目录和内容等等,这依旧是一项费力的工作,还望各位小伙伴多多支持和包涵 可选专栏(目前):专栏目录示例: 做个技术人,追求共鸣;做个俗人,贪财好色。从今天起,我重新拾起了公众号,不忘初心,砥砺前行,坚持只做技术干货输出,不求激增阅读量,只需小众共鸣。恰是因为小众,你们每个关注者都是我的VIP,有机会面基哈。 好了,就聊这么多。祝开工大吉,来个关注呗~ 关注A哥 原创不易,码字更不易。关注A哥的公众号【BAT的乌托邦】,开启有深度的专栏式学习,拒绝浅尝辄止 专栏式学习,我们是认真的(关注公众号回复“知识星球”领券后再轻装入驻) 加A哥好友(fsx641385712),备注“Java入群”邀你进入【Java高工、架构师】系列纯纯纯技术群
2021年01月
2020年12月
2020年11月
2020年10月
2020年09月
2020年08月
2020年07月