Быстрый поиск 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;
}
}
В память Дао
Простой в памяти дао просто для примера.
/*
* 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());
}
}
Заказать коммунальные услуги
Простые помощники, чтобы уменьшить шаблон и повторно использовать значения по умолчанию / строковые литералы на разных маршрутах. Весной нередко можно увидеть что-то вроде @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!");
}
}
}
Обработчики
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);
}
}
Обратите внимание, что есть два варианта обработки 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)
;
public static void main(String[] args) {
// Once again pull in a bunch of common middleware.
SimpleServer server = SimpleServer.simpleServer(Middleware.common(ROOT));
server.start();
}
Примеры
Создать пользователя
curl -X POST "localhost:8080/users" -d '
{
"email": "[email protected]",
"roles": ["USER"]
}
';
{"email":"[email protected]","roles":["USER"],"dateCreated":"2017-01-16"}
curl -X POST "localhost:8080/users" -d '
{
"email": "[email protected]",
"roles": ["ADMIN"]
}
';
{"email":"[email protected]","roles":["ADMIN"],"dateCreated":"2017-01-16"}
Обновить пользователя
curl -X PUT "localhost:8080/users" -d '
{
"email": "[email protected]",
"roles": ["USER", "ADMIN"]
}
';
{"email":"[email protected]","roles":["ADMIN","USER"]}
Список пользователей
curl -X GET "localhost:8080/users"
[{"email":"[email protected]","roles":["USER"],"dateCreated":"2017-01-16"},{"email":"[email protected]","roles":["ADMIN","USER"]}]
Получить пользователя
Мы используем оба стиля получения пользователя здесь.
curl -X GET "localhost:8080/users/[email protected]"
{"email":"[email protected]","roles":["USER"],"dateCreated":"2017-01-16"}
curl -X GET "localhost:8080/users/[email protected]/exception"
{"email":"[email protected]","roles":["USER"],"dateCreated":"2017-01-16"}
Удалить пользователя
curl -v -X DELETE "localhost:8080/users/[email protected]"
* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE /users/[email protected] 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/[email protected]"
{"statusCode":404,"message":"User [email protected] not found."}
curl -X GET "localhost:8080/users/[email protected]/exception"
{"statusCode":404,"message":"User [email protected] not found"}
Список пользователей снова
curl -X GET "localhost:8080/users"
[{"email":"[email protected]","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).