当HTTP标准还不够的时候。 Micronaut提交

大家好,我叫Dmitry,今天我将讨论生产需求如何使我成为Micronaut框架的贡献者 。 当然,许多人听说过他。 简而言之,这是Spring Boot的轻量级替代方案,其主要重点不是反射,而是所有必要依赖项的初步编译。 更详细的了解可以从官方文档开始。

Micronaut框架已在多个内部Yandex项目中使用,并且已经很好地建立了自己。 那我们错过了什么? 我可以马上说:框架在原则上支持程序员理论上可能需要开发后端的所有功能。 但是,在某些情况下,开箱即用不支持。 其中之一就是您不需要通过HTTP而是使用HTTP扩展名进行工作。 例如,使用其他方法。 实际上,这种情况远远超出了看起来。 此外,其中一些协议是标准的:

  • Webdav是访问资源的扩展。 除了标准方法外,HTTP还要求支持其他方法,例如LOCK,PROPPATCH等。
  • Caldav是用于处理日历类型事件的Webdav扩展。 此协议在智能手机上的应用程序中具有很高的概率:用于同步日历,约会等。

并且列表不限于此。 如果查看HTTP方法注册表 ,您会发现RFC标准仅描述了HTTP方法,目前为39种。还有多少种情况是基于HTTP的自写协议。 因此,对非标准HTTP方法的支持相当普遍。 您使用的框架经常不支持此类方法。 这是ExpressJS堆栈溢出的讨论。 这是github上对Tornado的拉取请求。 好吧,因为Micronaut通常被定位为Spring的轻量级替代品-这对于Spring来说是同样的问题。

不足为奇的是,当在一个项目中我们需要对方法进行扩展的HTTP协议的支持时,对于Micronaut来说,我们面临的问题已经很久了,该问题已经在该项目中使用了很长时间。 事实证明,让Micronaut处理非标准方法非常困难。

怎么了 因为如果您现在查看Micronaut中HTTP方法的定义,您会发现它们是使用Enum设置的,而不是使用类,例如在Netty中所做的设置(我不小心提到Netty,稍后它会弹出不止一次)。 更糟糕的是,所有服务器调用匹配都是通过枚举(而不是方法的字符串名称)进行过滤来完成的。 这意味着,如果您需要非标准的HTTP方法,则需要用Enum编写它,这实际上不是解决问题的好方法。 首先,每次需要新方法时,都需要提交到存储库。 其次,HTTP方法默认情况下未标准化,并且它们的列表在任何地方都没有固定,因此预见所有可能的情况是不现实的。 必须迫使Micronaut以某种方式处理开发人员以前未提供的方法。

解决方法一:额头


图片

第一个也是最明显的解决方案是完全不接触Micronaut,并且不重写其中的任何内容。 为什么,因为从我们开始,可以像我们一样将nronx放在Micronaut的前面:

http { upstream other_PROPPATCH { server ...; } upstream other_REPORT { server ...; } server { location /service { proxy_method POST; proxy_pass http://other_$request_method; } } } 

有什么意义? 我们可以强制nginx使非标准方法访问所需的代理,同时使用nginx的能力来更改方法:也就是说,我们将通过POST方法进行访问,而Micronaut可以对其进行处理。

什么不好 首先,我们实际上使从Micronaut角度来看的所有请求都是非等幂的。 不要忘记,对于非标准方法也存在这种分离。 例如,REPORT是幂等的,而PROPPATCH不是。 结果,框架不知道请求的类型,并且正在查看这些处理程序的代码的程序员也将无法确定该请求。 但是,事实并非如此。 我们已经有一组测试,可以自动检查应用程序是否符合所需协议。 为了使这些测试能够在项目中使用这样的解决方案,您需要选择以下两个选项之一:

  • 除了应用程序本身之外,还使用必要的设置来提高nginx图像,以便测试访问nginx,而不是Micronaut本身。 尽管Yandex基础结构肯定允许您增加其他组件,但在这种情况下,看起来过度设计纯粹是为了测试。
  • 重写测试,以便它们不测试所需的协议,而是参考nginx重定向到的路径。 也就是说,实际上,我们不是在测试协议,而是在测试其具体实现的实质。

这两个选项都不是很漂亮,所以提出了一个主意:为什么不为正确的目的修复Micronaut,更重要的是,这样的编辑不仅对我们有用。 也就是说,我想要这样的东西:

 @CustomMethod("PROPFIND") public String process( // Provide here HttpRequest or something else, as standard micronaut methods ) { } 

我兴高采烈地承担了这项任务,但最终发生了什么?

解决方案二:让我们重写所有内容!




实际上,这比乍看起来要容易得多。 提交只是将HttpMethod从枚举更改为类。 接下来,我们在类中创建了用于枚举的静态方法(主要是valueOf)。 IDEA和Gradle一起确保一切都没有发生。

这里最困难的是使用DefaultUriRouter,因为它假定该集合是固定的,并为可能的方法创建了路径列表数组。 必须将其放弃以进行新的实施。 但是总的来说,一切都变得非常简单。 请注意,您必须添加240行并删除116。

问题在于这是一个重大变化。 是的,实际上,在使用Micronaut的常规项目中,您-很可能-不要直接在代码中使用HttpMethod,并且,如果使用它,则不太可能在其中使用序数方法和其他特定的枚举方法。 但是,这仍然不允许在1.x版中进行这样的更改,尤其是考虑到所有这些都是为了支持一种相当罕见的情况而启动的事实。 但是对于2.x,这是正常的编辑,但是您仍然必须使用2.x。 因此,我不得不写更多的代码...

解决方案三:循序渐进


图片

实际上,您可以看到版本1.3的相应提取请求 。 如您所见,我必须编写比重大更改多五倍的代码,这绝非偶然。 在这里,我要赞扬第八种Java中引入的接口中的默认方法。 对于这种不会破坏向后兼容性的重构,这是不可替代的,而且我无法想象在第八版之前如何对Java进行这些编辑(尽管奇怪的是,很可能在第八版之前进行重大更改)。

基本编辑基于HttpRequest接口具有用于过滤的getMethod方法的事实。 如您所料,他返回了枚举。 因此,默认方法getHttpMethodName已添加到接口,默认情况下该接口返回枚举值的名称。 然后他们找到了在路径匹配中使用原始方法的位置,并在其中替换为对新方法的调用。 然后,在Netty服务器接口的实现中,接口方法被重新定义为使用HTTP方法的实际值。

它包含一个可以在讨论中看到的陷阱,并且涉及Micronaut的声明性客户。 他们将枚举值的名称转换为Netty的HttpMethod类的实例。 如果查看此类中valueOf方法的文档,则会注意到对于标准方法,将返回缓存的值,对于非标准方法,则将每次返回该类的新实例。 也就是说,如果您有高负载,并且使用非标准HTTP方法访问服务器一百万次,那么您将同时创建一百万个新对象。 当然,现代GC应该可以解决这个问题,但是我仍然不想那样创建其他对象。 然后想到了使用ConcurrentHashMap.computeIfAbsent进行缓存的想法,但这里也不是那么简单:问题出在Java 8的缺陷上,即使在没有执行记录的情况下,这也会导致阻塞流。 结果,我们做出了一项临时决定:

  • 对于标准方法,我们使用Netty提供的实例缓存(实际上,以前一样)。
  • 对于非标准方法,请创建新实例。 那些选择非标准方法的人应该确保垃圾收集器可以消化对象的创建(例如,我们使用Shenandoah)。

结论


图片

到底可以说些什么?

  • 在软件开发的不同阶段,众所周知的纠错成本曲线很明显地体现在这里。 具体来说,我们正在谈论Micronaut开发初期的错误计算,当时决定将枚举用于HTTP方法。 鉴于Micronaut正在Netty上旋转,而该类用于该类,很难说这个决定是合理的。 从本质上讲,维护一个类而不是枚举是不值得的。 这就是为什么在此计划中进行重大更改要比通过向后兼容性支持对其进行修复要容易的原因。
  • 开源项目的众所周知的致命弱点(但是,在具有封闭代码的工业项目中也可以观察到)-它们没有项目文档。 同时,Micronaut实际上拥有非常好的文档:使用它的选项有哪些等等。 但是,这里我们谈论的是记录如何制定设计决策。 结果,即使需要一些改进,来自外部的程序员也很难参与项目的开发。
  • 不要忘记考虑在高负载和多线程环境中使用一个或另一个开源项目的事实。 在此即使有一点点改进也必须考虑到这一点。

聚苯乙烯


在准备发布本文时,拉取请求已被Micronaut向导分支接受,并将在1.3版中发布。

Source: https://habr.com/ru/post/zh-CN466917/


All Articles