How does Spring Framework manage compatibility with different Java versions ?

on waitingforcode.com

How does Spring Framework manage compatibility with different Java versions ?

Java 9 comes quietly to Java ecosystem and Spring Framework already successfully tested the build on early release on the new Java version. If we add to this the backward compatibility between Java 8, 7 and 6, we could ask us the question: How does it succeed that ?

Through this article we'll try to answer to this question about triple compatibility. In the first part we'll discover the utility of org.springframework.lang package, containing among others, annotations used to mark classes as being destined only Java 7 or Java 8 ones. The second part will be devoted to the explain how Spring knows if given element can be used or not, depending on Java version.

UsesJavaX annotation

A little bit mysterious part of Spring Framework is hidden inside org.springframework.lang package where 3 annotations are currently placed (4.2.3 release): @UsesJava8, @UsesSunHttpServer. They all appeared in 4.1 version of the framework and are used to mark some classes or methods as "Java 7-only", "Java 8-only" or "com.sun server-only".

At first look, these annotations could be used in Spring context creation to constructs beans depending on runtime Java version. But it's not their goal. Spring Framework uses Animal Sniffer tool to check if code changes don't break the compatibility with Java 6. A breaking compatibility could be, for example, the use of new classes appeared in Java 8, such as Stream or Optional.

If we take a look at source code, we'll see there the dependencies on Java 8 features. But they are annotated with @UsesJava8. In the CI's side, Animal Sniffer is configured to ignore these classes during the analysis. And thanks to these annotations, build plan can be executed correctly.

How does Spring Framework detect Java 8 features ?

So, if @UsesJavaX annotations aren't used to detect if given feature should be used at runtime, how does Spring achieve that ? The answer is simpler that we can think. Spring simply checks if given Java 7 or Java 8 class can be loaded and, if it's the case, it considers that given bean can be loaded. It's made either with ClassUtils.isPresent() method or with ClassUtils.forName() one (in fact, isPresent() calls forName() under-the-hood). Before finish, we'll back to the past and see how Spring managed JDK version before 4.0 release.

We can distinguish two scenarios of conditional use:

  • bean static construction : is the case of org.springframework.core.convert.support.DefaultConversionService which loads some converters only for Java 8. The verification is made here through isPresent method and used further in the form of boolean flags. Let's take a look at code fragment of this class:

    /** Java 8's java.util.Optional class available? */
    private static final boolean javaUtilOptionalClassAvailable =
      ClassUtils.isPresent("java.util.Optional", DefaultConversionService.class.getClassLoader());
    
    /** Java 8's java.time package available? */
    private static final boolean jsr310Available =
      ClassUtils.isPresent("java.time.ZoneId", DefaultConversionService.class.getClassLoader());
    
    /** Java 8's java.util.stream.Stream class available? */
    private static final boolean streamAvailable = ClassUtils.isPresent(
      "java.util.stream.Stream", DefaultConversionService.class.getClassLoader());
    
    // ...
    
    if (javaUtilOptionalClassAvailable) {
      converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
    }
    
    // ...
    if (streamAvailable) {
      converterRegistry.addConverter(new StreamConverter(conversionService));
    }
    
  • dynamic user configuration : another use case comes from user configuration. Let's take the case of @Priority annotation, analyzed through org.springframework.core.annotation.OrderUtils with given construction:

    private static Class<? extends Annotation> priorityAnnotationType = null;
    
    static {
      try {
        priorityAnnotationType = (Class<? extends Annotation>)
          ClassUtils.forName("javax.annotation.Priority", OrderUtils.class.getClassLoader());
      }
      catch (Throwable ex) {
        // javax.annotation.Priority not available, or present but not loadable (on JDK 6)
      }
    }
    
    // ...
    public static Integer getPriority(Class<?> type) {
      if (priorityAnnotationType != null) {
        Annotation priority = AnnotationUtils.findAnnotation(type, priorityAnnotationType);
        if (priority != null) {
          return (Integer) AnnotationUtils.getValue(priority);
        }
      }
      return null;
    }
    

JDKVersion deprecated

All these changes of detection are caused also by the deprecation of of org.springframework.core.JdkVersion helper class since 4.2.1. As we can read in the comments, it's deprecated "in favor of direct checks for the desired JDK API variants via reflection". Previously, JdkVersion was used to manage Java version used by the Spring context. It was more declarative configuration because the user specified the JDK version in system parameters. But it's still used by some of Spring Boot dependencies (ConditionalOnJava in Spring Boot 1.2.7).

This class was used very often in 3.2.15 release and still a little bit used in the 4.0.0, for example here:

public class SqlRowSetResultSetExtractor implements ResultSetExtractor<SqlRowSet> {

  private static final CachedRowSetFactory cachedRowSetFactory;

  static {
    if (JdkVersion.getMajorJavaVersion() >= JdkVersion.JAVA_17) {
      // using JDBC 4.1 RowSetProvider
      cachedRowSetFactory = new StandardCachedRowSetFactory();
    }
    else {
      // JDBC 4.1 API not available - fall back to Sun CachedRowSetImpl
      cachedRowSetFactory = new SunCachedRowSetFactory();
    }
  }
// ...

In the 4.2.3 release, this code was replaced by "direct checks for the desired JDK API" we've already seen in the previous part:

public class SqlRowSetResultSetExtractor implements ResultSetExtractor<SqlRowSet> {

  private static final CachedRowSetFactory cachedRowSetFactory;

  static {
    if (ClassUtils.isPresent("javax.sql.rowset.RowSetProvider",
        SqlRowSetResultSetExtractor.class.getClassLoader())) {
      // using JDBC 4.1 RowSetProvider, available on JDK 7+
      cachedRowSetFactory = new StandardCachedRowSetFactory();
    }
    else {
      // JDBC 4.1 API not available - fall back to Sun CachedRowSetImpl on JDK 6
      cachedRowSetFactory = new SunCachedRowSetFactory();
    }
  }
// ...

As we can see, Spring Framework is designed to support 3 Java's versions: 6, 7 and 8. It achieves that by checking class existence in classpath. Thanks to that it can quite easily and inoffensively keep backward compatibility not only with the newest Java packages, but also with the newest language constructions, as lambdas. Everything is controlled with Animal Sniffer, a tool from MojoHaus Project destined to check if the code keeps backward compatibility. And it's not the end because Spring announced that the release 4.2 was correctly built on early Java 9 release..

Share on: