1. 概述

Java 生态中不乏优秀的 Web 框架,比如我们耳熟能详的 SpringPlayGrails。但要说完全遵循不可变性纯面向对象设计的,几乎没有。

本文带你了解一个另类框架 —— Takes。我们将用它搭建一个简单的 Web 应用,涵盖路由、请求/响应处理、异常处理以及单元测试等核心功能。

踩坑提示:Takes 的设计理念非常“函数式”和“纯 OOP”,如果你习惯了 Spring 的注解和配置,刚上手可能会觉得“反人类”,但理解其哲学后,代码会变得异常清晰。

2. Takes 框架核心理念

Takes 是一个基于 Java 8 的不可变 Web 框架,它的设计非常“洁癖”:

完全避免使用 null
禁用 public static 方法
不支持可变类(mutable classes)
禁止类型强制转换(casting)和反射(reflection)

正因为如此,Takes 才敢宣称自己是一个“真正面向对象”的框架。它不依赖任何 XML 或 YAML 配置文件,开箱即用,内置了 JSON/XML 响应、模板渲染等常用功能。

简单粗暴地说:一切皆对象,一切皆不可变

3. 环境搭建

首先,在 pom.xml 中引入 Takes 依赖:

<dependency>
    <groupId>org.takes</groupId>
    <artifactId>takes</artifactId>
    <version>1.19</version>
</dependency>

接着,创建一个实现 Take 接口的类:

public class TakesHelloWorld implements Take {
    @Override
    public Response act(Request req) {
        return new RsText("Hello, world!");
    }
}

Take 接口是框架的核心抽象,每个 Take 都是一个请求处理器,通过 act 方法接收请求并返回响应。

这里我们使用 RsText 类,将纯文本 "Hello, world!" 作为响应内容。

然后,创建启动类 TakesApp

public class TakesApp {
    public static void main(String... args) {
        new FtBasic(new TakesHelloWorld()).start(Exit.NEVER);
    }
}

FtBasicFront 接口的一个基础实现,负责启动内嵌的 Web 服务,并将请求转发给指定的 Take

⚠️ Takes 使用 ServerSocket 自研了一套无状态 Web 服务,默认监听 80 端口。我们可以在代码中指定端口:

new FtBasic(new TakesHelloWorld(), 6060).start(Exit.NEVER);

或者通过命令行参数传入:

--port=6060

最后,用 Maven 编译打包:

mvn clean package

就可以在 IDE 中直接运行 TakesApp 了。

4. 运行方式

除了在 IDE 中运行,也可以作为独立服务启动。

4.1. 命令行运行

先编译:

javac -cp "takes.jar:." com.baeldung.takes.*

再运行:

java -cp "takes.jar:." com.baeldung.takes.TakesApp --port=6060

4.2. 使用 Maven 插件

推荐使用 exec-maven-plugin,方便集成:

<profiles>
    <profile>
        <id>reload</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.1.0</version>
                    <executions>
                        <execution>
                            <id>start-server</id>
                            <phase>pre-integration-test</phase>
                            <goals>
                                <goal>java</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <mainClass>com.baeldung.takes.TakesApp</mainClass>
                        <cleanupDaemonThreads>false</cleanupDaemonThreads>
                        <arguments>
                            <argument>--port=${port}</argument>
                        </arguments>
                   </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

启动命令:

mvn clean integration-test -Preload -Dport=6060

这样就能在 CI/CD 环境中轻松集成。

5. 路由配置

Takes 使用 TkFork 类实现请求路由,通过匹配规则将请求分发给不同的 Take

示例:为根路径 //contact 添加路由:

public static void main(String... args) {
    new FtBasic(
        new TkFork(
            new FkRegex("/", new TakesHelloWorld()),
            new FkRegex("/contact", new TakesContact())
        ), 6060
    ).start(Exit.NEVER);
}

其中 FkRegex 使用正则表达式匹配请求路径,逻辑清晰,扩展性强。

6. 请求处理

Takes 在 org.takes.rq 包中提供了丰富的装饰器类来处理 HTTP 请求。

常用方式如下:

  • 获取请求方法

    String requestMethod = new RqMethod.Base(req).method(); 
    
  • 获取请求头

    Iterable<String> requestHeaders = new RqHeaders.Base(req).head();
    
  • 获取请求体

    String body = new RqPrint(req).printBody();
    
  • 获取表单参数

    String username = new RqFormSmart(req).single("username");
    

这些装饰器都遵循“装饰器模式”,可以链式调用,组合使用,非常灵活。

7. 响应处理

响应处理在 org.takes.rs 包中,同样采用装饰器模式。

核心装饰器包括:

  • 设置状态码

    Response resp = new RsWithStatus(200);
    

    验证:

    assertEquals("[HTTP/1.1 200 OK], ", resp.head().toString());
    
  • 设置 Content-Type

    Response resp = new RsWithType(new RsEmpty(), "text/html");
    
  • 设置响应体

    new RsWithBody("Contact us at https://www.baeldung.com")
    

组合使用示例:构建一个完整的 HTML 响应:

public class TakesContact implements Take {
    @Override
    public Response act(Request req) throws IOException {
        return new RsWithStatus(
          new RsWithType(
            new RsWithBody("Contact us at https://www.baeldung.com"), 
            "text/html"), 200);
    }
}
  • 返回 JSON
    @Override 
    public Response act(Request req) { 
      JsonStructure json = Json.createObjectBuilder() 
        .add("id", rs.getInt("id")) 
        .add("user", rs.getString("user")) 
        .build(); 
      return new RsJson(json); 
    }
    

8. 异常处理

Takes 通过 Fallback 接口处理异常场景,并提供多种实现。

  • 处理 404

    new FtBasic(
      new TkFallback(
        new TkFork(
          new FkRegex("/", new TakesHelloWorld()),
          // ...
          ),
          new FbStatus(404, new RsText("Page Not Found"))
      ), 6060
    ).start(Exit.NEVER);
    
  • 组合多个 fallback(如 404 和 405):

    new TkFallback(
      new TkFork( /* routes */ ),
      new FbChain(
        new FbStatus(404, new RsText("Page Not Found")),
        new FbStatus(405, new RsText("Method Not Allowed"))
      )
    ), 6060
    ).start(Exit.NEVER);
    
  • 自定义异常处理逻辑

    new FbChain(
      new FbStatus(404, new RsText("Page Not Found")),
      new FbStatus(405, new RsText("Method Not Allowed")),
      new Fallback() {
          @Override
          public Opt<Response> route(RqFallback req) {
            return new Opt.Single<>(new RsText(req.throwable().getMessage()));
          }
      }
    )
    

这种设计将错误处理也“对象化”,保持了整体设计的一致性。

9. 模板集成

Takes 支持与 Apache Velocity 集成,实现动态页面渲染。

先引入依赖:

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.2</version>
</dependency>

然后使用 RsVelocity 渲染模板:

  • 内联模板

    public class TakesIndex implements Take {
      @Override
      public Response act(Request req) throws IOException {
          return new RsHtml(
              new RsVelocity("${username}", new RsVelocity.Pair("username", "Baeldung"))
          );
      }
    }
    
  • 使用外部模板文件

    new RsVelocity(
      this.getClass().getResource("/templates/index.vm"), 
      new RsVelocity.Pair("username", username)
    )
    

最终用 RsHtml 包装返回 HTML 响应。

10. 单元测试

Takes 提供 RqFake 类用于创建伪造请求,方便单元测试。

示例:测试 TakesContact

String resp = new RsPrint(new TakesContact().act(new RqFake())).printBody();
assertEquals("Contact us at https://www.baeldung.com", resp);

RsPrint 用于提取响应体内容,测试轻量高效。

11. 集成测试

使用 FtRemote 可启动一个临时服务器,在随机端口运行 Take,适合集成测试。

示例:

new FtRemote(new TakesContact()).exec(
    new FtRemote.Script() {
        @Override
        public void exec(URI home) throws IOException {
            HttpClient client = HttpClientBuilder.create().build();    
            HttpResponse response = client.execute(new HttpGet(home));
            int statusCode = response.getStatusLine().getStatusCode();
            HttpEntity entity = response.getEntity();
            String result = EntityUtils.toString(entity);
            
            assertEquals(200, statusCode);
            assertEquals("Contact us at https://www.baeldung.com", result);
        }
    });

这里使用 Apache HttpClient 发起真实请求,验证端到端行为。

12. 总结

本文带你快速上手 Takes 框架,从环境搭建到路由、请求/响应处理、异常处理,再到单元和集成测试,覆盖了核心开发流程。

Takes 的设计哲学非常独特:不可变 + 纯对象 + 无配置。虽然学习曲线较陡,但一旦适应,代码的可读性和可测试性会大幅提升。

所有示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/web-modules/java-takes


原始标题:Introduction to Takes