我曾经利用Servlet,JSP,JAX-RS,Spring框架,Play框架,带有Facelets的JSF和一些Spark框架。 以我的拙见,所有这些解决方案都远非面向对象和优雅的。 它们都充满了静态方法,无法测试的数据结构和肮脏的骇客。 因此,大约一个月前,我决定创建自己的Java Web框架。 我将一些基本原则纳入其基础:1)没有NULL,2)没有公共静态方法,3)没有可变的类以及4)没有类的转换,反射和instanceof
运算符。 这四个基本原则应保证干净的代码和透明的体系结构。 这就是Takes框架的诞生方式。 让我们看看创建了什么以及它如何工作。
简而言之,Java Web体系结构
简单来说,这就是我理解Web应用程序体系结构及其组件的方式。
首先,要创建Web服务器,我们应该创建一个新的网络套接字 ,该套接字在某个TCP端口上接受连接。 通常是80,但是我将使用8080进行测试。 这是通过Java使用ServerSocket
类完成的:
import java.net.ServerSocket;
public class Foo {public static void main(final String... args) throws Exception {final ServerSocket server = new ServerSocket(8080);while (true);}
}
这足以启动Web服务器。 现在,套接字已准备就绪并且正在侦听端口8080。当有人在其浏览器中打开http://localhost:8080
时,将建立连接,浏览器将永远旋转其等待轮。 编译此代码段,然后尝试。 我们只是构建了一个简单的Web服务器,而没有使用任何框架。 我们尚未对传入的连接做任何事情,但是我们也不拒绝它们。 所有这些都在该server
对象内对齐。 它是在后台线程中完成的。 这就是为什么我们需要将while(true)
放在后面。 没有这种无休止的暂停,该应用程序将立即完成其执行,并且服务器套接字将关闭。
下一步是接受传入的连接。 在Java中,这是通过对accept()
方法的阻塞调用来完成的:
final Socket socket = server.accept();
该方法正在阻塞其线程,并等待新的连接到达。 一旦发生这种情况,它将返回Socket
的实例。 为了接受下一个连接,我们应该再次调用accept()
。 因此,基本上,我们的Web服务器应该像这样工作:
public class Foo {public static void main(final String... args) throws Exception {final ServerSocket server = new ServerSocket(8080);while (true) {final Socket socket = server.accept();// 1. Read HTTP request from the socket// 2. Prepare an HTTP response// 3. Send HTTP response to the socket// 4. Close the socket}}
}
这是一个无休止的循环,接受一个新的连接,理解它,创建一个响应,返回响应,然后再次接受一个新的连接。 HTTP协议是无状态的,这意味着服务器不应记住任何先前连接中发生的情况。 它关心的只是此特定连接中的传入HTTP请求。
HTTP请求来自套接字的输入流,看起来像是多行文本块。 如果读取套接字的输入流,将看到以下内容:
final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())
);
while (true) {final String line = reader.readLine();if (line.isEmpty()) {break;}System.out.println(line);
}
您将看到如下内容:
GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,ru;q=0.6,uk;q=0.4
客户端(例如Google Chrome浏览器)将此文本传递到已建立的连接中。 它连接到localhost
端口8080,一旦连接就绪,它将立即将文本发送到其中,然后等待响应。
我们的工作是使用在请求中获得的信息来创建HTTP响应。 如果我们的服务器非常原始,那么我们基本上可以忽略请求中的所有信息,而只需返回“ Hello,world!”。 到所有请求(为简单起见,我使用IOUtils
):
import java.net.Socket;
import java.net.ServerSocket;
import org.apache.commons.io.IOUtils;
public class Foo {public static void main(final String... args) throws Exception {final ServerSocket server = new ServerSocket(8080);while (true) {try (final Socket socket = server.accept()) {IOUtils.copy(IOUtils.toInputStream("HTTP/1.1 200 OK\r\n\r\nHello, world!"),socket.getOutputStream());}}}
}
而已。 服务器已准备就绪。 尝试编译并运行它。 将浏览器指向http:// localhost:8080 ,您将看到Hello, world!
:
$ javac -cp commons-io.jar Foo.java
$ java -cp commons-io.jar:. Foo &
$ curl http://localhost:8080 -v
* Rebuilt URL to: http://localhost:8080/
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
* no chunk, no close, no size. Assume close to signal end
<
* Closing connection 0
Hello, world!
这就是构建Web服务器所需的全部。 现在让我们讨论如何使其面向对象和可组合。 让我们尝试看看Takes框架是如何构建的。
路由/调度
最重要的步骤是确定谁负责构建HTTP响应。 每个HTTP请求都具有1)查询,2)方法和3)许多标头。 使用这三个参数,我们需要实例化一个将为我们构建响应的对象。 在大多数Web框架中,此过程称为请求分派或路由。 这是我们在Takes中的做法:
final Take take = takes.route(request);
final Response response = take.act();
基本上有两个步骤。 第一个是创建的一个实例Take
从takes
,而第二个是创建的实例Response
从take
。 为什么这样做呢? 主要是为了分开职责。 实例Takes
负责调度请求和实例右Take
,和实例Take
负责创建响应。
要在Takes中创建一个简单的应用程序,您应该创建两个类。 首先,执行Takes
:
import org.takes.Request;
import org.takes.Take;
import org.takes.Takes;
public final class TsFoo implements Takes {@Overridepublic Take route(final Request request) {return new TkFoo();}
}
我们分别为Takes
和Take
使用这些Ts
和Tk
前缀。 您应该创建的第二个类是Take
的实现:
import org.takes.Take;
import org.takes.Response;
import org.takes.rs.RsText;
public final class TkFoo implements Take {@Overridepublic Response act() {return new RsText("Hello, world!");}
}
现在是时候启动服务器了:
import org.takes.http.Exit;
import org.takes.http.FtBasic;
public class Foo {public static void main(final String... args) throws Exception {new FtBasic(new TsFoo(), 8080).start(Exit.NEVER);}
}
该FtBasic
类执行与上述完全相同的套接字操作。 它在端口8080上启动服务器套接字,并通过我们提供给其构造函数的TsFoo
实例调度所有传入的连接。 它以无休止的周期进行此分派,每秒检查一次是否应该使用Exit
实例停止。 显然, Exit.NEVER
始终不会回答“请别停下来”。
HTTP请求
现在,让我们看看到达TsFoo
的HTTP请求中TsFoo
什么以及我们可以从中获得什么。 这是在Takes中定义Request
接口的方式:
public interface Request {Iterable<String> head() throws IOException;InputStream body() throws IOException;
}
该请求分为两部分:头部和身体。 根据RFC 2616中的 HTTP规范,头部包含开始于正文的空行之前的所有行。 框架中有许多有用的装饰器用于Request
。 例如, RqMethod
将帮助您从标题的第一行获取方法名称:
final String method = new RqMethod(request).method();
RqHref
将帮助提取查询部分并进行解析。 例如,这是请求:
GET /user?id=123 HTTP/1.1
Host: www.example.com
此代码将提取123
:
final int id = Integer.parseInt(new RqHref(request).href().param("id").get(0)
);
RqPrint
可以将整个请求或其主体打印为String
:
final String body = new RqPrint(request).printBody();
这里的想法是使Request
接口保持简单,并向其装饰器提供此请求解析功能。 这种方法有助于框架使类保持较小且具有凝聚力。 每个装饰器都非常小巧,坚固,只能做一件事。 所有这些装饰器都在org.takes.rq
包中。 您可能已经知道, Rq
前缀代表Request
。
第一个Real Web App
让我们创建第一个真正的Web应用程序,它将做一些有用的事情。 我建议从Entry
类开始,这是Java从命令行启动应用程序所必需的:
import org.takes.http.Exit;
import org.takes.http.FtCLI;
public final class Entry {public static void main(final String... args) throws Exception {new FtCLI(new TsApp(), args).start(Exit.NEVER);}
}
此类仅包含一个main()
静态方法,当应用程序从命令行启动时,JVM将调用该方法。 如您所见,它实例化FtCLI
,为它提供类TsApp
和命令行参数的实例。 我们稍后将创建TsApp
类。 FtCLI
(转换为“带有命令行界面的前端”)创建相同FtBasic
的实例,将其包装到一些有用的修饰符中,并根据命令行参数进行配置。 例如,-- --port=8080
将转换为8080
端口号,并作为FtBasic
构造函数的第二个参数传递。
该Web应用程序本身称为TsApp
并扩展了TsWrap
:
import org.takes.Take;
import org.takes.Takes;
import org.takes.facets.fork.FkRegex;
import org.takes.facets.fork.TsFork;
import org.takes.ts.TsWrap;
import org.takes.ts.TsClasspath;
final class TsApp extends TsWrap {TsApp() {super(TsApp.make());}private static Takes make() {return new TsFork(new FkRegex("/robots.txt", ""),new FkRegex("/css/.*", new TsClasspath()),new FkRegex("/", new TkIndex()));}
}
我们将在稍后讨论此TsFork
课程。
如果您使用的是Maven,则应以pom.xml
开头:
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>foo</groupId><artifactId>foo</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>org.takes</groupId><artifactId>takes</artifactId><version>0.9</version> <!-- check the latest in Maven Central --></dependency></dependencies><build><finalName>foo</finalName><plugins><plugin><artifactId>maven-dependency-plugin</artifactId><executions><execution><goals><goal>copy-dependencies</goal></goals><configuration><outputDirectory>${project.build.directory}/deps</outputDirectory></configuration></execution></executions></plugin></plugins></build>
</project>
运行mvn clean package
应该在target
目录中构建foo.jar
文件,并在target/deps
构建所有JAR依赖项的集合。 现在,您可以从命令行运行该应用程序:
$ mvn clean package
$ java -Dfile.encoding=UTF-8 -cp ./target/foo.jar:./target/deps/* foo.Entry --port=8080
该应用程序已准备就绪,您可以将其部署到Heroku。 只需在存储库的根目录中创建一个Procfile
文件,然后将存储库推送到Heroku。 这是Procfile
样子:
web: java -Dfile.encoding=UTF-8 -cp target/foo.jar:target/deps/* foo.Entry --port=${PORT}
叉车
这个TsFork
类似乎是框架的核心元素之一。 它有助于路线传入的HTTP请求到右收 。 它的逻辑非常简单,并且里面只有几行代码。 它封装了“ forks”的集合,它们是Fork<Take>
接口的实例:
public interface Fork<T> {Iterator<T> route(Request req) throws IOException;
}
它唯一的route()
方法要么返回一个空的迭代器,要么返回一个带有单个Take
的迭代器。 TsFork
遍历所有fork,调用它们的route()
方法,直到其中一个返回take 。 一旦出现这种情况, TsFork
返回此取给调用者,这是FtBasic
。
现在让我们自己创建一个简单的fork。 例如,当请求/status
URL时,我们想显示应用程序的/status
。 这是代码:
final class TsApp extends TsWrap {private static Takes make() {return new TsFork(new Fork.AtTake() {@Overridepublic Iterator<Take> route(Request req) {final Collection<Take> takes = new ArrayList<>(1);if (new RqHref(req).href().path().equals("/status")) {takes.add(new TkStatus());}return takes.iterator();}});}
}
我相信这里的逻辑很明确。 我们要么返回一个空的迭代器,要么返回一个内部带有TkStatus
实例的迭代器。 如果返回一个空的迭代器,则TsFork
将尝试在集合中找到另一个实际上获取Take
实例的fork,以产生Response
。 顺便说一句,如果未找到任何内容,并且所有派生都返回空的迭代器,则TsFork
将引发“找不到页面”异常。
这种精确的逻辑由一个名为FkRegex
即用即用的叉子FkRegex
,它尝试将请求URI路径与提供的正则表达式进行匹配:
final class TsApp extends TsWrap {private static Takes make() {return new TsFork(new FkRegex("/status", new TkStatus()));}
}
我们可以组成TsFork
类的多层结构。 例如:
final class TsApp extends TsWrap {private static Takes make() {return new TsFork(new FkRegex("/status",new TsFork(new FkParams("f", "json", new TkStatusJSON()),new FkParams("f", "xml", new TkStatusXML()))));}
}
同样,我认为这很明显。 实例FkRegex
会问的一个封装实例TsFork
返回一个take,它会尝试从一个获取它FkParams
封装。 如果HTTP查询为/status?f=xml
,则将返回TkStatusXML
的实例。
HTTP响应
现在让我们讨论HTTP响应的结构及其面向对象的抽象Response
。 界面外观如下:
public interface Response {Iterable<String> head() throws IOException;InputStream body() throws IOException;
}
看起来非常类似于Request
,不是吗? 好吧,它是相同的,主要是因为HTTP请求和响应的结构几乎相同。 唯一的区别是第一行。
有很多有用的装饰器,可以帮助您建立响应。 它们是可组合的 ,这使它们非常方便。 例如,如果要构建一个包含HTML页面的响应,则可以这样编写它们:
final class TkIndex implements Take {@Overridepublic Response act() {return new RsWithStatus(new RsWithType(new RsWithBody("<html>Hello, world!</html>"),"text/html"),200);}
}
在此示例中,装饰器RsWithBody
创建一个带有主体但根本没有标题的响应。 然后, RsWithType
添加标题Content-Type: text/html
。 然后, RsWithStatus
确保响应的第一行包含HTTP/1.1 200 OK
。
您可以创建自己的装饰器,以重用现有的装饰器。 看看RsPage
在RsPage
是如何完成的。
模板如何?
如我们所见,返回简单的“ Hello,world”页面不是什么大问题。 但是,诸如HTML页面,XML文档,JSON数据集等更复杂的输出呢? 有一些方便的Response
装饰器可以实现所有功能。 让我们从简单的模板引擎Velocity开始。 好吧,这不是那么简单。 它相当强大,但是我建议仅在简单情况下使用它。 下面是它的工作原理:
final class TkIndex implements Take {@Overridepublic Response act() {return new RsVelocity("Hello, ${name}").with("name", "Jeffrey");}
}
RsVelocity
构造函数接受必须为Velocity模板的单个参数。 然后,调用with()
方法,将数据注入Velocity上下文中。 当需要呈现HTTP响应时, RsVelocity
将根据配置的上下文“评估”模板。 同样,我建议您仅对简单输出使用此模板方法。
对于更复杂HTML文档,我建议您将XML / XSLT与Xembly结合使用。 我在之前的几篇文章中对此想法进行了解释: 浏览器和RESTful API 中的XML + XSLT,以及同一URL中的网站 。 它简单而强大-Java生成XML输出,而XSLT处理器将其转换为HTML文档。 这就是我们将表示形式与数据分开的方式。 就MVC而言,XSL样式表是“视图”, TkIndex
是“控制器”。
我将很快写另一篇关于Xembly和XSL模板的文章。
同时,我们将在Takes中为JSF / Facelets和JSP渲染创建装饰器。 如果您有兴趣提供帮助,请分叉框架并提交请求。
持久性呢?
现在,出现的一个问题是如何处理持久性实体,例如数据库,内存结构,网络连接等。我的建议是在Entry
类内部对其进行初始化,并将其作为参数传递给TsApp
构造函数。 然后, TsApp
将它们传递到构造函数的定制需要 。
例如,我们有一个PostgreSQL数据库,其中包含一些需要渲染的表数据。 这是在Entry
类中初始化与它的连接的方式(我使用的是BoneCP连接池):
public final class Entry {public static void main(final String... args) throws Exception {new FtCLI(new TsApp(Entry.postgres()), args).start(Exit.NEVER);}private static Source postgres() {final BoneCPDataSource src = new BoneCPDataSource();src.setDriverClass("org.postgresql.Driver");src.setJdbcUrl("jdbc:postgresql://localhost/db");src.setUser("root");src.setPassword("super-secret-password");return src;}
}
现在, TsApp
的构造TsApp
必须接受类型为java.sql.Source
的单个参数:
final class TsApp extends TsWrap {TsApp(final Source source) {super(TsApp.make(source));}private static Takes make(final Source source) {return new TsFork(new FkRegex("/", new TkIndex(source)));}
}
TkIndex
类还接受Source
类的单个参数。 我相信您知道如何在TkIndex
中使用它来获取SQL表数据并将其转换为HTML。 这里的要点是,在实例化依赖项时,必须将其注入到应用程序中(类TsApp
的实例)。 这是一种纯净的依赖注入机制,它绝对没有容器。 在“依赖注入容器是代码污染者”中阅读有关它的更多信息。
单元测试
由于每个类都是不可变的,并且所有依赖项仅通过构造函数注入,因此单元测试非常容易。 假设我们要测试TkStatus
,它应该返回HTML响应(我正在使用JUnit 4和Hamcrest ):
import org.junit.Test;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
public final class TkIndexTest {@Testpublic void returnsHtmlPage() throws Exception {MatcherAssert.assertThat(new RsPrint(new TkStatus().act()).printBody(),Matchers.equalsTo("<html>Hello, world!</html>"));}
}
此外,我们可以开始在测试HTTP服务器的整个应用程序或任何个人起飞 ,并通过一个真实的TCP套接字测试它的行为; 例如(我正在使用jcabi-http发出HTTP请求并检查输出):
public final class TkIndexTest {@Testpublic void returnsHtmlPage() throws Exception {new FtRemote(new TsFixed(new TkIndex())).exec(new FtRemote.Script() {@Overridepublic void exec(final URI home) throws IOException {new JdkRequest(home).fetch().as(RestResponse.class).assertStatus(HttpURLConnection.HTTP_OK).assertBody(Matchers.containsString("Hello, world!"));}});}
}
FtRemote
在随机的TCP端口启动测试Web服务器,并在提供的FtRemote.Script
实例上调用exec()
方法。 此方法的第一个参数是刚启动的Web服务器主页的URI。
Takes框架的体系结构非常模块化且可组合。 任何个体取可以进行测试作为一个独立的部件,绝对独立于框架和其他需要 。
为什么叫名字?
这就是我经常听到的问题。 这个想法很简单,它起源于电影业。 当影片制成,剧组芽许多需要以捕捉现实,把它放在电影。 每次捕获称为一次获取 。
换句话说, 拍摄就像现实的快照。
同样适用于此框架。 Take
每个实例在某个特定时刻代表一个现实。 然后,将该现实以Response
的形式发送给用户。
翻译自: https://www.javacodegeeks.com/2015/04/java-web-app-architecture-in-takes-framework.html