GraphQL est un langage de requĂȘte API dĂ©veloppĂ© par Facebook. Cet article prĂ©sentera un exemple d'implĂ©mentation de l'API GraphQL sur la JVM, en particulier, en utilisant le langage Kotlin et le framework Micronaut ; la plupart des exemples peuvent ĂȘtre rĂ©utilisĂ©s sur d'autres frameworks Java / Kotlin. Ensuite, il sera montrĂ© comment combiner plusieurs services GraphQL en un seul graphique de donnĂ©es pour fournir une interface commune pour l'accĂšs Ă toutes les sources de donnĂ©es. Ceci est implĂ©mentĂ© Ă l'aide d' Apollo Server et d' Apollo Federation . En consĂ©quence, l'architecture suivante sera obtenue:

Chaque composant de l'architecture met en évidence plusieurs problÚmes pouvant survenir lors du développement de l'API GraphQL. Le modÚle de domaine comprend des données sur les planÚtes du systÚme solaire et leurs satellites.
:
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. !