Недавно я писал сервис 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. Я считаю это также уроком: отличные фреймворки часто могут улучшить ситуацию гораздо больше, чем мы думаем.

