Java Web App Architecture In Takes Framework

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

我曾经使用Servlets、JSP、JAX-RS、Spring Framework、Play Framework、JSF with Facelets和一点Spark Framework。在我个人的观点中,所有这些解决方案都离面向对象和优雅的目标相差甚远。它们都充斥着静态方法、无法测试的数据结构和肮脏的黑科技。因此,大约一个月前,我决定创建自己的Java Web框架。我将几个基本原则融入到了它的基础中:1)不使用NULL,2)不使用公共静态方法,3)不使用可变类,4)不使用类转换、反射和instanceof操作符。这四个基本原则应该能够保证代码的整洁和架构的透明。这就是Takes框架的诞生方式。让我们看看它是如何被创建出来并且如何工作的。

这是我对Web应用程序架构及其组件的简单理解。

首先,要创建一个Web服务器,我们需要创建一个新的网络套接字,它在特定的TCP端口上接受连接。通常情况下是80,但我将使用8080进行测试。在Java中,可以使用ServerSocket类来完成这个任务:

这就足够启动一个Web服务器了。现在,套接字已准备好,并在8080端口上进行监听。当有人在浏览器中打开http://localhost:8080时,连接将建立并浏览器将永远旋转等待。编译这段代码片段并尝试一下。我们刚刚构建了一个简单的Web服务器,没有使用任何框架。目前我们还没有对传入的连接进行任何操作,但我们也没有拒绝它们。所有的连接都被排列在那个server对象里。这是在后台线程中完成的;这就是为什么我们需要在后面放置那个while(true)的原因。如果没有这个无限暂停,应用程序将立即完成执行,并关闭服务器套接字。

下一步是接受传入的连接。在Java中,这通过对accept()方法进行阻塞调用来完成:

该方法会阻塞其线程并等待新连接的到来。一旦发生这种情况,它会返回一个Socket实例。为了接受下一个连接,我们应该再次调用accept()。所以基本上,我们的Web服务器应该按照以下方式工作:

这是一个无止境的循环,它接受一个新的连接,理解它,创建一个响应,返回响应,并再次接受新的连接。HTTP协议是无状态的,这意味着服务器不应该记住任何先前连接中发生的事情。它只关心这个特定连接中传入的HTTP请求。

HTTP请求来自于套接字的输入流,看起来像一个多行文本块。如果你读取套接字的输入流,你会看到这样的内容:

你会看到类似于这样的东西:

客户端(例如Google Chrome浏览器)将此文本传递给建立的连接。它连接到localhost的8080端口,一旦连接准备就绪,它立即将此文本发送到连接中,然后等待响应。

我们的任务是使用请求中获得的信息创建HTTP响应。如果我们的服务器非常简单,我们可以基本上忽略请求中的所有信息,并只返回”Hello, world!”给所有请求(为了简单起见,我使用IOUtils)。

就是这样。服务器已准备好。尝试编译并运行它。将您的浏览器指向 http://localhost:8080,您将看到 Hello, world!

这就是构建Web服务器所需的全部内容。现在让我们来讨论如何使其面向对象和可组合。让我们尝试看看Takes框架是如何构建的。

Routing/dispatching在Takes中与response printing结合在一起。您只需要创建一个实现Take接口的单个类,就可以创建一个工作的web应用程序。

现在是时候启动服务器了:

这个 FtBasic 类执行了上述解释的完全相同的套接字操作。它在端口8080上启动一个服务器套接字,并通过我们传递给它的 TkFoo 实例来分派所有传入的连接。它通过一个 Exit 的实例在一个无限循环中进行这个分派,并每秒检查是否停止。显然,Exit.NEVER 总是回答说:“请不要停止。”

现在让我们看看到达TkFoo的HTTP请求中有什么,以及我们可以从中获得什么。这是TakesRequest接口的定义方式。

请求分为两部分:头部和正文。头部包含所有在正文开始的空行之前的所有行,根据HTTP规范在RFC 2616中定义。框架中有许多有用的修饰符可用于Request。例如,RqMethod可以帮助您从头部的第一行获取方法名。

RqHref will help extract the query part and parse it. For example, this is the request:

GET /user?id=123 HTTP/1.1
Host: www.example.com

这段代码将提取123

RqPrint can get the entire request or its body printed as a String:

final String body = new RqPrint(request).printBody();

这里的想法是保持Request接口简单,并将此请求解析功能提供给其装饰器。这种方法有助于框架保持类小而有凝聚力。每个装饰器都非常小而坚实,只做一件事。所有这些装饰器都在org.takes.rq包中。正如您可能已经理解的,Rq前缀代表Request

让我们创建我们的第一个真正的网页应用程序,它将会做一些有用的事情。我建议从一个Entry类开始,这是Java在命令行中启动应用程序所必需的。

这个类只包含一个main()静态方法,当应用程序从命令行启动时,JVM将调用该方法。正如你所见,它实例化FtCli,给它一个TkApp类的实例和命令行参数。我们将在下一步创建TkApp类。FtCli(翻译为“带有命令行界面的前端”)创建了一个相同的FtBasic实例,并将其包装成几个有用的装饰器,并根据命令行参数进行配置。例如,--port=8080将被转换为8080端口号,并作为FtBasic构造函数的第二个参数传递。

这个 Web 应用程序本身被称为TkApp,并扩展了TsWrap

我们会在一分钟内讨论这个 TkFork 类。

如果你正在使用 Maven,请使用以下 pom.xml 作为起点:

运行mvn clean package应该在target目录中生成一个foo.jar文件,并在target/deps目录中生成所有JAR依赖项的集合。现在你可以从命令行运行该应用程序。

应用程序已经准备好了,你可以将其部署到Heroku等平台。只需在存储库的根目录中创建一个Procfile文件,并将存储库推送到Heroku。Procfile文件应该如下所示:

TkFork

这个TkFork类似乎是框架的核心元素之一。它帮助将传入的HTTP请求路由到正确的take。它的逻辑非常简单,在其中只有几行代码。它封装了一组“forks”,这些“forks”是Fork接口的实例:

它的唯一route()方法返回一个空迭代器或一个只有一个Response的迭代器。TkFork遍历所有的分支,调用它们的route()方法,直到其中一个返回一个响应。一旦这种情况发生,TkFork将这个响应返回给调用者,即FtBasic

现在让我们自己创建一个简单的分支。例如,当请求/status的URL时,我们想要显示应用程序的状态。以下是代码:

我相信这里的逻辑很清楚。我们要么返回一个空的迭代器,要么返回一个包含TkStatus实例的迭代器。如果返回一个空的迭代器,TkFork将尝试在集合中找到另一个能获得Response实例的分支。顺便说一下,如果找不到任何东西并且所有的分支都返回空的迭代器,TkFork将抛出一个”页面未找到”的异常。

这个准确的逻辑是由一个开箱即用的叉子FkRegex实现的,它尝试用提供的正则表达式匹配请求的URI路径。

我们可以组成一个多层结构的TkFork类;例如:

再说一次,我相信这是显而易见的。FkRegex的实例将要求一个封装的TkFork实例返回一个响应,并尝试从FkParams封装的实例中获取它。如果HTTP查询是/status?f=xml,则会返回一个TkStatusXML的实例。

现在让我们讨论HTTP响应的结构及其面向对象的抽象,Response。这是接口的外观:

看起来非常类似于 Request,不是吗?嗯,它们几乎是一样的,主要是因为HTTP请求和响应的结构几乎是相同的。唯一的区别在于第一行。

有一系列有用的装饰器可帮助构建响应。它们是可组合的,这使它们非常方便。例如,如果你想构建一个包含HTML页面的响应,你可以像这样组合它们:

在这个示例中,装饰器RsWithBody创建了一个具有正文但没有任何标头的响应。然后,RsWithType为其添加了标头Content-Type: text/html。然后,RsWithStatus确保响应的第一行包含HTTP/1.1 200 OK

您可以创建自己的装饰器,可以重用现有的装饰器。请查看RsPage在rultor.com中的实现方式。

返回简单的“Hello, world”页面并不是一个大问题,我们可以看到。但是对于更复杂的输出,比如HTML页面、XML文档、JSON数据集等,情况会怎样呢?有一些方便的Response装饰器可以实现这一切。让我们从Velocity开始,它是一个简单的模板引擎。嗯,它并不是那么简单。它相当强大,但我建议只在简单的情况下使用它。它的工作原理如下:

RsVelocity 构造函数接受一个参数,该参数必须是一个Velocity模板。然后,您调用 with() 方法,将数据注入到Velocity上下文中。当需要呈现HTTP响应时,RsVelocity 将根据配置的上下文来“评估”模板。再次强烈建议您仅在简单输出时使用这种模板化方法。

对于更复杂的HTML文档,我建议您使用XML/XSLT与Xembly结合使用。我在之前的几篇文章中解释了这个想法:在浏览器中使用XML+XSLT和RESTful API和Web站点在同一个URL中。这很简单且强大——Java生成XML输出,XSLT处理器将其转换为HTML文档。这是我们将表示与数据分离的方式。XSL样式表是一个“视图”,TkIndex 是一个“控制器”,从MVC的角度来说。

我很快就会写一篇关于使用Xembly和XSL进行模板化的独立文章。

与此同时,我们将在Takes中为JSF/FaceletsJSP渲染创建装饰器。如果您有兴趣帮助,请fork该框架并提交您的pull request。

现在,一个问题是如何处理持久性实体,比如数据库、内存结构、网络连接等。我的建议是在Entry类中初始化它们,并将它们作为参数传递给TkApp构造函数。然后,TkApp将它们传递给自定义* takes *的构造函数。

例如,我们有一个包含一些表数据的PostgreSQL数据库,我们需要渲染它们。以下是我在Entry类中如何初始化与之的连接(我正在使用BoneCP连接池):

现在,TkApp的构造函数必须接受一个类型为java.sql.Source的单个参数。

TkIndex还接受一个类Source的单一参数。我相信你知道在TkIndex内部如何处理它,以便获取SQL表格数据并将其转换为HTML。这里的重点是依赖必须在应用程序(TkApp类的实例)实例化的那一刻注入进去。这是一个纯净的依赖注入机制,完全不依赖容器。在《依赖注入容器是代码污染者》一文中可以了解更多。

由于每个类都是不可变的,并且所有的依赖关系只通过构造函数注入,因此单元测试非常容易。假设我们想要测试TkStatus,它应该返回一个HTML响应(我正在使用JUnit 4Hamcrest)。

此外,我们可以在一个测试HTTP服务器中启动整个应用程序或任何一个单独的take,并通过真实的TCP套接字测试其行为;例如(我正在使用jcabi-http发送HTTP请求并检查输出)。

FtRemote 在随机TCP端口上启动一个测试Web服务器,并在提供的FtRemote.Script实例上调用exec()方法。该方法的第一个参数是刚刚启动的Web服务器主页的URI。

Takes框架的架构非常模块化和可组合。任何单独的take都可以作为独立的组件进行测试,与框架和其他take完全独立。

这是我经常听到的问题。这个想法很简单,它起源于电影业。拍摄一部电影时,摄制组会拍摄很多个“镜头”以捕捉现实并将其呈现在电影中。每个捕捉到的镜头都被称为一个“镜头”。

换句话说,一个“镜头”就像是现实的快照。

这个框架也是如此。每个Take实例代表了某个特定时刻的现实。这个现实随后以Response的形式发送给用户。

附注:关于身份验证也有几个词:Cookie-Based身份验证在Takes框架中的工作原理。

附注:还有一些真实的网络系统,你可能会对它们感兴趣。它们都使用了Takes框架,并且它们的代码是开源的:rultor.com, jare.io, wring.io.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-17 at 14:59

sixnines availability badge   GitHub stars