GraphQL adalah bahasa permintaan API yang dikembangkan oleh Facebook. Artikel ini akan membahas contoh implementasi API GraphQL pada JVM, khususnya, menggunakan bahasa Kotlin dan kerangka kerja Micronaut ; sebagian besar contoh dapat digunakan kembali pada kerangka Java / Kotlin lainnya. Kemudian akan ditunjukkan cara menggabungkan beberapa layanan GraphQL ke dalam grafik data tunggal untuk menyediakan antarmuka umum untuk akses ke semua sumber data. Ini diimplementasikan menggunakan Apollo Server dan Apollo Federation . Akibatnya, arsitektur berikut akan diperoleh:

Setiap komponen arsitektur menyoroti beberapa masalah yang mungkin timbul selama pengembangan API GraphQL. Model domain mencakup data di planet-planet tata surya dan satelitnya.
:
Planet service
, GraphQL, :
implementation("io.micronaut.graphql:micronaut-graphql:$micronautGraphQLVersion")
implementation("io.gqljf:graphql-java-federation:$graphqlJavaFederationVersion")
( )
GraphQL Java Micronaut, , , , GraphQL . Spring and Micronaut; GET POST /graphql
. — , GraphQL Java Apollo Federation.
GraphQL Schema Definition Language (SDL) :
type Query {
planets: [Planet!]!
planet(id: ID!): Planet
planetByName(name: String!): Planet
}
type Mutation {
createPlanet(name: String!, type: Type!, details: DetailsInput!): Planet!
}
type Subscription {
latestPlanet: Planet!
}
type Planet @key(fields: "id") {
id: ID!
name: String!
# from an astronomical point of view
type: Type!
isRotatingAroundSun: Boolean! @deprecated(reason: "Now it is not in doubt. Do not use this field")
details: Details!
}
interface Details {
meanRadius: Float!
mass: BigDecimal!
}
type InhabitedPlanetDetails implements Details {
meanRadius: Float!
mass: BigDecimal!
# in billions
population: Float!
}
type UninhabitedPlanetDetails implements Details {
meanRadius: Float!
mass: BigDecimal!
}
enum Type {
TERRESTRIAL_PLANET
GAS_GIANT
ICE_GIANT
DWARF_PLANET
}
input DetailsInput {
meanRadius: Float!
mass: MassInput!
population: Float
}
input MassInput {
number: Float!
tenPower: Int!
}
scalar BigDecimal
Planet service ( )
Planet.id
ID
, 5- . GraphQL Java . , null, ( Kotlin GraphQL nullable ). @directive
’ . . IntelliJ IDEA, JS GraphQL plugin .
GraphQL API:
schema-first
(, , API),
code-first
; . schema-first . .
Micronaut , GraphQL IDE — GraphiQL — GraphQL :
graphql:
graphiql:
enabled: true
GraphiQL ( )
Main :
object PlanetServiceApplication {
@JvmStatic
fun main(args: Array<String>) {
Micronaut.build()
.packages("io.graphqlfederation.planetservice")
.mainClass(PlanetServiceApplication.javaClass)
.start()
}
}
Main ( )
GraphQL :
@Bean
@Singleton
fun graphQL(resourceResolver: ResourceResolver): GraphQL {
val schemaInputStream = resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()
val transformedGraphQLSchema = FederatedSchemaBuilder()
.schemaInputStream(schemaInputStream)
.runtimeWiring(createRuntimeWiring())
.excludeSubscriptionsFromApolloSdl(true)
.build()
return GraphQL.newGraphQL(transformedGraphQLSchema)
.instrumentation(
ChainedInstrumentation(
listOf(
FederatedTracingInstrumentation()
)
)
)
.build()
}
GraphQL ( )
FederatedSchemaBuilder
Apollo Federation. GraphQL Java , (. ).
RuntimeWiring
— data fetcher’, type resolver’ , GraphQLSchema
; :
private fun createRuntimeWiring(): RuntimeWiring {
val detailsTypeResolver = TypeResolver { env ->
when (val details = env.getObject() as DetailsDto) {
is InhabitedPlanetDetailsDto -> env.schema.getObjectType("InhabitedPlanetDetails")
is UninhabitedPlanetDetailsDto -> env.schema.getObjectType("UninhabitedPlanetDetails")
else -> throw RuntimeException("Unexpected details type: ${details.javaClass.name}")
}
}
return RuntimeWiring.newRuntimeWiring()
.type("Query") { builder ->
builder
.dataFetcher("planets", planetsDataFetcher)
.dataFetcher("planet", planetDataFetcher)
.dataFetcher("planetByName", planetByNameDataFetcher)
}
.type("Mutation") { builder ->
builder.dataFetcher("createPlanet", createPlanetDataFetcher)
}
.type("Subscription") { builder ->
builder.dataFetcher("latestPlanet", latestPlanetDataFetcher)
}
.type("Planet") { builder ->
builder.dataFetcher("details", detailsDataFetcher)
}
.type("Details") { builder ->
builder.typeResolver(detailsTypeResolver)
}
.type("Type") { builder ->
builder.enumValues(NaturalEnumValuesProvider(Planet.Type::class.java))
}
.build()
}
RuntimeWiring
( )
root- Query
( root- Mutation
Subscription
), , planets
, , DataFetcher
:
@Singleton
class PlanetsDataFetcher(
private val planetService: PlanetService,
private val planetConverter: PlanetConverter
) : DataFetcher<List<PlanetDto>> {
override fun get(env: DataFetchingEnvironment): List<PlanetDto> = planetService.getAll()
.map { planetConverter.toDto(it) }
}
PlanetsDataFetcher
( )
env
. DTO, :
@Singleton
class PlanetConverter : GenericConverter<Planet, PlanetDto> {
override fun toDto(entity: Planet): PlanetDto {
val details = DetailsDto(id = entity.detailsId)
return PlanetDto(
id = entity.id,
name = entity.name,
type = entity.type,
details = details
)
}
}
PlanetConverter
( )
GenericConverter
— Entity → DTO
. , details
— , , API. , details
id
. , RuntimeWiring
details
Planet
DataFetcher
, ( details.id
, ):
@Singleton
class DetailsDataFetcher : DataFetcher<CompletableFuture<DetailsDto>> {
private val log = LoggerFactory.getLogger(this.javaClass)
override fun get(env: DataFetchingEnvironment): CompletableFuture<DetailsDto> {
val planetDto = env.getSource<PlanetDto>()
log.info("Resolve `details` field for planet: ${planetDto.name}")
val dataLoader: DataLoader<Long, DetailsDto> = env.getDataLoader("details")
return dataLoader.load(planetDto.details.id)
}
}
DetailsDataFetcher
( )
, CompletableFuture
. Details
DetailsService
, , N+1: , :
{
planets {
name
details {
meanRadius
}
}
}
GraphQL
details
SQL . java-dataloader; BatchLoader
DataLoaderRegistry
:
@Bean
@Singleton
fun detailsBatchLoader(): BatchLoader<Long, DetailsDto> = BatchLoader { keys ->
CompletableFuture.supplyAsync {
detailsService.getByIds(keys)
.map { detailsConverter.toDto(it) }
}
}
@Bean
fun dataLoaderRegistry() = DataLoaderRegistry().apply {
val detailsDataLoader = DataLoader.newDataLoader(detailsBatchLoader())
register("details", detailsDataLoader)
}
BatchLoader
DataLoaderRegistry
( )
BatchLoader
Details
. , SQL N+1. , GraphQL , SQL . BatchLoader
stateless , . DataLoader
BatchLoader
; stateful, , DataLoaderRegistry
. - GraphQL , . GraphQL Java.
Details
GraphQL , RuntimeWiring
TypeResolver
, GraphQL DTO :
val detailsTypeResolver = TypeResolver { env ->
when (val details = env.getObject() as DetailsDto) {
is InhabitedPlanetDetailsDto -> env.schema.getObjectType("InhabitedPlanetDetails")
is UninhabitedPlanetDetailsDto -> env.schema.getObjectType("UninhabitedPlanetDetails")
else -> throw RuntimeException("Unexpected details type: ${details.javaClass.name}")
}
}
TypeResolver
( )
enum’ :
.type("Type") { builder ->
builder.enumValues(NaturalEnumValuesProvider(Planet.Type::class.java))
}
enum ( )
http://localhost:8082/graphiql
GraphiQL IDE, , ; IDE 3 : (query/mutation/subscription), :

GraphQL IDE, , GraphQL Playground Altair ( desktop , web-). :

query, , : _service
_entities
. , GraphQL Java Apollo Federation; .
Planet
, :

type
, @deprecated
isRotatingAroundSun
.
:
type Mutation {
createPlanet(name: String!, type: Type!, details: DetailsInput!): Planet!
}
( )
query, . , , input
type
:
input DetailsInput {
meanRadius: Float!
mass: MassInput!
population: Float
}
input MassInput {
number: Float!
tenPower: Int!
}
Input
Query, DataFetcher
:
@Singleton
class CreatePlanetDataFetcher(
private val objectMapper: ObjectMapper,
private val planetService: PlanetService,
private val planetConverter: PlanetConverter
) : DataFetcher<PlanetDto> {
private val log = LoggerFactory.getLogger(this.javaClass)
override fun get(env: DataFetchingEnvironment): PlanetDto {
log.info("Trying to create planet")
val name = env.getArgument<String>("name")
val type = env.getArgument<Planet.Type>("type")
val detailsInputDto = objectMapper.convertValue(env.getArgument("details"), DetailsInputDto::class.java)
val newPlanet = planetService.create(
name,
type,
detailsInputDto.meanRadius,
detailsInputDto.mass.number,
detailsInputDto.mass.tenPower,
detailsInputDto.population
)
return planetConverter.toDto(newPlanet)
}
}
DataFetcher
( )
, - . subscription:
type Subscription {
latestPlanet: Planet!
}
Subscription ( )
DataFetcher
subscription Publisher
:
@Singleton
class LatestPlanetDataFetcher(
private val planetService: PlanetService,
private val planetConverter: PlanetConverter
) : DataFetcher<Publisher<PlanetDto>> {
override fun get(environment: DataFetchingEnvironment) = planetService.getLatestPlanet().map { planetConverter.toDto(it) }
}
DataFetcher
subscription ( )
mutation subscription GraphQL IDE IDE; ( IDE subscription URL ws://localhost:8082/graphql-ws
):
subscription {
latestPlanet {
name
type
}
}
subscription
:
mutation {
createPlanet(
name: "Pluto"
type: DWARF_PLANET
details: { meanRadius: 50.0, mass: { number: 0.0146, tenPower: 24 } }
) {
id
}
}
mutation
:

Subscription’ Micronaut :
graphql:
graphql-ws:
enabled: true
GraphQL WebSocket ( )
subscription’ Micronaut — chat application. GraphQL Java.
query mutation :
@Test
fun testPlanets() {
val query = """
{
planets {
id
name
type
details {
meanRadius
mass
... on InhabitedPlanetDetails {
population
}
}
}
}
""".trimIndent()
val response = graphQLClient.sendRequest(query, object : TypeReference<List<PlanetDto>>() {})
assertThat(response, hasSize(8))
assertThat(
response, contains(
hasProperty("name", `is`("Mercury")),
hasProperty("name", `is`("Venus")),
hasProperty("name", `is`("Earth")),
hasProperty("name", `is`("Mars")),
hasProperty("name", `is`("Jupiter")),
hasProperty("name", `is`("Saturn")),
hasProperty("name", `is`("Uranus")),
hasProperty("name", `is`("Neptune"))
)
)
}
query ( )
query query, :
private val planetFragment = """
fragment planetFragment on Planet {
id
name
type
details {
meanRadius
mass
... on InhabitedPlanetDetails {
population
}
}
}
""".trimIndent()
@Test
fun testPlanetById() {
val earthId = 3
val query = """
{
planet(id: $earthId) {
... planetFragment
}
}
$planetFragment
""".trimIndent()
val response = graphQLClient.sendRequest(query, object : TypeReference<PlanetDto>() {})
}
Query ( )
variables, :
@Test
fun testPlanetByName() {
val variables = mapOf("name" to "Earth")
val query = """
query testPlanetByName(${'$'}name: String!){
planetByName(name: ${'$'}name) {
... planetFragment
}
}
$planetFragment
""".trimIndent()
val response = graphQLClient.sendRequest(query, variables, null, object : TypeReference<PlanetDto>() {})
}
Query ( )
, . . Kotlin raw strings, string templates, , $
( GraphQL) ${'$'}
.
GraphQLClient
— (framework-agnostic OkHttp). Java GraphQL , , Apollo GraphQL Client for Android and the JVM, .
in-memory H2 ORM Hibernate, micronaut-data-hibernate-jpa
. .
Auth service
GraphQL . JWT. Auth service JWT query mutation:
type Query {
validateToken(token: String!): Boolean!
}
type Mutation {
signIn(data: SignInData!): SignInResponse!
}
input SignInData {
username: String!
password: String!
}
type SignInResponse {
username: String!
token: String!
}
Auth service ( )
JWT, GraphQL IDE (Auth service URL http://localhost:8081/graphql
):
mutation {
signIn(data: {username: "john_doe", password: "password"}) {
token
}
}
JWT
Authorization
( Altair GraphQL Playground IDE) ; . Bearer $JWT
.
JWT micronaut-security-jwt
.
Satellite service
:
type Query {
satellites: [Satellite!]!
satellite(id: ID!): Satellite
satelliteByName(name: String!): Satellite
}
type Satellite {
id: ID!
name: String!
lifeExists: LifeExists!
firstSpacecraftLandingDate: Date
}
type Planet @key(fields: "id") @extends {
id: ID! @external
satellites: [Satellite!]!
}
enum LifeExists {
YES,
OPEN_QUESTION,
NO_DATA
}
scalar Date
Satellite service ( )
, Satellite
lifeExists
. , , GraphQL query/mutation/subscription , . . GraphQL /graphql
. , — GraphQL-specific , ( ):
micronaut:
security:
enabled: true
intercept-url-map:
- pattern: /graphql
httpMethod: POST
access:
- isAnonymous()
- pattern: /graphiql
httpMethod: GET
access:
- isAnonymous()
Security ( )
DataFetcher
, :
@Singleton
class LifeExistsDataFetcher(
private val satelliteService: SatelliteService
) : DataFetcher<Satellite.LifeExists> {
override fun get(env: DataFetchingEnvironment): Satellite.LifeExists {
val id = env.getSource<SatelliteDto>().id
return satelliteService.getLifeExists(id)
}
}
LifeExistsDataFetcher
( )
:
@Singleton
class SatelliteService(
private val repository: SatelliteRepository,
private val securityService: SecurityService
) {
fun getLifeExists(id: Long): Satellite.LifeExists {
val userIsAuthenticated = securityService.isAuthenticated
if (userIsAuthenticated) {
return repository.findById(id)
.orElseThrow { RuntimeException("Can't find satellite by id=$id") }
.lifeExists
} else {
throw RuntimeException("`lifeExists` property can only be accessed by authenticated users")
}
}
}
SatelliteService
( )
Authorization
JWT (. ):
{
satellite(id: "1") {
name
lifeExists
}
}
. ( Base64 ):
micronaut:
security:
token:
jwt:
enabled: true
signatures:
secret:
validation:
base64: true
# In real life, the secret should NOT be under source control (instead of it, for example, in environment variable).
# It is here just for simplicity.
secret: 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA=='
jws-algorithm: HS256
JWT ( )
. JWT ( validateToken
, ).
Date
, DateTime
GraphQL Java graphql-java-extended-scalars (com.graphql-java:graphql-java-extended-scalars:$graphqlJavaExtendedScalarsVersion
-). (scalar Date
) :
private fun createRuntimeWiring(): RuntimeWiring = RuntimeWiring.newRuntimeWiring()
.scalar(ExtendedScalars.Date)
.build()
( )
:
{
satelliteByName(name: "Moon") {
firstSpacecraftLandingDate
}
}
Request
{
"data": {
"satelliteByName": {
"firstSpacecraftLandingDate": "1959-09-13"
}
}
}
Response
GraphQL API (. ). , :
{
planet(id: "1") {
star {
planets {
star {
planets {
star {
... # more deep nesting!
}
}
}
}
}
}
}
“” query
, MaxQueryDepthInstrumentation
. query MaxQueryComplexityInstrumentation
; FieldComplexityCalculator
, . ( FieldComplexityCalculator
, , — , 1):
return GraphQL.newGraphQL(transformedGraphQLSchema)
.instrumentation(
ChainedInstrumentation(
listOf(
FederatedTracingInstrumentation(),
MaxQueryComplexityInstrumentation(50, FieldComplexityCalculator { env, child ->
1 + child
}),
MaxQueryDepthInstrumentation(5)
)
)
)
.build()
( )
, MaxQueryDepthInstrumentation
/ MaxQueryComplexityInstrumentation
, IDE, . . IDE IntrospectionQuery
, ( GitHub). FederatedTracingInstrumentation
, Apollo Gateway ( Apollo Graph Manager; , ). GraphQL Java.
GraphQL . -, , Micronaut:
@Singleton
@Primary
class HeaderValueProviderGraphQLExecutionInputCustomizer : DefaultGraphQLExecutionInputCustomizer() {
override fun customize(executionInput: ExecutionInput, httpRequest: HttpRequest<*>): Publisher<ExecutionInput> {
val context = HTTPRequestHeaders { headerName ->
httpRequest.headers[headerName]
}
return Publishers.just(executionInput.transform {
it.context(context)
})
}
}
GraphQLExecutionInputCustomizer
( )
FederatedTracingInstrumentation
, , , Apollo Server , .
:
@Singleton
class CustomDataFetcherExceptionHandler : SimpleDataFetcherExceptionHandler() {
private val log = LoggerFactory.getLogger(this.javaClass)
override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters): DataFetcherExceptionHandlerResult {
val exception = handlerParameters.exception
log.error("Exception while GraphQL data fetching", exception)
val error = object : GraphQLError {
override fun getMessage(): String = "There was an error: ${exception.message}"
override fun getErrorType(): ErrorType? = null
override fun getLocations(): MutableList<SourceLocation>? = null
}
return DataFetcherExceptionHandlerResult.newResult().error(error).build()
}
}
( )
— GraphQL (Planet
) ( ) Apollo Server. Planet
Planet service :
type Planet @key(fields: "id") {
id: ID!
name: String!
# from an astronomical point of view
type: Type!
isRotatingAroundSun: Boolean! @deprecated(reason: "Now it is not in doubt. Do not use this field")
details: Details!
}
Planet
Planet service ( )
Satellite service Planet
satellites
(, , non-nullable non-nullable ):
type Satellite {
id: ID!
name: String!
lifeExists: LifeExists!
firstSpacecraftLandingDate: Date
}
type Planet @key(fields: "id") @extends {
id: ID! @external
satellites: [Satellite!]!
}
Planet
Satellite service ( )
Apollo Federation Planet
— entity — , ( Satellite service, stub Planet
). @key
, , . @extends
, Planet
— , ( Planet service). Apollo Federation Apollo.
Apollo Federation; GraphQL Java, :
API; GitHub.
, Planet
FederatedEntityResolver
( , , Planet.satellites
); FederatedSchemaBuilder
:
@Bean
@Singleton
fun graphQL(resourceResolver: ResourceResolver): GraphQL {
val planetEntityResolver = object : FederatedEntityResolver<Long, PlanetDto>("Planet", { id ->
log.info("`Planet` entity with id=$id was requested")
val satellites = satelliteService.getByPlanetId(id)
PlanetDto(id = id, satellites = satellites.map { satelliteConverter.toDto(it) })
}) {}
val transformedGraphQLSchema = FederatedSchemaBuilder()
.schemaInputStream(schemaInputStream)
.runtimeWiring(createRuntimeWiring())
.federatedEntitiesResolvers(listOf(planetEntityResolver))
.build()
}
GraphQL Satellite service ( )
query (_service
and _entities
), Apollo Server. query , Apollo Server’. Apollo Federation - standalone-. API .
Apollo Server
Apollo Server Apollo Federation :
, , frontend- , .
— schema stitching — Apollo deprecated. , , : Nadel. GraphQL Java Apollo Federation; .
:
{
"name": "api-gateway",
"main": "gateway.js",
"scripts": {
"start-gateway": "nodemon gateway.js"
},
"devDependencies": {
"concurrently": "5.1.0",
"nodemon": "2.0.2"
},
"dependencies": {
"@apollo/gateway": "0.12.0",
"apollo-server": "2.10.0",
"graphql": "14.6.0"
}
}
-, ( )
const {ApolloServer} = require("apollo-server");
const {ApolloGateway, RemoteGraphQLDataSource} = require("@apollo/gateway");
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({request, context}) {
request.http.headers.set('Authorization', context.authHeaderValue);
}
}
const gateway = new ApolloGateway({
serviceList: [
{name: "auth-service", url: "http://localhost:8081/graphql"},
{name: "planet-service", url: "http://localhost:8082/graphql"},
{name: "satellite-service", url: "http://localhost:8083/graphql"}
],
buildService({name, url}) {
return new AuthenticatedDataSource({url});
},
});
const server = new ApolloServer({
gateway, subscriptions: false, context: ({req}) => ({
authHeaderValue: req.headers.authorization
})
});
server.listen().then(({url}) => {
console.log(` Server ready at ${url}`);
});
Apollo Server ( )
, ( ); , .
, ( Authorization
). security, , JWT apollo-server
.
, 3 GraphQL Java , , cd
apollo-server
, :
npm install
npm run start-gateway
:
[nodemon] 2.0.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node gateway.js`
Server ready at http://localhost:4000/
[INFO] Sat Feb 15 2020 13:22:37 GMT+0300 (Moscow Standard Time) apollo-gateway: Gateway successfully loaded schema.
* Mode: unmanaged
Apollo Server
GraphQL :

http://localhost:4000/playground
Playground IDE.
, , query MaxQueryComplexityInstrumentation
/ MaxQueryDepthInstrumentation
, GraphQL IDE . , Apollo Server query { _service { sdl } }
IntrospectionQuery
.
, :
, /, downstream Apollo Server’, Federation; , Apollo.
GraphQL JVM. API GraphQL Java GraphQL API; . Apollo Server, Apollo Federation graphql-java-federation. GitHub. !