Эксперимент: Todo API with SparkJava Framework
Spark - микро фреймворк для создания веб приложений на Kotlin и Java 8 с минимальными усилием - как говорится на официальном сайте
!Не путать с Apache Spark-ом
Цель: показать старый пример CRUD приложения Rest Todo API через новый стек - в данном случае - Spark Java
Вот пример, простого веб приложения с endpoint-ом GET /hello
import static spark.Spark.*;
public class HelloWorld {
public static void main(String[] args) {
get("/hello", (req, res) -> "Hello Spark Framework");
}
}
Запущенное приложение будет доступно по порту:
`http://localhost:4567/hello`
Итак, как и раньше нам нужно 4-5 endpoint-ов на создание, получение, удаление и редактирование /todos ресурсов
- GET - /api/todo - получаем список объектов
- POST - /api/todo - сохраняем объект
- PUT - /api/todo - обновляем объект
- DELETE - /api/todo/{id} - здесь мы удаляем объект по его id - который отмечен как переменная ресурса
Создаем новый проект в Intelij Idea как Maven, в `pom.xml` добавлем следующую зависимость:
<dependencies>
<!-- Главная зависимость для работы со Spark-ом -->
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.8.0</version>
</dependency>
<!-- конвертируем объекты в json и обратно-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
</dependencies>
Создаем класс TodoApplication - в нем у нас будет вся логика нашего Rest API от endpoint-ов до модели и виртуального хранилища в виде ConcurrentMap-ы
package com.timurisachenko.todo;
import static spark.Spark.*;
public class TodoApplication {
public static void main(String[] args) {
get("/todo", (req, res) -> "{\"id\": 1, \"text\": \"First task\", \"completed\":false}");
}
}
отлично, можно запустить и проверить работу сервера по адресу ресурса /todo
Дальше лучше, видите какой вернулся json - наша модель будет такой же, в плане набора полей, создадим класс Todo:
private static class Todo {
private Long id;
private String text;
private boolean completed;
public Todo(Long id) {
this.id = id;
}
public Todo(Long id, String text, boolean completed) {
this.id = id;
this.text = text;
this.completed = completed;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Todo todo = (Todo) o;
return completed == todo.completed &&
Objects.equals(id, todo.id) &&
Objects.equals(text, todo.text);
}
@Override
public int hashCode() {
return Objects.hash(id, text, completed);
}
}
Теперь мы можем делать Json преобразования как в нашу модель, так и обратно. Но чтобы сделать это более системно в фреймворке есть ResponseTransformator
get("/todo", (req, res) -> new Todo(1l, "First Task", false),
new ResponseTransformer() {
private Gson gson = new Gson();
@Override
public String render(Object o) throws Exception {
return gson.toJson(o);
}
});
post("/todo", (request, response) -> { // Create something
});
put("/todo", (request, response) -> { // Update something
});
delete("/todo/:id", (request, response) -> {
// Annihilate something
});
Сделаем хранилище repository
😏 из ConcurrentMap-ы
и заполним тестовыми строками:
Gson gson = new Gson();
repo = Stream.of(
new Todo(1l, "Привет это мой таск лист", false),
new Todo(2l, "Мне нужно постоянно просматривать и сзаписывать задания ", false),
new Todo(3l, "А это задание я уже выполнил", true))
.collect(toConcurrentMap(f -> f.getId(), Function.identity()));
idIncrement = new AtomicLong(repo.keySet().stream().max(Comparator.naturalOrder()).orElseGet(() -> 0l));
и обрамим все наши endpoint-ы в path("/api", () - { });
, таким образом получается что все наши endpoint-ы будут согласно первоначальному плану
👀👀👀 Итоговый класс:
package com.timuisachenko.todo;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.ResponseTransformer;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toConcurrentMap;
import static java.util.stream.Collectors.toList;
import static spark.Spark.*;
public class TodoApplication {
private static ConcurrentMap<Long, Todo> repo;
private static AtomicLong idIncrement;
private static Logger LOGGER = LoggerFactory.getLogger(TodoApplication.class.getSimpleName());
public static void main(String[] args) {
Gson gson = new Gson();
repo = Stream.of(
new Todo(1l, "Привет это мой таск лист", false),
new Todo(2l, "Мне нужно постоянно просматривать и сзаписывать задания ", false),
new Todo(3l, "А это задание я уже выполнил", true))
.collect(toConcurrentMap(f -> f.getId(), Function.identity()));
idIncrement = new AtomicLong(repo.keySet().stream().max(Comparator.naturalOrder()).orElseGet(() -> 0l));
path("/api", () -> {
get("/todo", "application/json", (req, res) -> repo.keySet().stream().map(k -> repo.get(k)).collect(toList()), new ResponseTransformer() {
@Override
public String render(Object o) throws Exception {
return gson.toJson(o);
}
});
post("/todo", (req, res) -> {
Todo todo = gson.fromJson(req.body(), Todo.class);
repo.putIfAbsent(idIncrement.incrementAndGet(), new Todo(idIncrement.longValue(), todo.getText(), false));
return repo.get(idIncrement);
});
put("/todo",
(req, res) -> {
Todo todo = gson.fromJson(req.body(), Todo.class);
repo.put(todo.id, new Todo(todo.id, todo.getText(), todo.isCompleted()));
LOGGER.debug(todo.toString());
return repo.get(idIncrement);
}
);
delete("/todo/:id", (req, res) -> repo.remove(Long.parseLong(req.params(":id"))));
});
}
private static class Todo {
private Long id;
private String text;
private boolean completed;
public Todo(Long id) {
this.id = id;
}
public Todo(Long id, String text, boolean completed) {
this.id = id;
this.text = text;
this.completed = completed;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Todo todo = (Todo) o;
return completed == todo.completed &&
Objects.equals(id, todo.id) &&
Objects.equals(text, todo.text);
}
@Override
public int hashCode() {
return Objects.hash(id, text, completed);
}
}
}
повторю пример того, как можно протестировать из консоли браузера наш созданный Rest API:
//GET
await (await fetch("/api/todo", {
method: "get",
headers: {
'Accept': 'application/json'
}
})).json()
//POST
await (await fetch("/api/todo", {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
//make sure to serialize your JSON body
body: JSON.stringify({
text: "Todo1",
completed: false
})
})).json()
//PUT
await (await fetch("/api/todo", {
method: "put",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
//make sure to serialize your JSON body
body: JSON.stringify({
id: 1,
text: "Blablahblah",
completed: true
})
})).json()
//DELETE
await (await fetch("/api/todo/1", {
method: "delete",
headers: {
'Accept': 'application/json'
}
})).json()
Итак, что мы видим это, то что если вы работали(ете) с Node.Js и знакомы с языком Java + хотите воспользоваться множеством библиотечных решений платформы , то Spark Framework отличный выбор.
Будьте здоровы!
Премного благодарен за поддержку, шаринг и комменты 😁