Skip to content

Access helpers

vcolin7 edited this page Feb 14, 2024 · 6 revisions

The design principles of Azure SDK for Java outline guidelines that require the APIs in client libraries to be consistent, approachable and backward compatible. So, we should only have public APIs that are needed by the user to interact with Azure services successfully.

Access helpers

Access helpers are a way to provide access to private and package-private members of classes across package boundaries without needing to expose public APIs. This allows for circumventing the Java scoping system in scenarios where functionality should be internal to the package, but access needs to happen across package boundaries.

For example, azure-core is lowercasing HttpHeader names contained in HttpHeaders to remove headers from logging, but HttpHeaders has a backing Map<String, HttpHeader> where the key value of the Map is known to be lowercased already. This functionality is internal to azure-core, therefore HttpHeaders shouldn't add an API to expose the backing Map, instead it creates an access helper to expose the backing map without adding a public API.

Design

Access helpers are defined in implementation packages and should be defined in the package implementation.accesshelpers, so all access helpers can be found in one place. You should have a one-to-one mapping of public class to access helper class so that the access helper is only associated with one class, this will keep the access helper simpler.

There will be two types defined, the utility access helper class and the accessor interface defining functionality, where the accessor interface is an inner type within the access helper utility class. The access helper utility class should be named <Class being access helped>AccessHelper, in the azure-core example it would be HttpHeadersAccessHelper. And the accessor interface should be named <Class being access helped>Accessor, again in the azure-core example it would be HttpHeadersAccessor.

The AccessHelper class will have a private static field for the Accessor and have one public static method per interface method defined on the Accessor plus a public static method to set the static Accessor field. The Accessor will define one or more methods to access internal members of the class being access helped, and these can be anything from constructor calls, getters and setters, and field access.

The class being access helped will add a static constructor that sets the static Accessor in the AccessHelper.

Note: If you have constructor calls, it is possible for these to be made before the class is loaded and therefore before the class calls its static constructor to set the accessor. To remedy this, if the class has a public constructor create an instance with dummy values to load the class, otherwise you'll need to load the class with the class loader.

Example

Continuing with the HttpHeaders example.

package com.azure.core.implementation.accesshelpers;

public final class HttpHeadersAccessHelper {
  private static HttpHeadersAccessor accessor; // Non-final static field so it can be set by the static constructor in HttpHeaders.
  
  // Interface needs to be public so HttpHeaders can define an implementation from a different package.
  public interface HttpHeadersAccessor {
    // All getter and setter type methods need to have a parameter of the type being access helped to scope it to that object.
    Map<String, HttpHeader> getBackingMap(HttpHeaders httpHeaders);
    
    // Constructors don't need to take the type being access helped as there is no scoped type.
    HttpHeaders internalCreate(Map<String, HttpHeader> internalMap);
  }

  public static void setAccessor(HttpHeadersAccessor accessor) {
    HttpHeadersAccessHelper.accessor = accessor; // This is thread safe as the static constructor should only be called once on class load.
  }

  public static Map<String, HttpHeader> getBackingMap(HttpHeaders httpHeaders) {
    return accessor.getBackingMap(httpHeaders); // simple call to the accessor that was set.
  }

  public static HttpHeaders internalCreate(Map<String, HttpHeader> internalMap) {
    if (accessor == null) {
      new HttpHeaders(); // Dummy create to make sure the class is loaded as constructor calls could happen before class loading.

      // HttpHeaders has a public constructor, so the above is preferred as it doesn't require any reflection. If HttpHeaders didn't have a public
      // constructor the following should be used.
      try {
        // We need to use the class loader for the access helper to make sure we're scoped to the right class loader.
        Class.forName(HttpHeaders.class.getName(), true, HttpHeadersAccessHelper.class.getClassLoader());
      } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
      }
    }

    assert accessor != null; // not really needed but Spotbugs will error if this isn't present as it can't correlate accessor will never be null here.
    return accessor.internalCreate(internalMap);
  }

  private HttpHeadersAccessHelper() {
    // Access helper classes shouldn't have an accessible constructor.
  }
}

In HttpHeaders a static constructor like this should be added:

public class HttpHeaders {
  static {
    HttpHeadersAccessHelper.setAccessor(new HttpHeadersAccessHelper.HttpHeadersAccessor() {
      @Override
      public Map<String, HttpHeader> getBackingMap(HttpHeaders httpHeaders) {
        return httpHeaders.backingMap;
      }
      
      @Override
      public HttpHeaders internalCreate(Map<String, HttpHeader> internalMap) {
        return new HttpHeaders(internalMap);
      }
    });
  }
}
Clone this wiki locally