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请求中有什么,以及我们可以从中获得什么。这是Takes中Request
接口的定义方式。
请求分为两部分:头部和正文。头部包含所有在正文开始的空行之前的所有行,根据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/Facelets和JSP渲染创建装饰器。如果您有兴趣帮助,请fork该框架并提交您的pull request。
现在,一个问题是如何处理持久性实体,比如数据库、内存结构、网络连接等。我的建议是在Entry
类中初始化它们,并将它们作为参数传递给TkApp
构造函数。然后,TkApp
将它们传递给自定义* takes *的构造函数。
例如,我们有一个包含一些表数据的PostgreSQL数据库,我们需要渲染它们。以下是我在Entry
类中如何初始化与之的连接(我正在使用BoneCP连接池):
现在,TkApp
的构造函数必须接受一个类型为java.sql.Source
的单个参数。
类TkIndex
还接受一个类Source
的单一参数。我相信你知道在TkIndex
内部如何处理它,以便获取SQL表格数据并将其转换为HTML。这里的重点是依赖必须在应用程序(TkApp
类的实例)实例化的那一刻注入进去。这是一个纯净的依赖注入机制,完全不依赖容器。在《依赖注入容器是代码污染者》一文中可以了解更多。
由于每个类都是不可变的,并且所有的依赖关系只通过构造函数注入,因此单元测试非常容易。假设我们想要测试TkStatus
,它应该返回一个HTML响应(我正在使用JUnit 4和Hamcrest)。
此外,我们可以在一个测试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