Java生态系统挤满了框架和库。 可以肯定的是,在JavaScript世界中,它的数量并不多,而且它们也不会很快老化,但这仍然使我敢于认为我们已经忘记了如何创建一个完全没有框架的应用程序。
You may say: Spring is a standard, why to re-invent a wheel. Spark is a nice small REST framework.
Light-rest-4j is yet another.
我告诉你,当然,你是对的。 有了框架,您会遇到很多麻烦,但同时,也会得到很多魔术,学习开销,您很可能不会使用的其他功能以及错误。
您的服务中存在的外部代码越多,其开发人员犯一些错误的机会就越大。
开源社区很活跃,很有可能很快会修复框架中的这些错误,但是我仍然鼓励您重新考虑是否确实需要框架。
如果您正在做小型服务或控制台应用程序,也许可以不用它。
通过坚持使用纯Java代码,您可能会获得(或失去)什么? 想想这些:
- 您的代码可能更清晰,可预测(如果您的编码不好,则可能会一团糟)您将对代码有更多的控制权,不会受到框架的限制(但是您必须经常为框架为您提供的框架编写自己的代码)您的应用程序将部署并启动得更快,因为框架代码不需要初始化很多类(或者如果您弄乱了东西,例如多线程,则根本不会启动)如果您在Docker上部署应用程序,则镜像可能会更苗条,因为jar也将变得更苗条
我做了一个小实验,并尝试开发一种无框架的REST API。
我认为从学习的角度来看这可能会很有趣,并且会让人耳目一新。
当我开始构建它时,我经常遇到一些情况,我错过了Spring提供的一些现成的功能。
那时,我没有重新启动其他Spring功能,而是不得不重新考虑并自己开发它。
碰巧的是,对于实际的业务案例,我可能仍会更喜欢使用Spring而不是重新发明轮子。
不过,我相信这次练习是非常有趣的经历。
Beginning.
I will go through this exercise step by step but not always paste a complete code here.
You can always checkout each step from a separate branch of the git repository.
用初始创建一个新的Maven项目pom.xml文件:
<?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>
Include java.xml.bind module dependency because those modules were removed in JDK 11 by JEP-320.
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.4.0-b180608.0325</version>
</dependency>
and J一种ckson for JSON serialization
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
Then we will use Lombok to simplify POJO classes:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
and v一种vr for functional programming facilities
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.9.2</version>
</dependency>
我从空虚开始应用主班。
You can get an initial code from step-1 branch.
First endpoint
Web应用程序的起点是com.sun.net.httpserver.HttpServer类。 最简单/ 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();
}
}
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并暴露出仅打印的第一个端点你好!,例如 使用curl:
curl localhost:8000/api/hello
Try it out yourself from step-2 branch.
Support different HTTP methods
我们的第一个端点就像一个超级按钮一样工作,但是您会注意到,无论使用哪种HTTP方法,它都会做出相同的响应。 例如。:
curl -X POST localhost:8000/api/hello
curl -X PUT localhost:8000/api/hello
在没有框架的情况下自行构建API的第一个陷阱是,我们需要添加自己的代码来区分方法,例如:
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();
}));
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
还有一些要记住的事情,例如每次从api返回时刷新输出或关闭交换。 当我使用Spring时,我什至不必考虑它。
Try this part from step-3 branch.
Parsing request params
解析请求参数是另一个“功能”,与使用框架相反,我们需要实现自己。 假设我们希望我们的hello api使用作为参数传递的名称进行响应,例如:
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())));
}
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);
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);
You can find complete example in step-4 branch.
同样,如果我们想使用路径参数,例如:
curl localhost:8000/api/items/1
要获得id = 1的项目,我们需要自己解析路径以从中提取ID。 这越来越麻烦。
Secure endpoint
每个REST API的常见情况是使用凭据保护某些端点,例如 使用基本身份验证。 对于每个服务器上下文,我们可以如下设置一个身份验证器:
HttpContext context =server.createContext("/api/hello", (exchange -> {
// this part remains unchanged
}));
context.setAuthenticator(new BasicAuthenticator("myrealm") {
@Override
public boolean checkCredentials(String user, String pwd) {
return user.equals("admin") && pwd.equals("admin");
}
});
HttpContext context =server.createContext("/api/hello", (exchange -> {
// this part remains unchanged
}));
context.setAuthenticator(new BasicAuthenticator("myrealm") {
@Override
public boolean checkCredentials(String user, String pwd) {
return user.equals("admin") && pwd.equals("admin");
}
});
The "myrealm" in BasicAuthenticator
is a realm name. Realm is a virtual name which can be used to separate different authentication spaces.
You can read more about it in RFC 1945
现在,您可以通过添加一个授权书标头是这样的:
curl -v localhost:8000/api/hello?name=Marcin -H 'Authorization: Basic YWRtaW46YWRtaW4='
之后的文字基本的是Base64编码的管理员:管理员这些凭证在我们的示例代码中是硬编码的。 在实际的应用程序中,要对用户进行身份验证,您可能会从标题中获取它,并与数据库中存储的用户名和密码进行比较。 如果您跳过标题,则API将以状态响应
HTTP/1.1 401 Unauthorized
HTTP/1.1 401 Unauthorized
Check out the complete code from step-5 branch.
JSON, exception handlers and others
现在是时候举更复杂的例子了。
根据我过去在软件开发中的经验,我正在开发的最通用的API是交换JSON。
我们将开发一个API来注册新用户。 我们将使用内存数据库来存储它们。
我们的用户域对象将很简单:
@Value
@Builder
public class User {
String id;
String login;
String password;
}
@Value
@Builder
public class User {
String id;
String login;
String password;
}
我正在使用Lombok批注将我从构造函数和getters样板代码中保存下来,它将在构建时生成。
在REST API中,我只想传递登录名和密码,所以我创建了一个单独的域对象:
@Value
@Builder
public class NewUser {
String login;
String password;
}
@Value
@Builder
public class NewUser {
String login;
String password;
}
将在我的API处理程序中使用的服务中创建用户。 服务方法只是存储用户。 在完整的应用程序中,它可以做更多的事情,例如在成功注册用户后发送事件。
public String create(NewUser user) {
return userRepository.create(user);
}
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;
}
}
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;
}
}
最后,让我们在处理程序中将所有内容粘合在一起:
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();
}
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请求转换为注册请求宾语:
@Value
class RegistrationRequest {
String login;
String password;
}
@Value
class RegistrationRequest {
String login;
String password;
}
我稍后将其映射到域对象新的用户最终将其保存在数据库中,并将响应写为JSON。
我需要翻译注册响应对象返回JSON字符串。
编组和解组JSON是通过Jackson对象映射器(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..
}
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..
}
You can find the working example in step-6 git branch, where I also added a global exception handler which is used
by the API to respond with a standard JSON error message in case, e.g. when HTTP method is not supported or API request is malformed.
您可以运行该应用程序,然后尝试以下示例请求之一:
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\"])"}
Also, by chance I encountered a project java-express
which is a Java counterpart of Node.js Express framework
and is using jdk.httpserver as well, so all the concepts covered in this article you can find in real-life application framework :)
which is also small enough to digest the codes quickly.