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, направляя их на обработку в инстанс GraphQLGraphQL
будет запрашивать необходимые данные и возвращать ответ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"
}
}
}
}
DmitryOlkhovoi
Все это конечно имеет место быть. Но после Strapi, который буквально из коробки дает мне Graphql + REST с хедлес CMS, не могу на такое смотреть :)