Создаем GraphQL API с помощью Guice и Spark

от автора

GraphQL это современный язык запросов для получения данных с сервера. Существует большое количество документации по построению API для целого вороха платформ, но к сожалению официальная документация для Java содержит лишь один пример, предполагающий построение приложения на базе Spring Framework. Пример скрывает некоторые детали реализации, заставляя пользователя читать исходники. В статье мы это исправим и создадим аналог на связке Google Guice и Spark. Перед тем как продолжить я рекомендую ознакомиться с оригинальным туторалом, т.к. я не буду углубляться в архитектуру библиотеки и описания сущностей GraphQL Java

1. Создание Guice приложения

За сборку приложения у нас будет отвечать Gradle. Для начала создайте новую папку, в ней терминал и введите gradle init

На предложенные вопросы нужно ответить следующим образом:

Select type of project to generate: application Select implementation language: Java Select build script DSL: Kotlin Select test framework: JUnit Jupiter Project name: guice-spark-graphql Source package: guice.spark.graphql

Для вас будет создан шаблонный проект Java приложения с точкой входа в классе src/main/java/guice/spark/graphql/App.java

Далее нам следует объявить наши зависимости в файле build.gradle.kts

dependencies {     // This dependency is used by the application.     implementation("com.google.guava:guava:29.0-jre")     //Guice     implementation("com.google.inject:guice:5.0.1") //NEW      //Spark     implementation("com.sparkjava:spark-core:2.9.3") //NEW     implementation("com.sparkjava:spark-template-velocity:2.7.1") ////NEW spark template engine     implementation("org.slf4j:slf4j-simple:1.7.21") //NEW fix Spark SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".     implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3") //NEW      //graphql     implementation("com.graphql-java:graphql-java:16.2") //NEW      // Use JUnit Jupiter API for testing.     testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")      // Use JUnit Jupiter Engine for testing.     testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.2") } 

Здесь мы добавляем зависимости от Guice, GraphQL и Spark вместе с Slf4j, Jackson и Velocity Template. Guava и тестовые зависимости необязательны, но с ними жить становится как-то проще

2. Схема данных GraphQL

Для использования GraphQL первым делом нужно объявить схему данных, которая будет описывать API. Создаем файл schema.graphqls в каталоге src/main/resources

type Query {   bookById(id: ID): Book  }  type Book {   id: ID   name: String   pageCount: Int   author: Author }  type Author {   id: ID   firstName: String   lastName: String }

3. Настраиваем Guice

Создадим экземпляр Guice DI контейнера в файле src/main/java/guice/spark/graphql/App.java

public class App {     @Inject     private GraphQLService service;      public static void main(String[] args) {         App app = new App();          Injector injector = Guice.createInjector(new GraphQLModule());         injector.injectMembers(app);          app.service.initialize();     } } 

Guice создаст новый Injector, наполненный объявленными в модуле GraphQLModule компонентами. С помощью метода injector.injectMembers(app)в поле service окажется полностью собранный и готовый к использованию сервис GraphQLService, но перед этим подробнее остановимся том, как объявляются компоненты

4. GraphQLModule

Объявление компонентов в Guice происходит в модулях. Создадим файл src/main/java/guice/spark/graphql/GraphQLModule.java

public class GraphQLModule extends AbstractModule {     protected void configure() {         bind(GraphQLService.class).asEagerSingleton();         bind(GraphQL.class).toProvider(GraphQlProvider.class).asEagerSingleton();         bind(ObjectMapper.class).asEagerSingleton();     } }

Метод configureописывает базовую структуру нашего приложения:

  • GraphQLService будет содержать логику инициализации контроллеров Spark и принимать запросы от внешних клиентов c помощью Spark, направляя их на обработку в инстанс GraphQL

  • GraphQLбудет запрашивать необходимые данные и возвращать ответ

  • ObjectMapperбудет все это дело сериализовать в JSON и возвращать обратно клиенту

5. GraphQlProvider

Для сложных случаев, например когда нам необходимо создавать наш GraphQLинстанс и при этом параметризовать его, мы используем механизм Provider в Guice, который служит фабрикой объектов. Создаем класс GraphQlProvider

public class GraphQlProvider implements Provider<GraphQL> {      private GraphQLDataFetchers graphQLDataFetchers;      @Inject     public GraphQlProvider(GraphQLDataFetchers graphQLDataFetchers) {         this.graphQLDataFetchers = graphQLDataFetchers;     }      @Override     public GraphQL get() {         URL url = Resources.getResource("schema.graphqls");         String sdl = null;         try {             sdl = Resources.toString(url, Charsets.UTF_8);         } catch (IOException e) {             e.printStackTrace();         }         GraphQLSchema graphQLSchema = buildSchema(sdl);         return GraphQL.newGraphQL(graphQLSchema).build();      }      private GraphQLSchema buildSchema(String sdl) {         TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);         RuntimeWiring runtimeWiring = buildWiring();         SchemaGenerator schemaGenerator = new SchemaGenerator();         return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);     }      private RuntimeWiring buildWiring() {         return RuntimeWiring.newRuntimeWiring()                 .type(newTypeWiring("Query")                         .dataFetcher("bookById", graphQLDataFetchers.getBookByIdDataFetcher()))                 .type(newTypeWiring("Book")                         .dataFetcher("author", graphQLDataFetchers.getAuthorDataFetcher()))                 .build();     } } 

Метод get() вернет нам готовый, настроенный GraphQL инстанс. При создании мы должны указать схему которую мы объявили выше, а также назначить обработчики поступающих запросов

6. GraphQLDataFetchers

DataFetcher это одна из самых важных концепций библиотеки GraphQL Java. DataFetcher содержит всю бинес-логику по обработке поступающих запросов.

Создадим файл src/main/java/guice/spark/graphql/GraphQLDataFetchers.java

@Singleton public class GraphQLDataFetchers {     private static List<Map<String, String>> books = Arrays.asList(             ImmutableMap.of("id", "book-1",                     "name", "Harry Potter and the Philosopher's Stone",                     "pageCount", "223",                     "authorId", "author-1"),             ImmutableMap.of("id", "book-2",                     "name", "Moby Dick",                     "pageCount", "635",                     "authorId", "author-2"),             ImmutableMap.of("id", "book-3",                     "name", "Interview with the vampire",                     "pageCount", "371",                     "authorId", "author-3")     );      private static List<Map<String, String>> authors = Arrays.asList(             ImmutableMap.of("id", "author-1",                     "firstName", "Joanne",                     "lastName", "Rowling"),             ImmutableMap.of("id", "author-2",                     "firstName", "Herman",                     "lastName", "Melville"),             ImmutableMap.of("id", "author-3",                     "firstName", "Anne",                     "lastName", "Rice")     );      public DataFetcher getBookByIdDataFetcher() {         return dataFetchingEnvironment -> {             String bookId = dataFetchingEnvironment.getArgument("id");             return books                     .stream()                     .filter(book -> book.get("id").equals(bookId))                     .findFirst()                     .orElse(null);         };     }      public DataFetcher getAuthorDataFetcher() {         return dataFetchingEnvironment -> {             Map<String, String> book = dataFetchingEnvironment.getSource();             String authorId = book.get("authorId");             return authors                     .stream()                     .filter(author -> author.get("id").equals(authorId))                     .findFirst()                     .orElse(null);         };     } }

Аннотация @Singletonсообщит Guice что если кто-то попытается получить инстанс этого класса он будет автоматически создан и предоставлен. В таких тривиальных случаях объявление в блоке configureмодуля GraphQLModuleне требуется, однако можно его там указать при желании — это повысит читабельность кода

7. GraphQLService

Пора предоставить возможность пользователям обращаться к нашему API по средствам web запросов. Как было сказано выше за это будет отвечать класс GraphQLService

Создадим его src/main/java/guice/spark/graphql/GraphQLService.java

public class GraphQLService {     private final GraphQL graphQL;     private final ObjectMapper mapper;      @Inject     public GraphQLService(GraphQL graphQL, ObjectMapper mapper) {         this.graphQL = graphQL;         this.mapper = mapper;     }      public void initialize() {         post("/graphql", (request, response) -> {             GraphQLRequestBody body = mapper.readValue(request.body(), GraphQLRequestBody.class);              String query = body.getQuery();             if (query == null) {                 query = "";             }              ExecutionResult executionResult = graphQL.execute(                     ExecutionInput.newExecutionInput()                             .query(query)                             .operationName(body.getOperationName())                             .variables(body.getVariables())                             .build()             );              response.type("application/json");             return mapper.writeValueAsString(executionResult.toSpecification());         });                  get("/playground", (req, res) -> new VelocityTemplateEngine().render(                 new ModelAndView(Collections.emptyMap(), "playground.html"))         );     } 

При первом обращении к методу initializeбудет запущен Jetty web server на дефолтном порту 4567. Мы предоставляем пользователю два ендпойнта:

При POST запросе на адрес http://localhost:4567/graphql ObjectMapper десериализует его в объект класса GraphQLRequestBody после чего отправит GraphQL инстансу на выполнение. Ответ виде JSON вернется обратно пользователю.

На всякий случай код класса GraphQLRequestBody:

public class GraphQLRequestBody {     private String query;     private String operationName;     private Map<String, Object> variables;      public String getQuery() {         return query;     }      public void setQuery(String query) {         this.query = query;     }      public String getOperationName() {         return operationName;     }      public void setOperationName(String operationName) {         this.operationName = operationName;     }      public Map<String, Object> getVariables() {         return variables;     }      public void setVariables(Map<String, Object> variables) {         this.variables = variables;     } }

При GET запросе на адрес http://localhost:4567/playground будет отрендерен playground.html файл, который также необходимо положить в папку src/main/resources. Это наш тул для проверки API прямо в браузере. Скачать его можно здесь

Теперь все готово и мы можем запустить приложение. Открываем браузере адрес http://localhost:4567/playground и видим нашу тестовую песочницу

Тестируем наш API

Все готово. Теперь на запрос:

query {   bookById(id: "book-1") {     name,     author {       firstName     }   } } 

вы должны получить ответ от API:

{   "data": {     "bookById": {       "name": "Harry Potter and the Philosopher's Stone",       "author": {         "firstName": "Joanne"       }     }   } }

Полный исходный код и дополнительная информация

ссылка на оригинал статьи https://habr.com/ru/post/556478/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *