本文的翻译是专门为该课程的学生准备的。 “ 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);
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);
再次尝试查询:
curl -v -X POST localhost:8000/api/hello
答案将是这样的:
> POST /api/hello HTTP/1.1 > Host: localhost:8000 > User-Agent: curl/7.61.0 > Accept: *
还有几点需要牢记。 例如,不要忘记对
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 -> {
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);
一个有效的示例可以在
步骤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
,因此您可以在一个真正的框架上学习本文中描述的所有概念,而且该框架很小,足以研究其代码。