Недавно я писал сервис RESTful с использованием Spark , веб-фреймворка для Java (который не связан с Apache Spark). Когда мы планировали написать это, я был готов к неизбежной лавинской Javaesque- интерфейсам, стандартному коду и глубоким иерархиям. Я был очень удивлен, обнаружив, что альтернативный мир существует и для разработчиков, ограниченных Java.
В этом посте мы увидим, как создать приложение RESTful для блога, используя JSON для передачи данных. Посмотрим:
- как создать простой Hello world в Spark
- как указать расположение объекта JSON, ожидаемого в запросе
- как отправить запрос на публикацию для создания нового сообщения
- как отправить запрос на получение, чтобы получить список сообщений
Мы не будем видеть, как вставить эти данные в БД. Мы просто сохраним список в памяти (в моем реальном сервисе я использовал sql2o ).
Несколько зависимостей
Мы будем использовать Maven, поэтому я начну с создания нового pom.xml, добавив несколько вещей. В принципе:
- искра
- Джексон
- Ломбок
- гуайява
- Easymock (используется только в тестах, не представлен в этом посте)
- Gson
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>com.sparkjava</groupId> <artifactId>spark-core</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.sql2o</groupId> <artifactId>sql2o</artifactId> <version>1.5.4</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.4-1201-jdbc41</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>3.3.1</version> <scope>test</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.3.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <configuration> <mainClass>me.tomassetti.BlogService</mainClass> <arguments> </arguments> </configuration> </plugin> </plugins> </build>
Искра Привет, мир
У вас есть все это? Круто, давайте напишем немного кода.
package me.tomassetti; import static spark.Spark.get; import static spark.Spark.post; import spark.Request; import spark.Response; import spark.Route; public class BlogService { public static void main( String[] args) { get("/posts", (req, res) -> { return "Hello Sparkingly World!"; }); } }
И теперь мы можем запустить его с чем-то вроде:
mvn compile && mvn exec:java
Давайте откроем браузер и зайдем на localhost http: // localhost: 4567 / posts . Здесь мы хотим сделать простое получение. Для выполнения постов вы можете использовать плагин Postman для вашего браузера или просто запустить curl . Все, что работает для вас.
Использование Джексона и Ломбока для потрясающих описательных объектов обмена
В типичном приложении RESTful мы ожидаем получать запросы POST с объектами json как часть полезной нагрузки. Наша работа будет состоять в том, чтобы проверить, правильно ли сформирован код JSON, соответствует ли он ожидаемой структуре, что значения находятся в допустимых диапазонах и т. Д. Вид скучный и повторяющийся. Мы могли бы сделать это по-разному. Самый простой из них — использовать gson :
JsonParser parser = new JsonParser(); JsonElement responseData = parser.parse(response); if (!responseData.isJsonObject()){ // send an error like: "Hey, you did not pass an Object! } JsonObject obj = responseData.getAsJsonObject(); if (!obj.hasField("title")){ // send an error like: "Hey, we were expecting a field name title! } JsonElement titleAsElem = obj.get("title"); if (!titleAsElem.isString()){ // send an error like: "Hey, title is not an string! } // etc, etc, etc
Мы, вероятно, не хотим этого делать.
Более декларативным способом указать, какую структуру мы ожидаем, является создание определенного класса.
class NewPostPayload { private String title; private List<String> categories; private String content; public String getTitle() { ... } public void setTitle(String title) { ... } public List<String> getCategories() { ... } public void setCategories(List<String> categories){ ... } public String getContent() { ... } public void setContent(String content) { ... } }
И тогда мы могли бы использовать Джексона:
try { ObjectMapper mapper = new ObjectMapper(); NewPostPayload newPost = mapper.readValue(request.body(), NewPostPayload.class); } catch (JsonParseException e){ // Hey, you did not send a valid request! }
Таким образом, Джексон автоматически проверяет, имеет ли полезная нагрузка ожидаемую структуру. Мы могли бы проверить, соблюдаются ли дополнительные ограничения. Например, мы можем проверить, не является ли заголовок пустым и указана ли хотя бы одна категория. Мы могли бы создать интерфейс только для проверки:
interface Validable { boolean isValid(); } class NewPostPayload implements Validable { private String title; private List<String> categories; private String content; public String getTitle() { ... } public void setTitle(String title) { ... } public List<String> getCategories() { ... } public void setCategories(List<String> categories){ ... } public String getContent() { ... } public void setContent(String content) { ... } public boolean isValid() { return title != null && !title.isEmpty() && !categories.isEmpty(); } }
Тем не менее у нас есть куча скучных добытчиков и сеттеров. Они не очень информативны и просто загрязняют код. Мы можем избавиться от них, используя Ломбок . Lombok — это процессор аннотаций, который добавляет вам повторяющиеся методы (методы получения, установки, равенства, hashCode и т. Д.). Вы можете думать об этом как о плагине для вашего компилятора, который ищет аннотации (например, @Data ) и генерирует методы на их основе. Если вы добавите его в свои зависимости, maven будет в порядке, но ваша IDE не сможет дать вам автозаполнение для методов, которые добавляет Lombok. Вы можете установить плагин. Для Intellij Idea я использую Lombok Plugin версии 0.9.1, и она прекрасно работает.
Теперь вы можете изменить класс NewPostPayload следующим образом:
@Data class NewPostPayload { private String title; private List<String> categories; private String content; public boolean isValid() { return title != null && !title.isEmpty() && !categories.isEmpty(); } }
Намного лучше, а?
Полный пример
Нам нужно сделать в основном две вещи:
- вставить новый пост
- получить весь список сообщений
Первая операция должна быть реализована как POST (имеет побочные эффекты), а вторая — как GET. Они оба работают с коллекцией сообщений, поэтому мы будем использовать конечную точку / сообщения .
Начнем со вставки поста. Первым делом разберемся
// insert a post (using HTTP post method) post("/posts", (request, response) -> { try { ObjectMapper mapper = new ObjectMapper(); NewPostPayload creation = mapper.readValue(request.body(), NewPostPayload.class); if (!creation.isValid()) { response.status(HTTP_BAD_REQUEST); return ""; } int id = model.createPost(creation.getTitle(), creation.getContent(), creation.getCategories()); response.status(200); response.type("application/json"); return id; } catch (JsonParseException jpe) { response.status(HTTP_BAD_REQUEST); return ""; } });
А затем посмотрим, как получить все сообщения:
// get all post (using HTTP get method) get("/posts", (request, response) -> { response.status(200); response.type("application/json"); return dataToJson(model.getAllPosts()); });
И окончательный код:
package me.tomassetti; import static spark.Spark.get; import static spark.Spark.post; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import lombok.Data; import spark.Request; import spark.Response; import spark.Route; import java.io.IOException; import java.io.StringWriter; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collector; import java.util.stream.Collectors; public class BlogService { private static final int HTTP_BAD_REQUEST = 400; interface Validable { boolean isValid(); } @Data static class NewPostPayload { private String title; private List<String> categories = new LinkedList<>(); private String content; public boolean isValid() { return title != null && !title.isEmpty() && !categories.isEmpty(); } } // In a real application you may want to use a DB, for this example we just store the posts in memory public static class Model { private int nextId = 1; private Map<Integer, Post> posts = new HashMap<>(); @Data class Post { private int id; private String title; private List<String> categories; private String content; } public int createPost(String title, String content, List<String> categories){ int id = nextId++; Post post = new Post(); post.setId(id); post.setTitle(title); post.setContent(content); post.setCategories(categories); posts.put(id, post); return id; } public List<Post> getAllPosts(){ return posts.keySet().stream().sorted().map((id) -> posts.get(id)).collect(Collectors.toList()); } } public static String dataToJson(Object data) { try { ObjectMapper mapper = new ObjectMapper(); mapper.enable(SerializationFeature.INDENT_OUTPUT); StringWriter sw = new StringWriter(); mapper.writeValue(sw, data); return sw.toString(); } catch (IOException e){ throw new RuntimeException("IOException from a StringWriter?"); } } public static void main( String[] args) { Model model = new Model(); // insert a post (using HTTP post method) post("/posts", (request, response) -> { try { ObjectMapper mapper = new ObjectMapper(); NewPostPayload creation = mapper.readValue(request.body(), NewPostPayload.class); if (!creation.isValid()) { response.status(HTTP_BAD_REQUEST); return ""; } int id = model.createPost(creation.getTitle(), creation.getContent(), creation.getCategories()); response.status(200); response.type("application/json"); return id; } catch (JsonParseException jpe) { response.status(HTTP_BAD_REQUEST); return ""; } }); // get all post (using HTTP get method) get("/posts", (request, response) -> { response.status(200); response.type("application/json"); return dataToJson(model.getAllPosts()); }); } }
Использование PostMan, чтобы попробовать приложение
Вы можете вместо этого использовать curl, если предпочитаете командную строку. Мне нравится не выходить из JSON и иметь базовый редактор, поэтому я использую PostMan (плагин Chrome).
Давайте вставим пост. Мы указываем все поля как часть объекта Json, вставленного в тело запроса. Мы возвращаем идентификатор созданного поста.
Тогда мы можем получить список сообщений. В этом случае мы используем GET (без тела в запросе) и получаем данные всех постов (только тот, который мы вставили выше).
Выводы
Я должен сказать, что я был положительно удивлен этим проектом. Я был готов к худшему: это такое приложение, которое требует элементарной логики и большого количества сантехники. Я обнаружил, что Python, Clojure и Ruby отлично справляются с подобными задачами, а когда я писал простые веб-приложения на Java, логика была утоплена в шаблонном коде. Ну, все может быть по-другому. Сочетание Spark, Lombok, Jackson и Java 8 действительно заманчиво. Я очень благодарен авторам этих частей программного обеспечения, они действительно улучшают жизнь разработчиков Java. Я считаю это также уроком: отличные фреймворки часто могут улучшить ситуацию гораздо больше, чем мы думаем.