Статьи

Облегченный встроенный сервер Java REST без каркаса

Быстрый поиск Google по Java REST-фреймворкам приведет вас к множеству существующих фреймворков. Например, вы можете легко найти тонны примеров Spring Boot REST, которые содержат очень мало строк кода. Однако у них есть много аннотаций и много магии. Кроме того, большинство из них не делают очень много; они, как правило, не учитывают обработку исключений, ведение журнала и метрики. Вот пример облегченного REST-сервера с большим количеством соответствующего промежуточного программного обеспечения, без магии и не так много строк пользовательского кода. Этот пример не включает проверку или базы данных, мы сохраним это на более поздний срок.

Модель / POJO

Наш сервис будет очень простым и будет обрабатывать только операции CRUD для класса User.

public class User {
    private final String email;
    private final Set<Role> roles;
    private final LocalDate dateCreated;

    public User(
            @JsonProperty("email") String email,
            @JsonProperty("roles") Set<Role> roles,
            @JsonProperty("dateCreated") LocalDate dateCreated) {
        super();
        this.email = email;
        this.roles = roles;
        this.dateCreated = dateCreated;
    }

    public String getEmail() {
        return email;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public LocalDate getDateCreated() {
        return dateCreated;
    }

    public static enum Role {
        USER, ADMIN
    }

    private static final TypeReference<User> typeRef = new TypeReference<User>() {};
    public static TypeReference<User> typeRef() {
        return typeRef;
    }
    private static final TypeReference<List<User>> listTypeRef = new TypeReference<List<User>>() {};
    public static TypeReference<List<User>> listTypeRef() {
        return listTypeRef;
    }
}

 Посмотреть на GitHub .

В память Дао

Простой в памяти дао просто для примера.

/*
 * In memory Dao. Less than ideal but just for an example.
 */
public class UserDao {
    private final ConcurrentMap<String, User> userMap;

    public UserDao() {
        this.userMap = new ConcurrentHashMap<>();
    }

    public User create(String email, Set<User.Role> roles) {
        User user = new User(email, roles, LocalDate.now());

        // If we get a non null value that means the user already exists in the Map.
        if (null != userMap.putIfAbsent(user.getEmail(), user)) {
            return null;
        }
        return user;
    }

    public User get(String email) {
        return userMap.get(email);
    }

    // Alternate implementation to throw exceptions instead of return nulls for not found.
    public User getThrowNotFound(String email) {
        User user = userMap.get(email);
        if (null == user) {
            throw Exceptions.notFound(String.format("User %s not found", email));
        }
        return user;
    }

    public User update(User user) {
        // This means no user existed so update failed. return null
        if (null == userMap.replace(user.getEmail(), user)) {
            return null;
        }
        // Update succeeded return the user
        return user;
    }

    public boolean delete(String email) {
        return null != userMap.remove(email);
    }

    public List<User> listUsers() {
        return userMap.values()
                      .stream()
                      .sorted(Comparator.comparing((User u) -> u.getEmail()))
                      .collect(Collectors.toList());
    }
}

 Посмотреть на GitHub

Заказать коммунальные услуги

Простые помощники, чтобы уменьшить шаблон и повторно использовать значения по умолчанию / строковые литералы на разных маршрутах. Весной нередко можно увидеть что-то вроде @PathParam («userId»), разбросанное по всему коду, или, что еще хуже, некоторые говорят, что userId некоторые говорят id, и это может быть противоречивым. Конечно, в Spring можно написать свои собственные аннотации для повторного использования, но кто хочет делать это для каждого параметра? (Существуют и другие, более подходящие подходы, но, похоже, их не очень часто используют).

public class UserRequests {

    public String email(HttpServerExchange exchange) {
        return Exchange.pathParams().pathParam(exchange, "email").orElse(null);
    }

    public User user(HttpServerExchange exchange) {
        return Exchange.body().parseJson(exchange, User.typeRef());
    }

    public void exception(HttpServerExchange exchange) {
        boolean exception = Exchange.queryParams()
                                    .queryParamAsBoolean(exchange, "exception")
                                    .orElse(false);
        if (exception) {
            throw new RuntimeException("Some random exception. Could be anything!");
        }
    }
}

 Посмотреть на GitHub .

Обработчики

public class UserRoutes {
    private static final UserRequests userRequests = new UserRequests();
    private static final UserDao userDao = new UserDao();

    public static void createUser(HttpServerExchange exchange) {
        User userInput = userRequests.user(exchange);
        User user = userDao.create(userInput.getEmail(), userInput.getRoles());
        if (null == user) {
            ApiHandlers.badRequest(exchange, String.format("User %s already exists.", userInput.getEmail()));
            return;
        }
        exchange.setStatusCode(StatusCodes.CREATED);
        Exchange.body().sendJson(exchange, user);
    }

    public static void getUser(HttpServerExchange exchange) {
        String email = userRequests.email(exchange);
        User user = userDao.get(email);
        if (null == user) {
            ApiHandlers.notFound(exchange, String.format("User %s not found.", email));
            return;
        }
        Exchange.body().sendJson(exchange, user);
    }

    // Alternative Not Found by throwing / handling Exceptions.
    public static void getUserThrowNotFound(HttpServerExchange exchange) {
        String email = userRequests.email(exchange);
        User user = userDao.getThrowNotFound(email);
        Exchange.body().sendJson(exchange, user);
    }

    public static void updateUser(HttpServerExchange exchange) {
        User userInput = userRequests.user(exchange);
        User user = userDao.update(userInput);
        if (null == user) {
            ApiHandlers.notFound(exchange, String.format("User {} not found.", userInput.getEmail()));
            return;
        }
        Exchange.body().sendJson(exchange, user);
    }

    public static void deleteUser(HttpServerExchange exchange) {
        String email = userRequests.email(exchange);

        // If you care about it you can handle it.
        if (false == userDao.delete(email)) {
            ApiHandlers.notFound(exchange, String.format("User {} not found.", email));
            return;
        }
        exchange.setStatusCode(StatusCodes.NO_CONTENT);
        exchange.endExchange();
    }

    public static void listUsers(HttpServerExchange exchange) {
        List<User> users = userDao.listUsers();
        Exchange.body().sendJson(exchange, users);
    }
}

 Посмотреть на GitHub .

Обратите внимание, что есть два варианта обработки NotFound на сервере. Вы можете создавать / обрабатывать исключения или обрабатывать их непосредственно в маршруте, поскольку мы уже знаем, что они не найдены. Spring и большинство фреймворков стремятся использовать модель исключений throw / handle. Это легко сделать, просто помните, что создание и отлов исключений может быть дорогостоящим, в этих случаях этого легко избежать, поэтому выберите то, что имеет смысл для вашего бизнеса.

Маршрутизация / Server

Обратите внимание, что у нас есть все остальные маршруты, а также  промежуточное ПО для обработки исключений / регистрации / метрик .

private static final HttpHandler ROUTES = new RoutingHandler()
    .get("/users", timed("listUsers", UserRoutes::listUsers))
    .get("/users/{email}", timed("getUser", UserRoutes::getUser))
    .get("/users/{email}/exception", timed("getUser", UserRoutes::getUserThrowNotFound))
    .post("/users", timed("createUser", UserRoutes::createUser))
    .put("/users", timed("updateUser", UserRoutes::updateUser))
    .delete("/users/{email}", timed("deleteUser", UserRoutes::deleteUser))
    .get("/metrics", timed("metrics", CustomHandlers::metrics))
    .get("/health", timed("health", CustomHandlers::health))
    .setFallbackHandler(timed("notFound", RoutingHandlers::notFoundHandler))
;

/*
 *  Small wrapper to mimic throwing exceptions. Just add &exception=true
 *  to any route and this will throw an exception. Notice it throws a RuntimeException
 *  not an API exception. This will be handled by the global ExceptionHandler.
 */
private static final HttpHandler EXCEPTION_THROWER = (HttpServerExchange exchange) -> {
    new UserRequests().exception(exchange);
    ROUTES.handleRequest(exchange);
};

private static final HttpHandler ROOT = CustomHandlers.exception(EXCEPTION_THROWER)
    .addExceptionHandler(ApiException.class, ApiHandlers::handleApiException)
    .addExceptionHandler(Throwable.class, ApiHandlers::serverError)
;

 Посмотреть на GitHub .

public static void main(String[] args) {
    // Once again pull in a bunch of common middleware.
    SimpleServer server = SimpleServer.simpleServer(Middleware.common(ROOT));
    server.start();
}

 Посмотреть на GitHub .

Примеры

Создать пользователя

curl -X POST "localhost:8080/users" -d '
{
  "email": "user1@test.com",
  "roles": ["USER"]
}
';
{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"}

curl -X POST "localhost:8080/users" -d '
{
  "email": "user2@test.com",
  "roles": ["ADMIN"]
}
';
{"email":"user2@test.com","roles":["ADMIN"],"dateCreated":"2017-01-16"}

Обновить пользователя

curl -X PUT "localhost:8080/users" -d '
{
  "email": "user2@test.com",
  "roles": ["USER", "ADMIN"]
}
';
{"email":"user2@test.com","roles":["ADMIN","USER"]}

Список пользователей

curl -X GET "localhost:8080/users"
[{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"},{"email":"user2@test.com","roles":["ADMIN","USER"]}]

Получить пользователя

Мы используем оба стиля получения пользователя здесь.

curl -X GET "localhost:8080/users/user1@test.com"
{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"}

curl -X GET "localhost:8080/users/user1@test.com/exception"
{"email":"user1@test.com","roles":["USER"],"dateCreated":"2017-01-16"}

Удалить пользователя

curl -v -X DELETE "localhost:8080/users/user1@test.com"
* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE /users/user1@test.com HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 204 No Content
< Date: Mon, 16 Jan 2017 18:45:52 GMT

Получить пользователя 404

Оба подхода отвечают одинаково, однако версия обработки исключений будет более дорогой. Он должен развернуть трассировку стека и еще много чего. Если это приемлемо во что бы то ни стало, пойти на это. Если вам нужна максимальная производительность, не создавайте исключения, когда вам это не нужно.

curl -X GET "localhost:8080/users/user1@test.com"
{"statusCode":404,"message":"User user1@test.com not found."}

curl -X GET "localhost:8080/users/user1@test.com/exception"
{"statusCode":404,"message":"User user1@test.com not found"}

Список пользователей снова

curl -X GET "localhost:8080/users"
[{"email":"user2@test.com","roles":["ADMIN","USER"]}]

Обработка неизвестного исключения

Добавление исключения = true к любому маршруту вызовет исключение RuntimeException. Обратите внимание, что это исключение обрабатывается с помощью резервного обработчика, который обрабатывает любой класс Throwable.class.

curl -X GET "localhost:8080/users?exception=true"
{"statusCode":500,"message":"Internal Server Error"}

метрика

Еще раз у нас есть все наши метрики из нашей логики промежуточного программного обеспечения.

{
  "version": "3.0.0",
  "gauges": {},
  "counters": {},
  "histograms": {},
  "meters": {
    "status.code.200": {
      "count": 6,
      "m15_rate": 17.219082651255942,
      "m1_rate": 0.5436625964502019,
      "m5_rate": 8.9579872747367,
      "mean_rate": 1.1273159803060255,
      "units": "events/minute"
    },
    "status.code.201": {
      "count": 1,
      "m15_rate": 9.397673938878663,
      "m1_rate": 0.3067383984780894,
      "m5_rate": 5.7636636130775925,
      "mean_rate": 0.2625854307076081,
      "units": "events/minute"
    },
    "status.code.204": {
      "count": 1,
      "m15_rate": 10.101505049683713,
      "m1_rate": 0.9062621341052866,
      "m5_rate": 7.158067076339619,
      "mean_rate": 0.3709252334705832,
      "units": "events/minute"
    },
    "status.code.400": {
      "count": 1,
      "m15_rate": 10.858049016431512,
      "m1_rate": 2.6775619217811597,
      "m5_rate": 8.889818648180613,
      "mean_rate": 0.6165310747609489,
      "units": "events/minute"
    },
    "status.code.404": {
      "count": 2,
      "m15_rate": 10.68590211257018,
      "m1_rate": 2.878023977404449,
      "m5_rate": 8.514829995177887,
      "mean_rate": 1.033923244541407,
      "units": "events/minute"
    },
    "status.code.500": {
      "count": 1,
      "m15_rate": 11.542290915278693,
      "m1_rate": 6.696421749240567,
      "m5_rate": 10.678581251856285,
      "mean_rate": 1.3928552880390135,
      "units": "events/minute"
    }
  },
  "timers": {
    "createUser": {
      "count": 2,
      "max": 468.563385,
      "mean": 230.19748354543808,
      "min": 2.3202819999999997,
      "p50": 2.3202819999999997,
      "p75": 468.563385,
      "p95": 468.563385,
      "p98": 468.563385,
      "p99": 468.563385,
      "p999": 468.563385,
      "stddev": 233.06255505190282,
      "m15_rate": 0.0939588952884057,
      "m1_rate": 0.010507188050111582,
      "m5_rate": 0.13998158006315464,
      "mean_rate": 0.36653143968815943,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "deleteUser": {
      "count": 1,
      "max": 4.8252109999999995,
      "mean": 4.8252109999999995,
      "min": 4.8252109999999995,
      "p50": 4.8252109999999995,
      "p75": 4.8252109999999995,
      "p95": 4.8252109999999995,
      "p98": 4.8252109999999995,
      "p99": 4.8252109999999995,
      "p999": 4.8252109999999995,
      "stddev": 0,
      "m15_rate": 0.055963873354550935,
      "m1_rate": 0.0724607194316669,
      "m5_rate": 0.11831244221923884,
      "mean_rate": 0.18326783459947024,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "getUser": {
      "count": 4,
      "max": 10.177731,
      "mean": 4.490080459374723,
      "min": 1.042088,
      "p50": 1.575589,
      "p75": 10.177731,
      "p95": 10.177731,
      "p98": 10.177731,
      "p99": 10.177731,
      "p999": 10.177731,
      "stddev": 4.276904527854368,
      "m15_rate": 0.22399902400953256,
      "m1_rate": 0.42427789694811446,
      "m5_rate": 0.47991807604468867,
      "mean_rate": 0.7330560454806502,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "listUsers": {
      "count": 2,
      "max": 18.343650999999998,
      "mean": 1.9680702759999467,
      "min": 1.244971,
      "p50": 1.244971,
      "p75": 1.244971,
      "p95": 1.244971,
      "p98": 18.343650999999998,
      "p99": 18.343650999999998,
      "p999": 18.343650999999998,
      "stddev": 3.441100196972346,
      "m15_rate": 0.1111005918141424,
      "m1_rate": 0.3354051301588863,
      "m5_rate": 0.2403451569244876,
      "mean_rate": 0.3665140477638753,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "metrics": {
      "count": 0,
      "max": 0,
      "mean": 0,
      "min": 0,
      "p50": 0,
      "p75": 0,
      "p95": 0,
      "p98": 0,
      "p99": 0,
      "p999": 0,
      "stddev": 0,
      "m15_rate": 0,
      "m1_rate": 0,
      "m5_rate": 0,
      "mean_rate": 0,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "notFound": {
      "count": 1,
      "max": 6.1470899999999995,
      "mean": 6.1470899999999995,
      "min": 6.1470899999999995,
      "p50": 6.1470899999999995,
      "p75": 6.1470899999999995,
      "p95": 6.1470899999999995,
      "p98": 6.1470899999999995,
      "p99": 6.1470899999999995,
      "p999": 6.1470899999999995,
      "stddev": 0,
      "m15_rate": 0.06648182394123926,
      "m1_rate": 0.9594670244481206,
      "m5_rate": 0.1983425541405901,
      "mean_rate": 0.18326906627298534,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "updateUser": {
      "count": 1,
      "max": 2.389231,
      "mean": 2.389231,
      "min": 2.389231,
      "p50": 2.389231,
      "p75": 2.389231,
      "p95": 2.389231,
      "p98": 2.389231,
      "p99": 2.389231,
      "p999": 2.389231,
      "stddev": 0,
      "m15_rate": 0.04710994577423798,
      "m1_rate": 0.005472367185912239,
      "m5_rate": 0.07057403311423895,
      "mean_rate": 0.18326677583386106,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    }
  }
}

Создание исполняемого FAR JAR

Посмотрите наш пост о  мультипроектных сборках с Gradle и Fat Jars with Shadow .

Java-клиент с OkHttp

 cURL  is great for debugging and testing, however, you will probably want programmatic access to your API with a Java HTTP Client (OkHttp).