
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://github.com/fzn7/guice-spark-graphql
-
Больше информации о GraphQL можно найти https://github.com/graphql-java/
ссылка на оригинал статьи https://habr.com/ru/post/556478/
Добавить комментарий