Skip to content

20.0

Compare
Choose a tag to compare
@bbakerman bbakerman released this 06 Dec 23:49
· 1320 commits to master since this release
557ad5d

We are pleased to announce the release of graphql-java 20.0. Special thanks to each of the 200+ contributors over the years, who have made this milestone possible.

Breaking changes

Aligning parseValue coercion with JS reference implementation

We have made changes to String, Boolean, Float, and Int parseValue coercion, to be consistent with the reference JS implementation. The key change is parseValue is now stricter on accepted inputs.

  • String parseValue now requires input is of type String. For example, a Number input 123 or a Boolean input true will no longer be accepted.
  • Boolean parseValue now requires input is of type Boolean. For example, a String input "true" will no longer be accepted.
  • Float parseValue now requires input is of type Number. For example, a String input "3.14" will no longer be accepted.
  • Int parseValue now requires input is of type Number. For example, a String input "42" will no longer be accepted.

String parseValue changes: #3030
Boolean, Float, and Int parseValue changes: #3042
JS reference implementation: https://github.com/graphql/graphql-js/blob/main/src/type/scalars.ts

Notable Changes

Record Like Property Fetching Support

We have now added the ability to find properties via "Record like" naming. We call it "Record like" based on Java 14 record classes but in fact any class with a method named directly as the graphql field is named will work.

If you had this graphql object type declared

type Person {
   name : String
   address : String
}

then this Java record would be supported for fetching values via the method names name() and address()

public record Person (String name, String address)

and equally a non record class like this would also work

public class Person {
   public String name() { return "Harry Potter"; }
   public String address() { return "4 Privet Drive, Little Whinging"; }
}

We still have Java Bean (aka POJO) getter naming support like public String getName() however now the "record like" name() method will be used in preference and then the getName() methods will be used if that's not present.

This means there is a new behavior if you had weird POJOs likes this

public class WeirdPerson {
   public String name() { return "Harry Potter"; }
   public String getName() { return "Tom Riddle"; }
}

A property fetch for name will now return Harry Potter and not Tom Riddle as it previously would have.

This is a behavioral breaking change but on balance we think this behavior is the most correct going forward.

#2994

Improved Data Fetching

The PropertyDataFetcher class is the most common data fetcher used in graphql-java. It uses Java reflection to get field values from objects based on field name.

This was logically the following

Method method = findMethod(fieldname);
method.invoke(object);

with the method lookup cached for performance reasons.

However there is mechanism in the JVM that provides even faster object reflective access.

See

https://wttech.blog/blog/2020/method-handles-and-lambda-metafactory/
https://www.optaplanner.org/blog/2018/01/09/JavaReflectionButMuchFaster.html

java.lang.invoke.LambdaMetafactory#metafactory is an arcane mechanism that can be used to create virtual method lambdas that give fast access to call object methods. It turns out to be significantly faster that Java reflection and only marginally slower that directly invoking a method.

If you use PropertyDataFetcher a lot (and chances are you do) then this should give improved performance.

The raw benchmarks are as follows

Java 8

Benchmark                                       Mode  Cnt         Score         Error  Units
GetterAccessBenchmark.measureDirectAccess      thrpt   15  81199548.105 ± 2717206.756  ops/s 0% slower (baseline)
GetterAccessBenchmark.measureLambdaAccess      thrpt   15  79622345.446 ± 1183553.379  ops/s 2% slower
GetterAccessBenchmark.measureReflectionAccess  thrpt   15  46102664.133 ± 4091595.318  ops/s 50% slower

Java 17


Benchmark                                       Mode  Cnt          Score          Error  Units
GetterAccessBenchmark.measureDirectAccess      thrpt   15  458411420.717 ± 34329506.990  ops/s 0%
GetterAccessBenchmark.measureLambdaAccess      thrpt   15  334158880.091 ± 10666070.698  ops/s 27% slower
GetterAccessBenchmark.measureReflectionAccess  thrpt   15   63181868.566 ±  3887367.970  ops/s  86% slower

It's worth noting that while the headline numbers here look impressive, the property fetching represents a smaller portion of what happens during graphql engine execution.

It probably won't be enough to keep Elon Musk happy but all performance improvements help and at scale they help the most.

Lightweight Data Fetchers

A DataFetcher gets invoked with a calling environment context object called graphql.schema.DataFetchingEnvironment. This is quite a rich object that contains all sorts of useful information.

However simple (aka trivial) data fetchers like PropertyDataFetcher they don't need access to such a rich object. They just need the source object, the field name and the field type

To marginally help performance, we have introduced a graphql.schema.LightDataFetcher for this use case

public interface LightDataFetcher<T> extends TrivialDataFetcher<T> {

    
    T get(GraphQLFieldDefinition fieldDefinition, Object sourceObject, Supplier<DataFetchingEnvironment> environmentSupplier) throws Exception;
}

PropertyDataFetcher implements this and hence this lowers the object allocation at scale (which reduces memory pressure) and will make the system marginally faster to fetch data.

#2953

Performance Improvements by avoid object allocations

We are always trying to wring out the most performance we can in graphql-java and so we reviewed our object allocations and found places where we can make savings.

These won't make dramatic performance savings but at scale all these things add up, reducing memory pressure and improving throughput marginally.

#2981
#2980
#2979

Locale is now available in Coercing and Parsing

The graphql.schema.Coercing interface used by scalars can now receive a Locale object that indicates the calling Locale. The same is true for the parsing code via graphql.parser.ParserEnvironment#getLocale

A custom scalar implementation could use the locale to decide how to coerce values.

#2912
#2921

Easier ways to build common objects

We have added extra builders on the GraphQLError, ErrorClassification and ExecutionResult interfaces that make it easier to build instances of these common classes.

#2939
#3011

The deprecated NextGen engine has been removed

The NextGen engine was an experimental feature that explored what it might take to build a new graphql engine. In many ways it was a success as it taught us a bunch of about graph algorithms and what works and what does not.

While it had some value, on balance it was not going to become production ready and so we deprecated it a while back and it has finally been removed.

#2923

What's Changed

  • docs: update latest release badge to 19 by @setchy in #2918
  • Fix printing directives when they contain something like a formatting… by @jmartisk in #2920
  • Implement pretty printer by @felipe-gdr in #2894
  • Fix snapshot badge by @dondonz in #2924
  • Remove @fetch and nextgen engine by @dondonz in #2923
  • Fix up field visibility doco example by @dondonz in #2927
  • We can rename scalar types by @bbakerman in #2928
  • Add deprecation date to all deprecated methods and fields by @dondonz in #2929
  • Fix field visibility bug with enum with enum args by @felipe-gdr in #2926
  • Adding Locale to Coercing and hence ValueResolver by @bbakerman in #2912
  • Removes the deprecated execute methods from GraphQL by @bbakerman in #2932
  • Removing deprecated methods from tests - part 1 by @dondonz in #2930
  • Reproduction of renaming scalars and applied directives bug by @bbakerman in #2934
  • Remove redundant NaN check, already handled in GraphqlFloatCoercing by @dondonz in #2936
  • Diff counts are the same by @bbakerman in #2935
  • Change Instrumentation production implementations to use non deprecated methods by @bbakerman in #2931
  • Make parseValue nullable and update Coercing javadoc by @dondonz in #2938
  • Cleaning up tests with deprecated usage: Part 2 by @dondonz in #2941
  • Deprecation test cleanup: GraphQLFieldDefinition datafetcher builder by @dondonz in #2942
  • Fix @Skip definition is added twice to the GraphQLSchema when defined in sdl by @tinnou in #2940
  • Added new builder to ExecutionResult by @bbakerman in #2939
  • Test cleanup: typeResolver builder on unions and interfaces, applied directive tests, and more by @dondonz in #2954
  • Update GraphQL Instrospection Spec Link by @cookieMr in #2977
  • Deprecation cleanout - programmatic schemas by @dondonz in #2974
  • Avoid allocating a type resolve env if the type is already an object type by @bbakerman in #2980
  • Avoids allocating a copy of the field names set inside the ES by @bbakerman in #2981
  • Patch SchemaDiff to apply respective nullability change validation for input vs. output types by @bspeth in #2971
  • Adding Locale to Parser by @bbakerman in #2921
  • Added workflow and gcp JS client library script with package json by @DiegoManzanarezx in #2889
  • Add missing equals/hashcode methods to relay classes by @pgr0ss in #2988
  • Float coercion tidy up by @dondonz in #2982
  • Only run GCP tests on main graphql-java repo (skip on forks). by @folone in #2997
  • Parameterized introspection queries by @MayCXC in #2993
  • Update aQute builder version by @dondonz in #3002
  • Avoid an allocation of a chained context in the most common case by @bbakerman in #2979
  • Polishing by @dfa1 in #3001
  • Document the new IntrospectionQueryBuilder and tweaked it a little by @bbakerman in #3003
  • LambdaMetafactory support for property fetches by @bbakerman in #2985
  • SchemaGeneratorPostProcessing should be deprecated by @bbakerman in #2999
  • Fixes in TwitterBenchmark by @dfa1 in #3006
  • Bugfix for SDL check if an Interface is implemented correctly by @andimarek in #3014
  • Adds an error builder on GraphQLError by @bbakerman in #3011
  • TypeResolutionEnvironment#getLocalContext seems accidentally non-public by @kaqqao in #3021
  • centralizing resource loading in BenchmarkUtils by @dfa1 in #3022
  • Helper for getting fields based on object type name by @bbakerman in #3016
  • Avoiding some duplicated work in Async by @dfa1 in #3023
  • Record like property access support by @bbakerman in #2994
  • Lightweight data fetchers by @bbakerman in #2953
  • Update ValidationError#toString to print the extentions field by @federicorispo in #3024
  • Minor fixes by @dfa1 in #3029
  • This makes sure every introspection type actually has a concrete data fetcher by @bbakerman in #3004
  • Make String parseValue coercion consistent with JS implementation & Gradle JCenter fix by @dondonz in #3030
  • Adding new schema diffing capability (First step) by @andimarek in #2983
  • chore: Remove unused imports and local variables by @federicorispo in #3034
  • Testing I18n lookup by @bbakerman in #3036
  • Consider union containers when checking if type is referenced by @felipe-gdr in #3037
  • master fix - use class loader on i18n by @bbakerman in #3039

New Contributors

Full Changelog: v19.1...v20.0