您如何用Java设计Web应用程序? 您安装了Spring,阅读了手册,创建了控制器 ,创建了一些视图,添加了一些注释 ,它就可以工作了。 如果没有Spring (Ruby中没有Ruby on Rails,PHP中没有Symphony,也没有…等),您将怎么办? 让我们尝试从头开始创建一个Web应用程序,从一个纯Java SDK到一个功能齐全的Web应用程序(由单元测试覆盖)结束。 几周前,我录制了第42号网络研讨会 ,但本文应该对此进行更详细的说明。
蒂芙尼早餐(Blake Edwards,1961年)
首先,我们必须创建一个HTTP服务器,该服务器将打开服务器套接字,侦听传入的连接,读取他们必须说的所有内容(HTTP请求)并返回任何Web浏览器想要的信息(HTTP响应)。 您知道HTTP的工作原理吧? 如果您不这样做,这里有个简短的提醒:
Web浏览器向服务器发送请求,该请求看起来像这样(这是纯文本数据):
GET /index.html HTTP/1.1
Host: www.example.com
服务器必须阅读此文本,准备答案(必须是浏览器可读HTML页面),然后像这样返回:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Hello, world!
而已。 这是一个非常简单的原始协议。 用Java实现Web服务器也不是那么复杂。 这是一个非常简单的形式:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Arrays;
public class Main {
public static void main(String... argv) {
try (ServerSocket server = new ServerSocket(8080)) {
server.setSoTimeout(1000);
while (true) {
try (Socket socket = server.accept()) {
try (InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream()) {
byte[] buffer = new byte[10000];
int total = input.read(buffer);
String request = new String(Arrays.copyOfRange(buffer, 0, total));
String response = "HTTP/1.1 200 OK\r\n\r\nHello, world!";
output.write(response.getBytes());
}
} catch (SocketTimeoutException ex) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
}
}
}
}
尝试运行它,它应该可以工作。 您应该能够在浏览器中打开http://localhost:8080
页面,然后看到Hello, world!
, Hello, world!
文本。
它还不是Web应用程序,而是一个框架,它可以将HTTP请求简单地分配到HTTP响应中。 尽管其中没有严重的面向对象的问题。 这是相当程序化的方法,但确实可行。 现在,我们应该关注一个更重要的问题:如何为Web应用程序添加更多功能,并使其能够处理不同的页面,呈现更大的内容并处理错误? 上面代码段中的request
变量应该以某种方式转换为response
。
最简单的方法是1)将请求转换为内部包含所有详细信息的DTO ,然后2)将其发送给知道如何处理DTO数据的“控制器”,然后3)接收响应DTO从控制器中取出数据并呈现响应。 这就是春天和 最 所有其他框架都可以做到。 但是,我们不会走这条路,我们将尝试做到无DTO且纯粹面向对象。
我不得不说,可能有多种设计,全部都是OOP风格。 现在,我仅向您显示这些选项之一。 您无疑会知道我们几年前诞生的Takes框架-它具有自己的设计,也面向对象。 但是我现在建议的那个似乎更好。 您可能还会提出其他建议,因此不要犹豫,在下面的评论中发表您的想法,甚至创建GitHub存储库并在那里分享您的想法。
我建议我们引入两个接口: Resource
和Output
。 Resource
是服务器端实体,它根据传入的请求参数而发生变化。例如,当我们只知道请求是GET /
,它就是一种资源。 但是,如果我们也知道该请求具有例如Accept: text/plain
,则可以更改该请求并创建一个新请求,该请求将传递纯文本。 这是界面:
interface Resource {
Resource refine(String name, String value);
}
这是我们创建和变异的方法:
Resource r = new DefaultResource()
.refine("X-Method", "GET")
.refine("X-Query", "/")
.refine("Accept", "text/plain");
注意:每次调用.refine()
返回一个新的接口Resource
实例。 它们都是不可变的,就像对象必须是一样 。 由于这种设计,我们不会将数据与处理器分开。 资源是数据和处理器。 每个资源都知道如何处理数据,并且仅接收应该接收的数据。 从技术上讲,我们只是以面向对象的方式实现请求调度 。
然后,我们需要将资源转换为响应。 我们赋予资源使其能够响应的能力。 我们不希望数据以某种DTO的形式泄漏资源。 我们希望该资源打印响应。 如何为资源提供其他方法print()
:
interface Resource {
Resource refine(String name, String value);
void print(Output output);
}
然后,接口Output
看起来像这样:
interface Output {
void print(String name, String value);
}
这是Output
的原始实现:
public class StringBuilderOutput implements Output {
private final StringBuilder buffer;
StringBuilderOutput(StringBuilder buf) {
this.buffer = buf;
}
@Override
public void print(String name, String value) {
if (this.buffer.length() == 0) {
this.buffer.append("HTTP/1.1 200 OK\r\n");
}
if (name.equals("X-Body")) {
this.buffer.append("\r\n").append(value);
} else {
this.buffer.append(name).append(": ").append(value).append("\r\n");
}
}
}
要构建HTTP响应,我们可以这样做:
StringBuilder builder = new StringBuilder();
Output output = new StringBuilderOutput(builder);
output.print("Content-Type", "text/plain");
output.print("Content-Length", "13");
output.print("X-Body", "Hello, world!");
System.out.println(builder.toString());
现在,让我们创建一个类,该类使用Resource
的实例作为调度程序 ,以接收传入的请求String
并生成响应String
:
public class Session {
private final Resource resource;
Session(Resource res) {
this.resource = res;
}
String response(String request) throws IOException {
Map pairs = new HashMap<>();
String[] lines = request.split("\r\n");
for (int idx = 1; idx < lines.length; ++idx) {
String[] parts = lines[idx].split(":");
pairs.put(parts[0].trim(), parts[1].trim());
if (lines[idx].empty()) {
break;
}
}
String[] parts = lines[0].split(" ");
pairs.put("X-Method", parts[0]);
pairs.put("X-Query", parts[1]);
pairs.put("X-Protocol", parts[2]);
App.Resource res = this.resource;
for (Map.Entry pair : pairs.entrySet()) {
res = res.refine(pair.getKey(), pair.getValue());
}
StringBuilder buf = new StringBuilder();
res.print(new StringBuilderOutput(buf));
return buf.toString();
}
}
首先,我们解析请求,将其标头分成几行,并忽略请求的主体。 您可以使用X-Body
作为键,修改代码以解析主体并将其传递给refine()
方法。 目前,上面的代码无法做到这一点。 但是你明白了。 片段的解析部分准备了可以在请求中找到的对,并将它们一对一地传递给封装的资源,对其进行变异直到最终形式。 始终返回文本的简单资源可能如下所示:
class TextResource implements Resource {
private final String body;
public TextResource(String text) {
this.body = text;
}
@Override
public Resource refine(String name, String value) {
return this;
}
@Override
public void print(Output output) {
output.print("Content-Type", "text/plain");
output.print("Content-Length", Integer.toString(this.body.length()));
output.print("X-Body", this.body);
}
}
根据查询的路径,关注查询字符串并将请求分派给其他资源的资源可能看起来像这样:
new Resource() {
@Override
public Resource refine(String name, String value) {
if (name.equals("X-Query")) {
if (value.equals("/")) {
return new TextResource("Hello, world!");
} else if (value.equals("/balance")) {
return new TextResource("256");
} else if (value.equals("/id")) {
return new TextResource("yegor");
} else {
return new TextResource("Not found!");
}
} else {
return this;
}
}
@Override
public void print(final Output output) {
throws IllegalStateException("This shouldn't happen");
}
}
我希望你有主意。 上面的代码很粗略,并且大多数用例都没有实现,但是如果您感兴趣,可以自己做。 该代码位于yegor256 / jpages存储库中。 请毫不犹豫地为请求请求做出贡献,并使这个小型框架成为现实。
翻译自: https://www.javacodegeeks.com/2019/03/how-to-create-a-java-web-framework-from-scratch-the-right-object-oriented-way.html