没有框架的Java中的REST API

本文的翻译是专门为该课程的学生准备的。 “ Java开发人员




Java生态系统中有许多框架和库。 尽管它们不像JavaScript中那样多,但它们不会很快过期。 但是,这使我认为我们已经忘记了如何在没有框架的情况下编写应用程序。

您可以说Spring是标准,为什么要重新发明轮子? 而且Spark是一个不错的,方便的REST框架。 或Light-rest-4jis 。 我会说你当然是对的。

但是,随着框架的发展,除了完成的功能之外,您还将获得很多魔术,学习上的困难,您很可能不会使用的其他功能以及错误。 服务中的第三方代码越多,发生错误的可能性就越大。

开源社区非常活跃,并且很有可能很快修复框架中的错误。 但是,我仍然敦促您考虑是否真的需要一个框架。 如果您有小型服务或控制台应用程序,则可能不需要它。

使用纯Java代码可以获得(或失去)什么? 考虑一下:

  • 您的代码可以更简洁,更易理解(或者,如果您是一个糟糕的程序员,可能会陷入一片混乱)
  • 您将对代码有更多的控制权,不会受到框架的限制(尽管您将不得不为框架提供的功能编写更多的代码)
  • 您的应用程序的部署和启动速度将大大提高,因为该框架不需要初始化数十个类(或者如果您混合使用某些东西(例如在多线程中),则根本不会启动)
  • 如果您将应用程序部署在Docker中,则您的映像将小得多,因为jar也将变小

我做了一个小实验,试图在没有框架的情况下开发REST API。 就学习和更新知识而言,这可能会很有趣。

当我开始编写此代码时,经常会遇到Spring没有开箱即用的功能的情况。 在这些时刻,您不必重新考虑Spring,而必须自己重新思考和开发所有内容。

我意识到,要解决实际的业务问题,我还是更喜欢使用Spring而不是重新发明轮子。 但是,我认为此练习是一次非常有趣的体验。

开始使用


我将描述每个步骤,但是我不会总是给出完整的源代码。 您可以在git存储库的单独分支中查看完整代码。

首先使用以下pom.xml创建一个新的Maven项目。

 <?xml version="1.0" encoding="UTF-8"?> <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>com.consulner.httpserver</groupId> <artifactId>pure-java-rest-api</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>11</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> </properties> <dependencies></dependencies> </project> 

添加java.xml.bind作为依赖项,因为它已在JDK 11( JEP-320 )中删除。

 <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.4.0-b180608.0325</version> </dependency> 

Jackson进行JSON序列化

 <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.7</version> </dependency> 

为了简化POJO类,我们将使用Lombok

 <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.0</version> <scope>provided</scope> </dependency> 

vavr用于功能性编程工具

 <dependency> <groupId>io.vavr</groupId> <artifactId>vavr</artifactId> <version>0.9.2</version> </dependency> 

我们还创建了主要的空类Application

步骤1分支中的源代码。

第一终点


我们的Web应用程序将基于com.sun.net.httpserver.HttpServer类。 最简单的端点/api/hello可能看起来像这样:

 package com.consulner.api; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpServer; class Application { public static void main(String[] args) throws IOException { int serverPort = 8000; HttpServer server = HttpServer.create(new InetSocketAddress(serverPort), 0); server.createContext("/api/hello", (exchange -> { String respText = "Hello!"; exchange.sendResponseHeaders(200, respText.getBytes().length); OutputStream output = exchange.getResponseBody(); output.write(respText.getBytes()); output.flush(); exchange.close(); })); server.setExecutor(null); // creates a default executor server.start(); } } 

Web服务器在端口8000上运行,并提供仅返回Hello!..的终结点,例如,可以使用curl进行检查:

curl localhost:8000/api/hello

步骤2分支中的源代码。

支持各种HTTP方法


我们的第一个端点工作正常,但是您可能会注意到,无论使用哪种HTTP方法,它始终以相同的方式响应。

例如:

 curl -X POST localhost:8000/api/hello curl -X PUT localhost:8000/api/hello 

首先要做的是添加代码以区分方法,例如:

 server.createContext("/api/hello", (exchange -> { if ("GET".equals(exchange.getRequestMethod())) { String respText = "Hello!"; exchange.sendResponseHeaders(200, respText.getBytes().length); OutputStream output = exchange.getResponseBody(); output.write(respText.getBytes()); output.flush(); } else { exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed } exchange.close(); })); 

再次尝试查询:

 curl -v -X POST localhost:8000/api/hello 

答案将是这样的:

 > POST /api/hello HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.61.0 > Accept: */* > < HTTP/1.1 405 Method Not Allowed 

还有几点需要牢记。 例如,不要忘记对OutputStream进行flush()并对于exchange close() 。 使用Spring时,我什至不必考虑它。

步骤3分支中的源代码。

解析请求参数


解析查询参数是我们需要自己实现的另一个“功能”。

假设我们希望hello api在name参数中获得一个名称,例如:

 curl localhost:8000/api/hello?name=Marcin Hello Marcin! 

我们可以将参数解析如下:

 public static Map<String, List<String>> splitQuery(String query) { if (query == null || "".equals(query)) { return Collections.emptyMap(); } return Pattern.compile("&").splitAsStream(query) .map(s -> Arrays.copyOf(s.split("="), 2)) .collect(groupingBy(s -> decode(s[0]), mapping(s -> decode(s[1]), toList()))); } 

并使用如下:

 Map<String, List<String>> params = splitQuery(exchange.getRequestURI().getRawQuery()); String noNameText = "Anonymous"; String name = params.getOrDefault("name", List.of(noNameText)).stream().findFirst().orElse(noNameText); String respText = String.format("Hello %s!", name); 

步骤4分支中的完整示例。

同样,如果我们要在path中使用参数。 例如:

 curl localhost:8000/api/items/1 

要通过id = 1获取元素,我们需要自己解析url。 它变得笨重。

安全性


通常,我们需要保护对某些端点的访问。 例如,可以使用基本身份验证来完成。

对于每个HttpContext,我们可以设置一个身份验证器,如下所示:

 HttpContext context = server.createContext("/api/hello", (exchange -> { //     })); context.setAuthenticator(new BasicAuthenticator("myrealm") { @Override public boolean checkCredentials(String user, String pwd) { return user.equals("admin") && pwd.equals("admin"); } }); 

BasicAuthenticator构造函数中的值“ myrealm”是名称领域。 领域是一个虚拟名称,可用于分隔身份验证域。

您可以在RFC 1945中阅读有关此内容的更多信息。

现在,您可以通过添加Authorization标头来调用此安全端点:

 curl -v localhost:8000/api/hello?name=Marcin -H 'Authorization: Basic YWRtaW46YWRtaW4=' 

“ Basic”之后的文本是Base64编码的admin:admin文本,这是在我们的示例中硬编码的凭据。

为了在真实应用程序中进行身份验证,您可能会从标头中获得凭据,并将其与数据库中存储的用户名和密码进行比较。

如果您未指定标题,则API将以以下状态响应

 HTTP/1.1 401 Unauthorized 

步骤5分支中的完整示例。

JSON,异常处理等


现在该举一个更复杂的例子了。

根据我在软件开发中的经验,我开发的最常见的API是JSON交换。

我们将开发一个用于注册新用户的API。 为了存储它们,我们将使用内存中的数据库。

我们将有一个简单的User域对象:

 @Value @Builder public class User { String id; String login; String password; } 

我使用Lombok摆脱了样板(构造函数,吸气剂)。

在REST API中,我只想传递登录名和密码,因此创建了一个单独的对象:

 @Value @Builder public class NewUser { String login; String password; } 

User对象是在我们将在API处理程序中使用的服务中创建的。 服务方法只是保存用户。

 public String create(NewUser user) { return userRepository.create(user); } 

您可以在实际的应用程序中做更多的事情。 例如,成功注册用户后发送事件。

存储库实现如下:

 import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import com.consulner.domain.user.NewUser; import com.consulner.domain.user.User; import com.consulner.domain.user.UserRepository; public class InMemoryUserRepository implements UserRepository { private static final Map USERS_STORE = new ConcurrentHashMap(); @Override public String create(NewUser newUser) { String id = UUID.randomUUID().toString(); User user = User.builder() .id(id) .login(newUser.getLogin()) .password(newUser.getPassword()) .build(); USERS_STORE.put(newUser.getLogin(), user); return id; } } 

最后,将所有内容粘在handle()

 protected void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equals("POST")) { throw new UnsupportedOperationException(); } RegistrationRequest registerRequest = readRequest(exchange.getRequestBody(), RegistrationRequest.class); NewUser user = NewUser.builder() .login(registerRequest.getLogin()) .password(PasswordEncoder.encode(registerRequest.getPassword())) .build(); String userId = userService.create(user); exchange.getResponseHeaders().set(Constants.CONTENT_TYPE, Constants.APPLICATION_JSON); exchange.sendResponseHeaders(StatusCode.CREATED.getCode(), 0); byte[] response = writeResponse(new RegistrationResponse(userId)); OutputStream responseBody = exchange.getResponseBody(); responseBody.write(response); responseBody.close(); } 

此处,JSON请求将转换为RegistrationRequest对象:

 @Value class RegistrationRequest { String login; String password; } 

我稍后将其映射到NewUser对象,以将其保存在数据库中并将响应作为JSON发送。

我还需要将RegistrationResponse对象转换回JSON字符串。 为此,我们使用杰克逊
com.fasterxml.jackson.databind.ObjectMapper )。

这就是我在main()创建新处理程序的方式:

 public static void main(String[] args) throws IOException { int serverPort = 8000; HttpServer server = HttpServer.create(new InetSocketAddress(serverPort), 0); RegistrationHandler registrationHandler = new RegistrationHandler(getUserService(), getObjectMapper(), getErrorHandler()); server.createContext("/api/users/register", registrationHandler::handle); // here follows the rest.. } 

一个有效的示例可以在步骤6的分支中找到。 我还在那里添加了一个全局异常处理程序,以发送标准的JSON错误消息。 例如,如果不支持HTTP方法或对API的请求格式不正确。

您可以运行该应用程序并尝试以下查询之一:

  • 正确的请求示例

 curl -X POST localhost:8000/api/users/register -d '{"login": "test" , "password" : "test"}' 

答:

 {"id":"395eab24-1fdd-41ae-b47e-302591e6127e"} 

  • 错误示例

 curl -v -X POST localhost:8000/api/users/register -d '{"wrong": "request"}' 

答:

 < HTTP/1.1 400 Bad Request < Date: Sat, 29 Dec 2018 00:11:21 GMT < Transfer-encoding: chunked < Content-type: application/json < * Connection #0 to host localhost left intact {"code":400,"message":"Unrecognized field \"wrong\" (class com.consulner.app.api.user.RegistrationRequest), not marked as ignorable (2 known properties: \"login\", \"password\"])\n at [Source: (sun.net.httpserver.FixedLengthInputStream); line: 1, column: 21] (through reference chain: com.consulner.app.api.user.RegistrationRequest[\"wrong\"])"} 

另外,我不小心遇到了java-express项目,它是Node.js Express框架的Java对应版本。 它还使用jdk.httpserver ,因此您可以在一个真正的框架上学习本文中描述的所有概念,而且该框架很小,足以研究其代码。

Source: https://habr.com/ru/post/zh-CN480564/


All Articles