Java中的无框架REST API

Photo by Hieu Vu Minh on Unsplash

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();
    }
}

当您运行主程序时,它将在端口启动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();
        }));

现在再尝试请求:

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())));

    }

并如下使用:

 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");
    }
});

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

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;
}

我正在使用Lombok批注将我从构造函数和getters样板代码中保存下来,它将在构建时生成。

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

@Value
@Builder
public class NewUser {

    String login;
    String password;
}

将在我的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;
    }
}

最后,让我们在处理程序中将所有内容粘合在一起:

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;
}

我稍后将其映射到域对象新的用户最终将其保存在数据库中,并将响应写为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.. 

 }

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.

from: https://dev.to//piczmar_0/framework-less-rest-api-in-java-1jbl

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
(1)项目简介 这个demo很简单,是一个记账小工程。用户可以注册、修改密码,可以记账、查找记账记录等。 (2)接口介绍 用户操作相关: post /users 用户注册 post /users/login 用户登录(这里我把login当成一个名词) put /users/pwd?userId=xxx&sign=xxx 用户修改密码 delete /users?uerId=xxx&sign=xxx 删除用户 记账记录操作相关: post /records?userId=xxx&sign=xxx 增加一条记账记录 get /records/:id?userId=xxx&sign=xxx 查询一条记账记录详情 put /records/:id?userId=xxx&sign=xxx 修改一条记账记录详情 get /records?查询参数&userId=xxx&sign=xxx 分页查询记账记录 delete /records/:id?userId=xxx&sign=xxx 删除一条记账记录 其url带sign参数的表示该接口需要鉴权,sign必须是url最后一个参数。具体的鉴权方法是:用户登录后,服务器生成返回一个token,然后客户端要注意保存这个token,需要鉴权的接口加上sign签名,sign=MD5(url+token),这样可以避免直接传token从而泄露了token。这里我觉得接口最好还带一个时间戳参数timestamp,然后可以在服务端比较时间差,从而避免重放攻击。而且这样还有一个好处,就是如果有人截获了我们的请求,他想伪造我们的请求则不得不改时间戳参数(因为我们在服务器端会比较时间),这样一来sign势必会改变,他是无法得知这个sign的。如果我们没有加时间戳参数的话,那么,他截获了请求url,再重发这个请求势必又是一次合法的请求。我在这里为了简单一些,就不加时间戳了,因为这在开发测试阶段实在是太麻烦了。 (3)关于redis和数据库的说明 服务端在用户登录后,生成token,并将token保存到redis。后面在接口鉴权的时候会取出token计算签名MD5(除sign外的url+token),进行比对。 这个demo搭建了一个redis主从复制,具体可以参考:http://download.csdn.net/detail/zhutulang/9585010 数据库使用mysql,脚本在 src/main/resources/accounting.sql

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值