java的那些框架曾经疯狂地使用xml,说是不用硬编码,但现在怎么感觉又“去xml化”回归硬编码了?

XML XML 124 人阅读 | 20 人回复 | 2021-01-09

java的那些框架曾经疯狂地使用xml,说是不用硬编码,但现在怎么感觉又“去xml化”回归硬编码了?
回复

使用道具 举报

回答|共 20 个

快乐人L

发表于 2021-1-9 20:25:46 | 显示全部楼层

从10年前Spring、Struts、Hibernate 框架的xml虐过的人来回答一下:
这些东西就是妥妥的弯路。
但是确实不可避免之弯路,历史之必然也。


可以说不光是Java的后端开发,实际上整个计算机科学技术和工程化历史上,就不断上演着重复历史的一幕。
首先说一下,xml文件确实意味着松散的耦合。当然随之而来的就是反射降低性能的代价。可以看得出来,当时的需求已经是那种变更大于性能的优先级,当然这些都是有救的,毕竟xml本身是用于表达的语言而不是用于编程的语言。那么对于当时的Java语言(1.5以前的版本)来说,可描述性、动态性、模板化是极其缺失的特性。
需要澄清问题里一个概念,不使用xml其实不能叫做“硬编码”,硬编码其实和使用xml与否没有什么必然联系。


之所以使用xml,是因为xml语言无关性、及其出色的对象的表达能力、可嵌套的树结构,可以借助简单的标签语法通过组合来搭建出任意想要的对象和结构
因此,社区里无论是开发工具也好,比如Tomcat、JSP这类容器技术或者模板技术,或者构建工具也好,比如ant、maven,或者后端开发技术,比如SSH、SSM组合,都是以大量xml作为配置文件来对一些简单对象和行为进行描述,因此才会出现前面所说的“硬编码”问题。xml文件确实可以提供一定程度的动态化,这也是社区使用xml的理由之一。
说明在这些开源社区中,xml的方式对内容的表达性和解耦性在当时还是受到广泛的认可的。包括后来的Android系统,也是一样。
要说到原因,其实这跟当年 Java 语言本身的太简练有点关系。在Java 1.5以前的是不存在注解这种东西的,因此在需要元编程的领域里,在程序中跟语言无关的“动态语言”、“模板化”和“描述语言”的支持是缺失的,这极大的限制了Java本身在协议性的开发上的简化和创想。因此,在个中权衡之后,xml毫无疑问就成为了“动态语言”、“模板化”和“描述语言”上的最佳选择。


事实上,那个时候的Java还有一条路可以走,就是Scala的路。将面向对象和函数式混合到一种语言之中,可以从另一个层面上解决“动态语言”、“模板化”和“描述语言”缺失的问题。但这样的话就会导致另一个问题:函数式编程思维事实上更难,实现细节黑箱化,语言更加语义化而不是形象化,对象内部的状态更加不可变,使用不当的话,实质上可能会增加很多工作,且有可能更加难以调试。
因此JCP一直是对函数式特性的引入持有非常非常谨慎的态度。以至于到了Oracle从Sun手里收购Java以后,函数式特性在Java 8版本中才得以引入,使得C#在函数式的支持上捷足先登。
重新回到xml的话题上。Java 1.5版本引入了注解,Java 1.6版本增强了注解和反射的功能,更加广泛的注解支持可以在函数、接口、变量、方法、注解上任意添加,使得Java在语言层面本身具有描述性。结合反射可以编写基于接口和注解的代码生成器,用于编译时生成代码。即便不使用生成代码,也提供了在运行时的代码的动态性。在这一点上使得过去需要在xml中语义描述的内容,使用注解就可以实现。注解的对象可以是具体化的,也可以是抽象的,使得语言本身的描述性、动态性和灵活性提供了极大的提升。
然而,xml的缺点在使用多年以后也逐渐显现,当xml文件中需要描述的内容越多,产生的尖括号越多,在阅读上和心理上产生了极大的障碍。人这种生物,大多是讨厌看到尖锐物或者恐惧看到尖锐物的,当阅读大量的尖括号括起来的内容时,会产生不自觉的烦躁感,阅读时也会因为尖括号数量的内容占比过高,造成过大的心智负担时,社区里就会越来越多抗拒使用xml的声音。




那么,由谁来代替呢?
统共分两个方向发展:
第1种:元编程方法。也就是注解、反射的使用。编写大量的“首次载入”程序,用于反序列化原先想要使用xml表达的内容,此时可以用注解+接口的形式来替代xml作为描述语言,提高可读性。实际上,自从Spring 从3开始,就已经支持了注解的使用。而使用xml的大多是遗留下来的陈旧系统和过于老的运行时。那些老旧的系统和运行时实际上是无法找到其他的组件或者语言作为xml的替代品的。这也是Java世界里使用时间较多较长且稳定的替代方案。对于一个设计较为复杂的注解程序来说,注解了注解的结构本身,就成为描述上的描述结构。使用简单,理解背后的原理可能会增加一些难度。当注解注解不慎,导致注解和注解之间的形成了一些语义上的交集关系,要处理起来的问题可能会更加复杂,且容易出错。因此使用注解本身没有什么问题,尽管它有着相当程度的黑箱化,对于注解功能的设计者来说要慎重。尤其对于注解注解接口来说要更加慎重。因为一旦设计不当,可能会导致使用者在用于描述类作为配置类的时候,可能会出现莫名其妙的问题。不过对于一般的设计者来说这种问题只要稍微小心一点就不会发生。这种方式对于使用者来说,可以普遍意义上的使用,只要会使用该语言,只要简单的进行注解配置就可以轻松使用框架提供的功能,且极少需要调试注解本身提供的功能。对于调试非常友好。


第2种:函数式编程方法。这种方法不同于声明式语言和命令式语言,函数式语言本身是将函数作为第一等公民的,所有让语言有可描述性的方法,注解成了可有可无的东西,而更多的工作都承载到了回调函数的身上。让lambda结构层层嵌套化,从整个框架设计本身开始就设计好内部结构,到了使用时,只需要设计好需要调用的函数与闭包内逻辑,闭包内的逻辑会被当做参数传入到函数中,将函数层层递进传入。所以从函数式语言看来,只有一个根节点结构的描述性的“脚本”,实质上只调用了一个函数而已。而剩下的逻辑就是“由外而内”的结构,层层传参了而已,传的参还都是函数。这种函数式的方法不但没有超出编程语言本身的设计,没有使用反射等消耗资源的逻辑,还增加了相当的可阅读性,节省了“设计模式”的时间,没有那么多的尖括号,只需要保留闭包作为逻辑分界的代码块的大括号即可。绕过了可描述性这个难以解决的问题。因此其实可以看到,对于相对更加完善的函数式语言来说,设计良好的DSL语言逻辑是比使用反射和元编程方法更加简练、整洁的代码之道。设计DSL会不会有问题呢?会。问题也不小。不但问题不小,而且对运行时的开发者来说,开源就成了近乎必须的选择。为什么会这样呢?基于注解的元编程方法是将语言本身可描述化。而函数式则不一样,函数式是由外向内,函数调用的结构决定了要描述的本身。因此框架在设计时,运行时的代码必须要透明才能做到调用关系明确。因此由函数式语言设计的DSL本身其实也是一种黑箱。对于熟手来说,精准无误的使用会有十分大的效率提高。但是对于生手来说,数据结构的黑箱性和调试的难度会大大拉高使用门槛。
这几种方法都各自有利有弊。历史的发展就是不断的轮回、扬弃、权衡的结果,就个人来讲更喜欢的是函数式方法,所有的东西做好后只要参照手册简单的配置就能实现很多动态化内容的变更。缺点其实也有运行时效率可能出现底下的情况。假如说对底层过度封装并且底层有着复杂的数据结构,运行时的分析过程可能会耗时过长。在这一点上并不一定比xml明确的定义结构更好。
在动态化过程中效率低下的部分,有时候可以使用生成器去生成代码来代替。这就是为什么权衡运行效率和开发效率的时候会有不同的选择。如果运行效率更重要,代码生成器生成静态代码比动态分析的代码效率更高。毕竟函数式的DSL的设计比较黑箱,谁也不知道背后运行时设计的运行效率如何。注解+反射的方式也有一定的效率损失,但是开发过程快,毕竟是运行时元编程,解析结构会有一点时间上的损耗。


总结起来说,其实在java或者说类似的语言的发展过程中,xml的方式、注解、函数式,其实更可以看做一套发展过程。


新的技术点出现 --->XML--->注解元编程--->函数式/DSL


所以其实在某个语言体系中出现某个新的技术后,基本上都遵循这个发展历程。只不过注解这个过程是可有可无的。毕竟很多语言没有注解这种东西。


越是设计简单的语言,这个过程发展的就快,最终到达函数式。越是设计的特性复杂的语言,这个过程发展的就慢,越复杂越慢。




更新:
如果有人有过编写xml的经验并且同时有用某种DSL脚本配置的经验,比方说Groovy on Grails、Gradle这类使用Groovy这种动态函数式语言做配置脚本。可以考虑对比一下XML和函数式DSL脚本,仔细观察他们的结构。
你会发现,
xml不就是函数式DSL语言么。
回复

使用道具 举报

简单350

发表于 2021-1-9 20:26:34 | 显示全部楼层

这个东西被某人称为 “逻辑守恒定律”。
有时候某个功能的逻辑很简单,你为了增加其可拓展性开始上 GoF,最后到达了只用改配置文件,不用改代码就能加功能的终极形态,但实际上:
    90% 的对 “可扩展性” 的需求是程序员自己意淫的,是过度设计。90% 的新需求用配置文件根本做不出来,还是得改代码。
里里外外浪费的时间还不如拿去重构代码呢 …
回复

使用道具 举报

简单350

发表于 2021-1-9 20:26:55 | 显示全部楼层

一个客观原因是,jdk1.5才引入annotation,大概三年后,2007年,spring 2.5才开始正式支持使用annotation声明bean。多数JAVA生态的框架在诞生之初,在如何做“配置”这个问题上,并没有多少选择。使用XML,在当时,是个十分合理的选择。
但是,不能说现在放回代码,就是又倒退回去硬编码了。在代码里声明bean,并不就等同于hardcode。满世界自己new对象也许是算硬编码。
我们管难以变化与管理的东西叫硬编码。而不是把放在代码里的叫硬编码,这明明就是XML自卖自夸的slogan。


说来说去还是JAVA太烂。
回复

使用道具 举报

简单350

发表于 2021-1-9 20:27:47 | 显示全部楼层

大家的回答都是从开发人员的角度来看的。
作为一个(甲方的)运维人员,我想说的是,以前总的来说觉得用XML把一些容易变的东西解耦出来是好事……如果这些东西比较简单,比如一个外部数据库的配置,不管是写死了用YourSQL,还是可以在YourSQL、PregreSQL、SQLFat里做切换,我们运维人员当然都是欢迎的;再复杂一点,需要在XML里写一些class name和构造函数的参数,我们也能接受;但要把不同的业务流程写到XML里去配置,要我们去改那肯定不愿意的,最终还是要(乙方)开发人员改了重新发布版本……既然这样,为啥要用XML绕来绕去?谁能从中受益?
回复

使用道具 举报

万胜

发表于 2021-1-9 20:28:41 | 显示全部楼层

先看看为什么要用xml(json/yaml)
因为现实就是要求代码承载比本身符号更多的信息。比如一个JAVA POJO,除了语法本身的属性get/set方法以外,我们还期望和一个数据库的表关联,期望和一个表单字段关联。
这个在注解出现以前的java没有太好的方法,c++那种多继承解决不了。实现多个标记性质接口一样解决不了,假如是c++通过宏或者模板插入代码能(不太那么优雅)解决,java没有(虽然现在有JSR269),唯一办法就是在代码以外用一个表现能力强配置项进行关联,xml是最好的选择。
为什么不用?
    xml看似分离,实际太麻烦了,编码过程中不可能一次完成不做修改。代码+xml+配置文件的方式在修改业务时候有巨大的麻烦,更何况很多还没ide加成自己一个个翻检注解,注解就是我上面说的“c++宏插入代码”类似的玩意儿,只不过不是编译期插入而是运行期反射获取注解然后根据注解在切面上加逻辑。注解做到了代码和代码以外的数据的耦合,方便啊!
回复

使用道具 举报

快乐人L

发表于 2021-1-9 20:29:18 | 显示全部楼层

win下面有个api叫做CreateWindow,要创建窗口应用就要调用这个api,它有十多个参数需要传递……包括窗口位置,窗口标题,窗口成分(最小化按钮和最大化按钮有无)等。
参数多了会有什么问题呢?首当其冲就是不方便记忆,其次就是代码会写的很长,最后是令人反思真的要这么多参数吗?我不想处理那么多我想大部分给个默认值行不行?
参数过多这个问题,很多人动手解决了。大体上的解决方案都是写一套库封装窗口创建的过程,使用者不需要自己调用这个函数,这一套参数都有默认值,如果需要修改某个值,调用另一个setXXX来修改。这样就可以避免了参数过多的问题了。
然而新的问题来了,这种库选择的默认值不够令一些使用者满意,每次都在代码最前面重新配置一次默认值,又显得非常不优雅,于是这种库的作者增加了一个功能:设定默认值。但是总不能和CreateWindow再撞车回去吧?所以聪明的库作者想到一个新方法:把默认值配置到程序外面来!所以配置文件出现了,它们大部分时候后缀是ini。
后来java各大web框架的xml文件策略只不过是继承了这样的优良传统:已封装好的库代码,为了使用上的方便,通过一套封装者规定的协议(这里是xml)去配置库代码的缺省参数。这里跟耦合程度其实没多少关系,就是我使用别人的库而已。
那为何后来注解把这一套方案替换了?因为注解把问题的核心解决了呀!我使用你的框架,需要配置你的缺省参数,配置的位置就在我的代码中,这才是最方便的用法,另起一个xml文件,xml得解析吧,xml怎么列得有详细文档吧,文件命名得规范吧,得手动校对是否符合字段定义吧?难道在我自己的代码中来一长串setXXX来配置参数?简直是代码污染好不好?现在一个注解,达到了同样的目的,还把这些问题都解决了。显然,这是一种技术上的进步,值得替换掉旧的方案。
至于xml是不是弯路?那当然不能这么说,毕竟注解是后来才加上的功能。如果没有注解,配置文件依然是众多方案中最让人认可的一个。
回复

使用道具 举报

快乐人L

发表于 2021-1-9 20:29:45 | 显示全部楼层

用XML做配置本意是为了制作各种开发辅助工具来提高开发效率,因为XML规范容易编写工具去处理嘛。但现实是那些工具往往做的BUG遍体而且还挺难学,最后逼得大家就都去手写,例如安卓的布局XML手写的特别多。这哪受得了,于是各种“投机取巧”的例如注解方式就流行起来了。
所以说一个技术好不好学是硬道理,那些把自己都转晕了的最后就只剩下几个自以为看懂的在吹嘘并一副世人皆醉我独醒的样子。
回复

使用道具 举报

万胜

发表于 2021-1-9 20:30:45 | 显示全部楼层

不说xml,我现在说另一个Java程序员都在犯的浪费时间操作——Service层的接口实现分离
对,就是那个一个XxxxService,一个XxxxServiceImpl。
这个思想的前提是很好,最早来源于分布式系统下,我只要引用你的接口就可以了,而不用整个包全拿过来。
可是如今这个操作真是越来越多余,何必要干这个分离。如果我是单点系统,这个分离没有必要;如果是微服务系统,直接从controller层过去了,接口层分他干什么。现在系统里绝大部分都是Autowired,你写多个实现类不怕搞出事吗?(架构处理除外)
即使你用dubbo来做,你也发现90%的分离是白做的,只有少部分服务才需要跨系统调用。也就是说,这个很优秀的设计模式,绝大部分情况下是做白工。
作为一个架构人员,我的建议就是“接口实现分离这种事,业务开发不要再做了”。真需要接口实现分离的场景有这么几个:
1、架构处理,这部分不用业务开发去做,架构来做
2、Dubbo等协议的跨服务器调用,这部分不用刻意去规定,等做到的时候,自然会将这些部分独立成一个工程来放
我说这些的意思是说,别死盯着“优秀模式”,好的模式比如代码配置分离等东西是要考虑使用场景和适用模式的。在当今的情况下,程序一般都活不到扩展性体现就重构了,与其把精力放在扩展性上,不如放在高效开发上。在不同的场景要学会选择不同的方式,
回复

使用道具 举报

万胜

发表于 2021-1-9 20:31:25 | 显示全部楼层

因为这群用 Extended Markup Language 的人,真的把XML当Language来用。
然而XML又没有XMLVM,而需要各门Language去按照Language那样去解析它。
既想要Markup的简单,又想要Language的灵活。实际上既没有Markup的简单纯粹,又没有Language的逻辑性和灵活性。
掌握这门Language的难度,已经显著高于直接用其他Language去表达逻辑的难度。


对比之下,HTML就安分守己很多,从没有谁认为HTML是一门正经的Language……只知道,这玩意是写网页的,多好
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则 需要先绑定手机号