关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » JAVA »自己编写Java Web框架:Takes框架的Web App架构

    自己编写Java Web框架:Takes框架的Web App架构

    2015-05-30 00:00:00 出处:张纪刚的博客
    分享

    我用过Servlets、JSP、JAX-RS、 Spring框架、Play框架、带Facelets的JSF以及Spark Framework。在我看来,这些框架并没有很好地实现面向对象设计。它们充斥着静态方法、未经测试的数据结构以及不够美观的解决方式。因此一个月前我决定开始编写自己的Java Web框架,我制定了一些基本的信条:1) 没有NULL,2) 没有public static方法,3) 没有可变类(mutable class),4) 没有类型转换、反射和instanceof操作。这四条基本准则应该足够保证干净的代码和透明的架构。这就是Takes框架诞生的原因。让我们看看这是如何实现的。

    Java Web架构简介

    简单来说,这就是我对一个Web应用架构以及其组件的理解。

    首先,要创建一个Web服务器,我们应该新创建一个网络套接字(socket),其将会在特定的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服务器。现在,socket已经就绪监听8080端口。当有人在浏览器打开 http://localhost:8080 ,将会建立连接并且等待的齿轮在浏览器上不停的旋转。编译这些片段试一下。我们刚刚没有使用任何框架搭建了一个简单的Web服务器。我们并没有对进入的连接做任何事情,但是也没有拒绝它们。所有的连接都正在服务器对象内部排队。这些在后台线程中完成,这就是为什么需要在最后放一个 while(true) 的原因。没有这个无限循环,应用将会立即终止操作并且服务器套接字将会关闭。

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

    final Socket socket = server.accept();

    这个方法将会一直阻塞线程等待直到一个新的连接到达。新连接一发生,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

    客户端(例如谷歌的Chrome浏览器)把这些文本传给已建立的连接。它连接本地的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();
    基本上有两步。第一步从takes创建Take的实例,第二步从takes创建响应的实例。为什么采用这种方式?主要是为了分离责任。Takes的实例负责分发请求并且初始化正确的Take,Take的实例负责创建响应。

    用Takes创建一个简单的应用,你应该创建两个类。首先,一个实现Takes接口的类:

    import org.takes.Request;
    import org.takes.Take;
    import org.takes.Takes;
    public final class TsFoo implements Takes {
      @Override
      public Take route(final Request request) {
        return new TkFoo();
      }
    }
    我们分别用Ts和Tk的前缀代表Takes和Take。第二个你应该创建的类,一个实现Take接口的类:
    import org.takes.Take;
    import org.takes.Response;
    import org.takes.rs.RsText;
    public final class TkFoo implements Take {
      @Override
      public 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类正是实现了上面解释过的和socket一样的操作。它在端口8080上启动一个服务器端的socket,通过传给构造函数TsFoo实例来分发所有进入的连接。它在一个无限循环中完成分发,用Exit实例每秒检查是否是时候停止。显然,Exit.NEVER总是返回“请不要停止”。

    HTTP请求

    现在让我们来了解一下到达TsFoo的HTTP请求内部都有什么,我们能从请求中得到什么。下面是在Takes中定义的Request接口:

    public interface Request {
      Iterable<String> head() throws IOException;
      InputStream body() throws IOException;
    }

    请求分为两部分:头部和正文。根据RFC 2616中HTTP规范,头部包含用来开始正文的空行前的所有的行。框架中有很多有用的请求装饰器。例如,RqMethod可以帮助从头部第一行取到方法名。

    final String method = new RqMethod(request).method();

    RqHref用来帮助提取查询部分并且进行解析。例如,下面是一个请求:

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

    代码将会提取得到“123”:

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

    RqPrint可以获取整个请求或者正文,作为字符串打印出来:

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

    这里的想法是保持请求接口简单,并且用装饰器提供解析请求的功能。每一个装饰器都非常小巧稳定,只用来完成一件事。所有这些装饰器都在“org.takes.rq”包中。你可能已经理解,“Rq”前缀代表请求(Request)。

    第一个真正的Web应用

    让我们创建我们第一个真正意义上的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(翻译成“front-end with command line interface”即“带命令行接口的前端”)创建了FtBasic的实例,用一些有用的装饰器对它进行包装并根据命令行参数配置。例如,“–port=8080”将会转换成8080端口号并被当做 FtBasic 构造函数的第二个参数传入。

    web应用本身继承TsWrap,叫做TsApp:

    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。在仓库的根目录下创建一个Profile文件,然后把仓库推入Heroku。下面是Profile的内容:

    web: java -Dfile.encoding=UTF-8 -cp target/foo.jar:target/deps/* foo.Entry --port=${PORT}

    TsFork

    TsFork类看上去是其中一个框架核心元素。它将进入的HTTP请求路由到正确的“take”。它的逻辑非常的简单,代码也只有少量行。它封装了“forks”的一个集合,“forks”是Fork<Take>接口的实例。

    public interface Fork<T> {
      Iterator<T> route(Request req) throws IOException;
    }

    仅有的 route() 方法返回空迭代器或者含有单个take的迭代器。TsFork遍历所有的forks,调用它们的 route() 方法直到其中一个返回take。一旦发生,TsFork会把这个take返回给调用者,即 FtBasic。

    现在我们自己来创建一个简单的fork。例如,当请求URL“/status”时,我们想展示应用的状态。以下是代码实现:

    final class TsApp extends TsWrap {
      private static Takes make() {
        return new TsFork(
          new Fork.AtTake() {
            @Override
            public 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将尝试在集合中寻找另一个这样的fork,它可以获得Take的实例从而进行响应。顺便提一下,如果什么也没发现所有的forks返回空迭代器,那么TsFork将抛出“Page not found”的异常。

    这样的逻辑通过叫做FkRegex的开箱即用fork实现,尝试用提供的通用表达式去匹配请求的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())
            )
          )
        );
      }
    }

    Again, I believe it’s obvious. The instance of FkRegex will ask an encapsulated instance of TsFork to return a take, and it will try to fetch it from one that FkParams encapsulated. If the HTTP query is /status f=xml, an instance of TkStatusXML will be returned.

    我相信逻辑是很清晰的。FkRegex的实例将会要求TsFork的封装实例返回一个take,并且它会尝试从FkParams封装的实例中获取。

    HTTP响应

    现在让我们讨论HTTP响应的结构以及它的面向对象的抽象—— Response。以下是接口的定义:

    public interface Response {
      Iterable<String> head() throws IOException;
      InputStream body() throws IOException;
    }

    和Request看起来非常类似,是不是?好吧,它是相同的。因为HTTP请求和响应的结构几乎是相同的,唯一的区别只是第一行。有很多有用的装饰器帮助构建响应。他们是组件化的,这使得使用起来非常方便。例如,如果你想构建一个包含HTML页面的响应,你可以这样做:

    final class TkIndex implements Take {
      @Override
      public 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”。

    你可以复用已有的装饰器来创建自己的装饰器。可以看看 rultor.com 上 RsPage 如何自定义装饰器。

    如何使用模板?

    如你所见,返回简单的“Hello, world”页面并不是一个大问题。但是返回更复杂的输出例如HTML页面、XML文档、JSON数据集,又该怎么办?让我们从一个简单的模板引擎“Velocity”开始。好吧,其实它并不简单。它相当强大,但是我只建议在简单情形下使用。下面是关于它如何工作:

    final class TkIndex implements Take {
      @Override
      public Response act() {
        return new RsVelocity("Hello, ${name}")
          .with("name", "Jeffrey");
      }
    }

    RsVelocity 构造器接受Velocity模板作为唯一参数。然后,你可以调用“with()”方法,往Velocity上下文注入数据。当到渲染HTTP响应的时候,RsVelocity 将会将模板和配置的上下文进行“评估”。再次强调,我只推荐在非常简单的输出时使用这种模板方式。

    对于更复杂的HTML文档,我将推荐你使用结合Xembly使用XML/XSLT。在先前的几篇博客中我解释了这种想法,XML+XSLT in a Browser 和RESTful API and a Web Site in the Same URL。这种方式简单强大——用Java生成XML,XSLT 处理器将其转换成HTML文档。这就是我们如何分离表示和数据。在MVC来看,XSL样式表是一个“视图”,TkIndex 是一个“控制器”。

    不久我会单独写一篇文章来介绍使用Xembly和XSL模板生成页面。

    同时,我会在Takes框架中为 JSF/Facelets 和 JSP 渲染创建装饰器。如果你对这部分工作感兴趣,请fork这个框架并提交你的pull请求。

    如何持久化?

    现在,一个问题就出来了。如何处理诸如数据库、内存结构、网络连接之类的持久层实体?我的建议是在Entry类中实例化它们,并把它们作为参数传入TsApp的构造函数中。然后,TsApp将会把它们传入自定义的“takes”的构造函数中。

    例如,我们有一个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的构造器必须接受一个“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类型的参数。为了取SQL表数据并把它转换成HTML,相信你知道TkIndex内部如何处理的。这里的关键点是在应用(TsApp类的实例)初始化时必须注入依赖。这是纯粹干净的依赖注入机制,完全无需任何容器。更多相关阅读请参阅“Dependency Injection Containers Are Code Polluters”。

    单元测试

    因为每个类是不可变的并且所有的依赖都是通过构造函数注入,所以单元测试非常简单。比如我们想测试“TkStatus”,假定它将会返回一个HTML响应(我使用JUnit 4 和Hamcrest):

    import org.junit.Test;
    import org.hamcrest.MatcherAssert;
    import org.hamcrest.Matchers;
    public final class TkIndexTest {
      @Test
      public void returnsHtmlPage() throws Exception {
        MatcherAssert.assertThat(
          new RsPrint(
            new TkStatus().act()
          ).printBody(),
          Matchers.equalsTo("<html>Hello, world!</html>")
        );
      }
    }

    同样,我们可以在一个测试HTTP服务器中启动整个应用或者任何一个单独的“take”,然后通过真实的TCP套接字测试它的行为;例如(我使用jcabi-http构造HTTP请求并且检测输出):

    public final class TkIndexTest {
      @Test
      public void returnsHtmlPage() throws Exception {
        new FtRemote(new TsFixed(new TkIndex())).exec(
          new FtRemote.Script() {
            @Override
            public 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”都可以作为一个单独的组件被测试,绝对独立于框架和其它“takes”。

    为什么叫这个名字?

    这是我听到最频繁的问题。想法很简单,它和电影有关。当制作一部电影时,工作人员为了捕捉现实会拍摄很多镜头然后放入电影中。每一个拍摄称作一个镜头(take)。

    换句话说,一个镜头就像现实的一个快照。每一个镜头实例代表特定时刻的一个事实。这个事实然后以响应的形式发送给用户。

    同样的道理也适用于框架。每个Take实例都代表着特定某个时刻的真实存在。这个信息会以Response形式发送。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    小米路由器买哪款?Miwifi热门路由器型号对比分析

    DESTOON标签(tag)调用手册说明(最新版)

    Destoon 9.0全站伪静态规则设置清单(Apache版)

    Destoon 9.0全站伪静态规则设置清单(Nginx版)

    Destoon 8.0全站伪静态规则设置清单(Apache版)

    Destoon 8.0全站伪静态规则设置清单(Nginx版)

    Destoon会员公司地址伪静态com/目录如何修改?两步轻松搞定,适合Nginx和Apache

    Python 并行处理列表的常见方法及其优缺点分析

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    IT头条

    StorONE 的高效平台将 Storage Guardian 数据中心占用空间减少 80%

    11:03

    年赚千亿的印度能源巨头Nayara 云服务瘫痪,被微软卡了一下脖子

    12:54

    国产6nm GPU新突破!砺算科技官宣:自研TrueGPU架构7月26日发布

    01:57

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    技术热点

    最常用的 Eclipse 快捷键整理

    多表多查询条件对SQL Server查询性能的优化

    浅谈如何优化SQL Server服务器

    HTTP 协议中使用 Referer Meta 标签控制 referer

    好用的mysql备份工具

    Android开发中的MVP架构详解

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。