-
Notifications
You must be signed in to change notification settings - Fork 2k
Access helpers
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 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.
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.
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);
}
});
}
}
- Frequently Asked Questions
- Azure Identity Examples
- Configuration
- Performance Tuning
- Android Support
- Unit Testing
- Test Proxy Migration
- Azure Json Migration
- New Checkstyle and Spotbugs pattern migration
- Protocol Methods
- TypeSpec-Java Quickstart
- Getting Started Guidance
- Adding a Module
- Building
- Writing Performance Tests
- Working with AutoRest
- Deprecation
- BOM guidelines
- Release process
- Access helpers