Статьи

Начало работы с Spark

Недавно я писал сервис 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();
   }
}

Намного лучше, а?

Полный пример

Нам нужно сделать в основном две вещи:

  1. вставить новый пост
  2. получить весь список сообщений

Первая операция должна быть реализована как 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, вставленного в тело запроса. Мы возвращаем идентификатор созданного поста.

Снимок экрана 2015-03-30 в 17.25.22

Тогда мы можем получить список сообщений. В этом случае мы используем GET (без тела в запросе) и получаем данные всех постов (только тот, который мы вставили выше).

Снимок экрана 2015-03-30 в 17.30.33

Выводы

Я должен сказать, что я был положительно удивлен этим проектом. Я был готов к худшему: это такое приложение, которое требует элементарной логики и большого количества сантехники. Я обнаружил, что Python, Clojure и Ruby отлично справляются с подобными задачами, а когда я писал простые веб-приложения на Java, логика была утоплена в шаблонном коде. Ну, все может быть по-другому. Сочетание Spark, Lombok, Jackson и Java 8 действительно заманчиво. Я очень благодарен авторам этих частей программного обеспечения, они действительно улучшают жизнь разработчиков Java. Я считаю это также уроком: отличные фреймворки часто могут улучшить ситуацию гораздо больше, чем мы думаем.