Going native: enabling Specification Argument Resolver for GraalVM

In one of the previous articles, we described the GraalVM native image technology. The main motivation of the previous article was our willingness to evaluate technology in context of usage in our apps in the future. However, there is one additional motivation which stands behind our interest in this topic, the open source project maintained by Tratif! In the below article we described the problems which we have to solve in the Specification Argument Resolver to support library compilation to GraalVM native image. If you want to familiarize with the library first, please take a look at the previous articles: Effective RESTful Search API In Spring, Open-Source For The Win! Benefits Of Contributing To OSS.

Specification argument resolver

Specification Argument Resolver is an alternative API for filtering data with Spring MVC & Spring Data JPA. It was described at the first time on our blog in the article about effective RESTful Search API In Spring. It allows us to define a spring search specification for our model using annotations above the endpoint parameters. 

To understand what is it, let’s consider the following entity:

@Entity
public class Customer {
 
   @Id
   @GeneratedValue
   private Long id;

   private String firstName;

   private Date registrationDate;
 
   @Embedded
   private Address address;
 
};
 
@Embeddable
public class Address {

   private String city;

};

and let’s assume that we want to create an REST API which allows us to search customers by fields: firstName, registrationDate, city. With Specifications Argument Resolver the API could be implemented in this way:

@GetMapping("/customers")
public List<Customer> findCustomers(@And({
     @Spec(path = "firstName", params = "firstName", spec = Like.class),
     @Spec(path = "registrationDate", params = "registrationDate", spec = GreaterThan.class),
     @Spec(path = "city", params = "city", spec = EqualIgnoreCase.class)
}) Specification<Customer> specification) {
  return customerRepository.findAll(specification);
}

For the request:

GET /customers?firstName=Jakub&city=Trzebnica&registrationDate=10.01.2023T10:00:00

the query similar to the one below will be generated:

SELECT c.id, c.first_name, c.registration_date, c.street FROM Customer c WHERE c.first_name LIKE "%Jakub%" AND UPPER(c.street)=UPPER('Trzebnica') AND c.last_order_time > "2020-06-16T10:08:53"

As you can notice in the above example, Specification Argument Resolver creates a Spring Specification based on metadata in annotations and on the request data (HTTP parameters, path variable, headers, body). If you want to use the above approach in your application, please read our article about effective RESTful Search API In Spring

Challenges with native-image support

Specification Argument Resolver has been maintained since 2014. Originally, it was supporting only spring boot 1.X. In 2019 the library was migrated to spring boot 2.X. In 2023 the next stage in the library’s history began, the 3.0 (actual) version that supports the spring boot 3.0 and GraalVM native image was released. The migration of spring-boot version 2.X to 3.X and hibernate version 5.X to 6.X is out of scope of this article. On the internet there are many articles about this topic and we didn’t encounter any new challenges that are worth mentioning. However, in the context of adding support for GraalVM native-image we faced the two challenges which solutions have been described below.

During the spring boot version migration in our library, we were surprised by two behaviors:
– From Hibernate ORM 6, distinct is always passed to the SQL query. See the Hibernate migration guide for details.
– “When limits or pagination are combined with a fetch join, Hibernate must retrieve all matching results from the database and apply the limit in memory!” – it can lead to OutOfMemoryError in some scenarios. See Hibernate documentation for details.

Reflection

Specification Argument Resolver supports over 30 types of specifications with various constructor signatures. Internally, the specification classes are created using Java Reflection mechanism.

Let’s look at the attributes of the basic specification definition:

 @Spec(path = "firstName", params = "name", spec = Like.class)

In the above example:

  • path – defines a name of entity field (in this case specification should be applied to firstName attribute of the entity)
  • params – defines a name of http parameter which value should be used during search
  • spec – type of specification (Like.class, Equal.class etc)

Based on specification definition, Specification Argument Resolver creates an instance of a particular specification depending on type defined in spec attribute using reflection mechanism. The simplified fragment of logic responsible for this looks as follows:

private Specification<Object> newSpecification(Spec def, String[] argsArray, ProcessingContext context) throws InstantiationException, IllegalAccessException,
     InvocationTargetException, NoSuchMethodException {
 
  QueryContext queryCtx = context.queryContext();
  Converter converter = resolveConverter(def);
 
  Specification<Object> spec;
 
  try {
     spec = def.spec().getConstructor(QueryContext.class, String.class, String[].class)
           .newInstance(queryCtx, def.path(), argsArray);
  } catch (NoSuchMethodException e2) {
     spec = def.spec().getConstructor(QueryContext.class, String.class, String[].class, Converter.class)
           .newInstance(queryCtx, def.path(), argsArray, converter);
  }
 
  return spec;
}

The reflection mechanism described above it is not supported by GraalVM native image out-of-the box. To add support for this mechanism in our open source library we had two options:

  • Provide a reachability-metadata – the configuration files which could be used during the compilation to the native code. These configuration files could contain information about code accessed via Reflection.
  • Provide a java configuration class which could be imported in the user application. Spring provides a mechanism which allows to declare a reflection config programmatically, using the RuntimeHintsRegistrar API.

We decided to provide a solution based on the second option. Main reason for our decision was the fact that not all java classes and interfaces used together with our library are known during the library compilation time – we are not able to generate the complete reachability-metadata in advance. The user can define their own specifications or specification definitions for which the reachability-metadata should be also configured. The other reason comes from the fact that the second solution is based on the Spring Framework API for which the library was originally built.

We implemented specification argument hints registrar. It is implementation is quite simple:

public class SpecificationArgumentResolverHintRegistrar implements RuntimeHintsRegistrar {
 
   private static final Set<Class<?>> SPECIFICATION_CLASSES = Set.of(
           Equal.class,
           NotEqual.class
           // Other specification classes 
           // the list has been shortened to reduce amount of lines in this example
   );
 
   @Override
   public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
       SPECIFICATION_CLASSES.forEach(specificationClass -> {
           for (Constructor<?> constructor : specificationClass.getConstructors()) {
               hints.reflection().registerConstructor(constructor, ExecutableMode.INVOKE);
           }
       });
   }
}

(The full java code can be found on GitHub: SpecificationArgumentResolverHintRegistrar.java

In the listing above, we can see registration of each constructor of each specification for invocation. When the user application imports the above registar, then relevant reachability metadata is generated during the compilation phase.

Example of registrar import:

@Configuration
@ImportRuntimeHints({
     SpecificationArgumentResolverHintRegistrar.class
})
public class UserApplicationConfig {
}

Below you can find a fragment of the generated reflect-config.json which is used by GraalVM during compilation:

[
…, //other config entries
{
 "name": "net.kaczmarzyk.spring.data.jpa.domain.Equal",
 "methods": [
   {
     "name": "<init>",
     "parameterTypes": [
       "net.kaczmarzyk.spring.data.jpa.utils.QueryContext",
       "java.lang.String",
       "java.lang.String[]",
       "net.kaczmarzyk.spring.data.jpa.utils.Converter"
     ]
   }
 ]
},
…, //other config entries
]

In the snippet example, we can see that Equal constructor with parameter types QueryContext, String, String[], Converter is registered as reachable via reflection. In result, the user application will be able to invoke this constructor using reflection.

Dynamic proxy

Specifications can be also defined above the custom interfaces which extend Specification<EntityType>. For example:

@Or({
     @Spec(path="firstName", params="name", spec=Like.class),
     @Spec(path="lastName", params="name", spec=Like.class)
})
public interface FullNameSpec extends Specification<Customer> {
}

Then in the controller we can just do as follows:

@RequestMapping("/customers")
@ResponseBody
public List<Customer> findByFullName(FullNameSpec spec) {
  return repository.findAll(spec);
}

Specification Argument Resolver detects FullNameSpec as supported parameter and resolves the specifications. However, the type of resolved specifications is a Disjunction (with two inner specs Like) not the FullNameSpec – type expected by Spring. In such situations, the library creates a proxy to adjust Disjunction (or OtherTypeWhichExtendsSpecificationInterface) to FullNameSpec. The proxy is created in following way:

static <T> T wrapWithIfaceImplementation(final Class<T> expectedType, final Specification<Object> targetSpec) {
   return (T) java.lang.reflect.Proxy.newProxyInstance(
           targetSpec.getClass().getClassLoader(),
           new Class[]{ expectedType },
           (proxy, method, args) -> switch (method.getName()) {
               case "toPredicate" -> targetSpec.toPredicate(
                       (Root<Object>) args[0],
                       (CriteriaQuery<?>) args[1],
                       (CriteriaBuilder) args[2]
               );
               case "toString" -> expectedType.getSimpleName() + "[" + targetSpec.toString() + "]";
               case "equals" -> EnhancerUtil.equals(expectedType, targetSpec, args);
               case "hashCode" -> targetSpec.hashCode();
               default -> targetSpec.getClass().getMethod(method.getName(), method.getParameterTypes())
                       .invoke(targetSpec, args);
           });
}

(The full java code could be found on GitHub: EnhancerUtil.java

Dynamic proxies are not supported by GraalVM native-image out-of-the box. The reachability metadata configuration should be prepared for proxied classes. It is not possible to provide some kind of static reachability-metadata config with a “wildcard” configuration – a single configuration entry which will match with interfaces which could be declared in user application. We decided to provide a configuration class which scans classpath to find user-defined custom-interfaces with specification definitions and registers them.

The class which implements this logic is called SpecificationArgumentResolverProxyHintRegistrar.java. The simplified approach is presented in the snipped below:

public class SpecificationArgumentResolverProxyHintRegistrar implements RuntimeHintsRegistrar {
 
   @Override
   public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
       scanClassPathForCustomInterfaces()
               .forEach(annotatedInterface -> {
                   hints.proxies().registerJdkProxy(annotatedInterface.loadClass());
               });
   }
 
}

(The full java code can be found on GitHub: SpecificationArgumentResolverProxyHintRegistrar.java

In the listing above, we can see that the registrar scans classpath and registers any annotated interfaces that are used for proxy creation. This registrar must be imported in the Spring application in order to use it during reachability metadata generation.

Example of registrar import:

@Configuration
@ImportRuntimeHints({
     SpecificationArgumentResolverProxyHintRegistrar.class
})
public class UserApplicationConfig {
}
To scan classpath, specification argument resolver uses a library called ClassGraph which is “an uber-fast parallelized classpath scanner and module scanner for Java, Scala, Kotlin and other JVM languages.”

Fragment of generated proxy-config.json which is used during compilation to GraalVM native image, in which interface FullNameSpec is specified for proxy purposes:

[
…, //other config entries
{
 "interfaces": [
   "net.kaczmarzyk.example.FullNameSpec"
 ]
},
…, //other config entries
]

The final result

To add support for Specification Argument Resolver with native-image support to user application, the only thing that user should do is a adding two imports to the application:

@Configuration
@ImportRuntimeHints({
     SpecificationArgumentResolverHintRegistrar.class, //support for reflection
     SpecificationArgumentResolverProxyHintRegistrar.class //support for dynamic proxy
})
public class UserApplicationConfig {
}

And this is it! With the methods and tricks described above, the library is ready to be used in native-images.

Summary

In this article we presented challenges related with migrating a mature library to the newest Spring Boot version. We focused on the challenge of supporting native-images — this was not a trivial task, due to the heavy usage of Java Reflection.

This article shows another benefit of contributing to OSS. Contribution to open-source provides challenges that are different from ones that we typically face in our regular work. Implementing a library brings different problems and solutions than implementing an enterprise application that uses such a library. This is a refreshing and developing process. We can not wait until we have the opportunity to face challenges with supporting Spring Boot 4 and native-images 2 some day. In the meantime, please share your feedback and experience when using Spring Boot 3 and native-images — potentially with Specification Argument Resolver.

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *