如何从头开始以正确的面向对象方式创建Java Web Framework

您如何用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存储库并在那里分享您的想法。

我建议我们引入两个接口: ResourceOutput 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

你可能感兴趣的:(如何从头开始以正确的面向对象方式创建Java Web Framework)