Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
We're happy to announce: SAP Cloud SDK Version 3 is finally here!

Note: For a quick tutorial on how to migrate, please visit our step-by-step guide.

Note: In case your Java project uses the CAP stack, please alter your project structure to use SAP Cloud SDK 3 beforehand as per the related blog post.

Note: For a complete overview of our blog post series visit the SAP Cloud SDK Overview.


Introduction


As the new version of the SAP Cloud SDK for Java was announced many software projects may face the same migration scenario. This major update brings numerous improvements in code style, destination handling, test tools and much more. Some of the changes require a code adaptation on the consumer side of the API. That's why we highly recommend to read this document and follow the suggested steps whenever necessary.

So before starting to blindly resolve dependency issues, missing classes or type conflicts, please notice the changes done to...

  • Dependency Identifiers

  • VDM Usage


In addition we suggest to read up on the updated behavior with...

  • Resilience and Caching

  • Destination Handling

  • VAVR Usage

  • Accessors Handling

  • ThreadContext

  • Environment Variables

  • MockUtil


Changed dependency identifiers and class names


As part of our renaming from SAP S/4HANA Cloud SDK to SAP Cloud SDK we now also adjusted the group id of all our modules to no longer contain the s4hana section by default, but only when the module actually is S/4HANA specific. The artifact ids are now also unique, so that build tools like Gradle can easily import our modules just be using the artifact id. The concrete changes can be found in our release notes.

Migrating the Maven project setup for an existing application


When migrating your existing application to use version 3 of SAP Cloud SDK, please manually update the Maven groupId and artifactId of any referenced dependency entry in your pom.xml file. Please note, in a multi project setup you will find multiple pom.xml files.

While all of them need to be checked, we advice to start with the project root pom.xml. Find the dependencyManagemententry and replace the sdk-bom entry with the following:

<dependency>
<groupId>com.sap.cloud.sdk</groupId>
<artifactId>sdk-bom</artifactId>
<version>3.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>


When iterating over the other dependencies, please check for the moved Maven group ids:






































































Old GroupId New GroupId Context
com.sap.cloud.s4hana.archetypes com.sap.cloud.sdk.archetypes Maven archetypes
com.sap.cloud.s4hana.cloudplatform com.sap.cloud.sdk.cloudplatform SCP specific API
com.sap.cloud.s4hana.datamodel com.sap.cloud.sdk.datamodel API for virtual datamodel
com.sap.cloud.sdk.s4hana S/4HANA specific API
com.sap.cloud.s4hana.frameworks com.sap.cloud.sdk.frameworks Selection of frameworks
com.sap.cloud.s4hana.plugins com.sap.cloud.sdk.plugins Maven plugins
com.sap.cloud.s4hana.quality com.sap.cloud.sdk.quality Code quality ensuring classes
com.sap.cloud.s4hana.services com.sap.cloud.sdk.services Contributed serivce classes
com.sap.cloud.s4hana com.sap.cloud.sdk.s4hana S/4HANA specific API
com.sap.cloud.sdk.datamodel API for virtual datamodel
com.sap.cloud.sdk Commonly used library core classes
com.sap.cloud.sdk.testutil Unit and integration tests

As you can see, with the library re-branding to SAP Cloud SDK, we were able to extract the S/4HANA related classes into their dedicated artifacts. While doing so, we slightly changed the name of some artifactIds to make them nonambiguous. You can find the full list of changed names in the table of the release notes.

Some modules of the SAP Cloud SDK have been discontinued. They have been used mostly internally and thus it is highly unlikely your application has used any of them directly.

Migrating a new application


If you have not yet generated a Maven project from one of the SAP Cloud SDK archetypes, you don't have to change anything. Just make sure to follow the tutorials accordingly and keep in mind the new archetype ids:

  • com.sap.cloud.sdk.archetypes:scp-cf-spring

  • com.sap.cloud.sdk.archetypes:scp-cf-tomee

  • com.sap.cloud.sdk.archetypes:scp-neo-javaee7


Renamed type names


To better reflect the actual use of a class we also renamed all *Query classes into *Request as long as their main purpose was to request/read data from another system. This is especially relevant for the SoapQuery which is now called SoapRequestbut behaves completely the same.

The entity User has been replaced with the more generic entity Principal. It still fulfills the same functionality but now the updated name correctly reflects the meaning in the context of the application.

Using the VDM


The capabilities of the OData VDM provided by the SAP Cloud SDK 3 have not changed over the version. However, we introduce a completely new concept to handle destinations.

Quick overview


A quick overview of how we handled destinations prior to version 3:

  • A Destination was always identified by its name

  • To retrieve the actual information of a destination we used the DestinationAccessor with the provided name

  • Class hierarchy representing the different types of destinations (HTTP, RFC)


A call to the OData VDM looked like this:

new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.execute(new ErpConfigContext("some-destination-name"));


The downsides of this approach are:

  • With the tight coupling between the OData execution logic to the DestinationAccessor it was necessary to mock a whole lot of code to execute a call inside a unit test

  • Having the different types as part of a strict class hierarchy means that adding new types in the future (or on-demand by a consumer) was rather complex and in some cases not possible at all.


The new concept now represents all destinations via interfaces (DestinationPropertiesHttpDestinationPropertiesRfcDestinationProperties) that specify what properties a destinations exposes. Those properties are handled internally by an immutable Map, making the new destinations immutable themselves.

The example call above can now look like this

Destination destination = DestinationAccessor.getDestination("some-destination-name");
new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.execute(destination.asHttp());


This, for itself, doesn't look like an improvement at all. But consider the following example:

HttpDestination httpDestination = DefaultHttpDestination.builder("uri.to.your.system").build()
new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.execute(httpDestination);


This means you no longer need to mock any Destination for the DestinationAccessor just to execute a call via the OData VDM, e.g. in a test.

Removal of the ErpQueryEndpoint and ErpConfigContext


With the new destination concept we also removed the ErpConfigContext and the hidden default destination name ErpQueryEndpoint. This means your call like this

new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.execute(new ErpConfigContext());


will look this in the future (if you want to stay with the default destination name):

Destination destination = DestinationAccessor.getDestination("ErpQueryEndpoint").decorate(DefaultErpHttpDestination::new);
new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.execute(destination.asHttp());


This makes it clearer which destination actually gets loaded and consequently used by your OData VDM call.

The ErpConfigContext prior to version 3 handles the resolution of the sap-client and sap-locale properties of a Destination. This is now handled by the DefaultErpHttpDestination by decorating an already existing HttpDestination, as can be seen above. An alternative way to retrieve an ErpHttpDestination directly is provided by the ErpHttDestinationUtilsclass like this:

ErpHttpDestination erpHttpDestination = ErpHttpDestinationUtils.getErpHttpDestination("some-destination-name");


Under the hood this just wraps the returned Destination of the DestinationAccessor and wraps it as an ErpHttpDestination. This is basically a drop-in replacement of the old ErpConfigContext.

Resilience and Caching


In the past Hystrix has been used as a library to ensure latency and fault tolerance conditions for the application runtime. But since Hystrix is not longer in active development but in maintenance mode, a migration happened to the spiritual successor: Resilience4j. The consuming API has seen some drastic changes.

Lambda instead of Inheritance


When migrating to version 3, some adjustments need to be made for any application classes inheriting directly or indirectly from the class HystrixCommand (or CommandErpCommandCachingErpCommandCachingCommand). This Command model was used to implement child classes dedicated to a resilient operation. It has been discontinued in favor of function decoration.

Function decoration describes the practice of altering a provided lambda with the help of a utility class. This provides some advantages compared to the old inheritance model:

  • Modern API style, less boiler-plate code, fewer classes

  • Optional decoration options can customize the lambda modification

  • Interchangeable dependencies enables easy switching of involved frameworks


Example: A resilient call to an OData service endpoint.

  • Before:

    public class GetBusinessPartnerCommand extends ErpCommand<List<BusinessPartner>>
    {
    private final BusinessPartnerService businessPartnerService;

    public GetBusinessPartnerCommand( BusinessPartnerService businessPartnerService, ErpConfigContext configContext )
    {
    super(GetBusinessPartnerCommand.class, configContext);
    this.businessPartnerService = businessPartnerService;
    }

    @Override
    protected List<BusinessPartner> run()
    throws ODataException
    {
    return businessPartnerService.getAllBusinessPartner().top(10).execute(getConfigContext());
    }
    }

    ...

    final BusinessPartnerService service = new DefaultBusinessPartnerService();
    final ErpConfigContext configContext = new ErpConfigContext("MyDestination");

    final List<BusinessPartner> businessPartners = new GetBusinessPartnerCommand(service, configContext).execute();


    Characteristics:

    • Class inheritance is required, therefore also an implementation of run() and the super constructor invocation.

    • Requires a reference of ErpConfigContext.

    • No easy way to declare the operation's dependency to tenant or principal.

    • No easy way to customize resilience options.

    • (Required) external dependency to Hystrix.



  • After (with minimal application change):

    public class GetBusinessPartnerCommand
    {
    private final BusinessPartnerService businessPartnerService;
    private final Destination destination;

    public GetBusinessPartnerCommand( BusinessPartnerService businessPartnerService, Destination destination )
    {
    this.businessPartnerService = businessPartnerService;
    this.destination = destination;
    }

    public List<BusinessPartner> execute()
    {
    return ResilienceDecorator.executeSupplier(this::run, ResilienceConfiguration.of(GetBusinessPartnerCommand.class));
    }

    private List<BusinessPartner> run() {
    try {
    return businessPartnerService.getAllBusinessPartner().top(10).execute(destination.asHttp());
    }
    catch( ODataException e ) {
    throw new ResilienceRuntimeException(e);
    }
    }
    }

    ...

    final BusinessPartnerService service = new DefaultBusinessPartnerService();
    final Destination destination = DestinationAccessor.getDestination("MyDestination");

    final List<BusinessPartner> businessPartners = new GetBusinessPartnerCommand(service, destination).execute();


    Characteristics:

    • No inheritance, no implementation requirements.

    • ResilienceConfiguration requires a key, that can be either a String or a class reference.

    • ResilienceConfiguration is customizable, featuring many different settings.

    • Direct exception handling.

    • (Optional) external dependency to Resilience4j.



  • Best practice:

    final BusinessPartnerService service = new DefaultBusinessPartnerService();
    final Destination destination = DestinationAccessor.getDestination("MyDestination");

    final List<BusinessPartner> businessPartners = ResilienceDecorator.executeCallable(
    () -> service.getAllBusinessPartner().top(10).execute(destination.asHttp()),
    ResilienceConfiguration.of(BusinessPartnerService.class)
    );


    Characteristics:

    • No extra class definition required. Short inline code.

    • If exception handling needs to be adjusted, we recommend the Try construct from VAVR.




ResilienceDecorator


The ResilienceDecorator class allows the API consumer to wrap a lambda of type Supplier (without checked exceptions) or Callable (with checked exceptions). Besides wrapping, it can also execute the lambda right away, even with optional fallback strategy - in case something goes wrong.

With version 3 the following options are available to customize the instance of ResilienceConfiguration:

  • ResilienceIsolationMode

    • Enum: NO_ISOLATION, TENANT_REQUIRED, TENANT_OPTIONAL, PRINCIPAL_REQUIRED, PRINCIPAL_OPTIONAL, TENANT_AND_USER_REQUIRED, TENANT_AND_USER_OPTIONAL (default)



  • BulkheadConfiguration

    • MaxConcurrentCalls (int, default: 50)

    • MaxWaitDuration (Duration, default: 60s)



  • CacheConfiguration

    • ExpirationDuration (Duration, default: instant)

    • Parameters (Serializable[] or Object[], default: empty)



  • CircuitBreakerConfiguration

    • FailureRateThreshold (float, default: 50%)

    • WaitDuration (Duration, default: 10s)



  • TimeLimiterConfiguration

    • TimeoutDuration (Duration, default: 30s)

    • ShouldCancelRunningFuture (boolean, default: true)




Since the default resilience isolation mode is TENANT_AND_USER_OPTIONAL the resilience decorator decides at runtime, whether or not to include the tenant and principal data, depending on their existence in the current thread context. If the consumer wants to enforce principal or tenant separation, the isolation mode must be declared in the decorator options.

As a result of the migration, the asResilientCommand() methods were removed from all VDM fluent helpers. Now the API consumer is required to wrap their invocation of execute() from VDM fluent helpers using ResilienceDecorator instead.

Caching with JCache


When using the ResilienceDecorator, the caching feature is not enabled by default. It is activated once a CacheConfiguration is provided. Additional parameters can be passed along to customize the internal cache key. If these parameters are serializable, the cache key will be serializable as well.

With version 3, the internal framework to manage cache instances has changed from Guava Cache to the widely used JCache(JSR 107). Since this framework acts as Service Provider Interface, any adapter for JCache can be attached to your application. We highly recommend Caffeine to provide superior caching capabilities. The required dependency can be quickly added to the application pom.xml:

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>jcache</artifactId>
<version>2.7.0</version>
</dependency>


However if the consumer decides to use their own JCache provider, it's possible to exchange the respective dependency in the application Maven project setup. Consequently, the caching strategy only works, when a service provider for JCache is accessible at runtime.

Example: A cached resilient call to an exemplary service, which considers additional parameters for the cache key.

  • Before:

    public class AddressBookLookupCommand extends CachingErpCommand<Address> {
    private static final Cache<CacheKey, Address> cache = CacheBuilder.newBuilder().build();

    private final AddressBookService addressBookService;

    public AddressBookLookupCommand( AddressBookService addressBookService, ErpConfigContext configContext )
    {
    super(GetBusinessPartnerCommand.class, configContext);
    this.addressBookService = addressBookService;
    }

    @Override
    protected CacheKey getCacheKey() {
    return super.getCacheKey().append(lastName, firstName);
    }

    @Override
    protected Cache<CacheKey, Address> getCache() {
    return cache;
    }

    @Override
    protected final Address run() {
    return addressBookService.lookup(lastName, firstName);
    }
    }

    ...

    AddressBookService service;
    String lastName;
    String firstName;

    Address address = new AddressBookLookupCommand(service, lastName, firstName).execute();


    Characteristics:

    • Enabling caching requires CachingCommand or CachingErpCommand as super class.

    • Inheritance model requires overriding several methods.

    • Manually manage static cache instance.

    • Customization of CacheKey can easily be missed.



  • After:

    AddressBookService service;
    String lastName;
    String firstName;

    ResilienceConfiguration resilienceConfiguration = ResilienceConfiguration.of(AddressBookService.class)
    .cacheConfiguration(CacheConfiguration.of(Duration.ofDays(1)).withParameters(lastName, firstName)); // <--

    Address address = ResilienceDecorator.executeSupplier(
    () -> service.lookup(lastName, firstName),
    resilienceConfiguration
    );


    Characteristics:

    • Enabling caching only requires the attachment of CacheConfiguration to the instance of ResilienceConfiguration.

    • API requires the consumer to specify the additional cache key parameters. Alternatively there is withoutParameters() when no customization of cache key is required.




Resilience4j


With version 3, the actual resilience framework can be customized. The application developer can implement their own implementation of ResilienceDecorationStrategy. But by default the resilience implementation of Cloud SDK is used, which is based on Resilience4j. However the strategy can be changed on the static methods of ResilienceDecorator.

Destinations


The API for DestinationAccessor and the related DestinationLoader provide a powerful set of tools to leverage the integration process to external services.

Deeper look into the DestinationAccessor


Under the hood of the DestinationAccessor no longer a single DestinationFacade handles the retrieval of destinations, but a chain of so called DestinationLoader.

The default way the DestinationAccessor searches for Destinations is the same with version 3 as before:

  1. Search for a destination with the given name in the destinations environment variable.

  2. Look into any additional loader provided by the libraries you have on your class path (e.g. the Destination Service on SAP Cloud Foundry).


This is now represented in a more flexible way by using the DestinationLoaderChain. This now represents a chain of DestinationLoader implementations that are used one after another until a Destination is found. For an example, lets assume you want to retrieve your destinations first from the destinations environment variable, then from the Destination Service on SAP Cloud Foundry, and last from the Environment variable provided by the Extension Factory. Your custom loader chain would be instantiated the following way:

DestinationLoader customLoaderChain =
DestinationLoaderChain
.builder(new EnvVarDestinationLoader())
.append(new ScpCfDestinationLoader())
.append(new ScpXfDestinationLoader())
.build();
DestinationAccessor.setLoader(customLoaderChain);


How to use a different instances of RetrievalStrategy


In a provider/subscriber setup of your application you might have the possibility to retrieve a destination from either the provider or the subscriber. Before version 3 this looked like the following:

DestinationAccessor.setRetrievalStrategy("some-destination-name", DestinationRetrievalStrategy.ALWAYS_SUBSCRIBER);
Destination destination = DestinationAccessor.getDestination("some-destination-name");


The change in version 3 removed this from the DestinationAccessor and moved it to the actual DestinationLoaderimplementation. This makes it clearer that this is very platform specific knowledge.

DestinationOptions customOptions = 
DestinationOptions
.builder()
.augmentBuilder(
ScpCfDestinationOptionsAugmenter
.augmenter()
.retrievalStrategy(ScpCfDestinationRetrievalStrategy.ALWAYS_SUBSCRIBER))
.build();

Destination destination =
new ScpCfDestinationLoader()
.tryGetDestination("some-destination-name", customOptions)
.get();


This allows you to set the retrieval strategy on a per-call basis.

Usage of VAVR in the API


To follow the latest state-of-the-art paradigms with the SDK we decided to embrace the vavr library. This allows the consumer of the SDK to use different methods in a more functional way.

Try<Tenant> maybeTenant = TenantAccessor.tryGetCurrentTenant();
String tenantId = maybeTenant.onFailure(this::recoverFromException).map(Tenant::getTenantId).get();


This new dependency introduces, besides many other classes, the Try mentioned above containing either an Throwable or an actual value, as well as an Option. This Option is a more powerful replacement of the Java provided Optional class.

All of our APIs now use the vavr Option instead of the Java Optional, so that a migration on the consumer side would replace

import java.util.Optional;


with

import io.vavr.control.Option;


as well as any usage of Option with Optional in the corresponding places.

If you want to fall back to the Java provided Optional you can easily do so by calling the toJavaOptional method:

Option<BasicCredentials> vavrCredentials = httpDestination.getBasicCredentials();
Optional<BasicCredentials> javaCredentials = vavrCredentials.toJavaOptional();


For more information have a look into the great overview of the vavr library: https://www.vavr.io/vavr-docs/

Accessors


Simplified Facade Structure and New Accessors


In version 3, the following accessor classes and their corresponding facades have been restructured, simplifying their structure and leveraging functional concepts from io.vavr.control.Try.

This affects the following classes:

  • TenantAccessor

  • PrincipalAccessor

  • DestinationAccessor

  • HttpClientAccessor

  • CloudPlatformAccessor

  • SecretStoreAccessor

  • AuthTokenAccessor

  • LocaleAccessor

  • JndiLookupAccessor


As mentioned earlier, User has been migrated to Principal. Accordingly the original UserAccessor is no longer available but has been merged with the more generic PrincipalAccessor.

For example, before version 3, the TenantFacade looked like this:
public interface TenantFacade
{
Class<? extends Tenant> getTenantClass();

Optional<Tenant> resolveCurrentTenant()
throws TenantAccessException;

Tenant getCurrentTenant()
throws TenantNotAvailableException, TenantAccessException;

Optional<Tenant> getCurrentTenantIfAvailable()
throws TenantAccessException;

Try<Tenant> tryGetCurrentTenant();
}

Given that this class had five methods that needed to be implemented, providing a custom facade or mocking a facade in a test was rather cumbersome. In contrast, in version 3, the interface is now simplified and only consists of one method, wrapping possible error conditions in a Try.
public interface TenantFacade
{
Try<Tenant> tryGetCurrentTenant();
}

Accordingly, the TenantAccessor only relies on this one method to provide the following methods on its API:
public final class TenantAccessor {

private static Try<TenantFacade> tenantFacade = FacadeLocator.getFacade(TenantFacade.class);

public static TenantFacade getTenantFacade() {
...
}

public static Try<TenantFacade> tryGetTenantFacade() {
...
}

public static void setTenantFacade( @Nullable final TenantFacade tenantFacade ) {
...
}

public static Tenant getCurrentTenant() throws TenantAccessException {
return tryGetCurrentTenant().getOrElseThrow(failure -> {
if( failure instanceof TenantAccessException ) {
throw (TenantAccessException) failure;
} else {
throw new TenantAccessException("Failed to get current tenant.", failure);
}
});
}

public static Try<Tenant> tryGetCurrentTenant() {
final Try<Tenant> tenantTry = tenantFacade.flatMap(TenantFacade::tryGetCurrentTenant);
...
return tenantTry;
}

...
}

As you can see, on one hand, the FacadeLocator now returns a Try when looking up an implementation, allowing the SDK to more leniently handle errors and only yield them if an accessor is really used. On the other hand, you may notice that the getCurrentTenant method simply delegates to the tryGetCurrentTenant method now, simplifying the implementation efforts for a facade.

In addition to the existing accessors above, the following accessors have been added in version 3:

  • ThreadContextAccessor

  • RequestAccessor


New Methods for "on-behalf" Execution


In addition to these changes, the following accessors now offer additional methods:

  • TenantAccessor now offers:

    • T executeWithTenant( Tenant tenant, Callable<T> callable )

    • T executeWithFallbackTenant( Supplier<Tenant> fallbackTenant, Callable<T> callable )

    • void executeWithTenant( Tenant tenant, Executable executable )

    • void executeWithFallbackTenant( Supplier<Tenant> fallbackTenant, Callable<T> callable )

    • Supplier<Tenant> getFallbackTenant()

    • void setFallbackTenant(Supplier<Tenant> fallbackTenant)



  • PrincipalAccessor now offers:

    • T executeWithPrincipal( Principal principal, Callable<T> callable )

    • T executeWithFallbackPrincipal( Supplier<Principal> fallbackPrincipal, Callable<T> callable )

    • void executeWithPrincipal( Principal principal, Executable executable )

    • void executeWithFallbackPrincipal( Supplier<Principal> fallbackPrincipal, Callable<T> callable )

    • Supplier<Principal> getFallbackPrincipal()

    • void setFallbackPrincipal(Supplier<Principal> fallbackPrincipal)



  • RequestAccessor now offers:

    • T executeWithRequest( HttpServletRequest request, Callable<T> callable )

    • T executeWithFallbackRequest( Supplier<HttpServletRequest> fallbackRequest, Callable<T> callable )

    • void executeWithRequest( HttpServletRequest request, Executable executable )

    • void executeWithFallbackRequest( Supplier<HttpServletRequest> fallbackRequest, Callable<T> callable )

    • Supplier<HttpServletRequest> getFallbackRequest()

    • void setFallbackRequest(Supplier<HttpServletRequest> fallbackRequest)



  • AuthTokenAccessor now offers:

    • T executeWithAuthToken( AuthToken authToken, Callable<T> callable )

    • T executeWithFallbackAuthToken( Supplier<AuthToken> fallbackAuthToken, Callable<T> callable )

    • void executeWithAuthToken( AuthToken authToken, Executable executable )

    • void executeWithFallbackAuthToken( Supplier<AuthToken> fallbackAuthToken, Callable<T> callable )

    • Supplier<AuthToken> getFallbackToken()

    • void setFallbackToken(Supplier<AuthToken> fallbackToken)




These methods allow to run code on behalf of another tenant, principal, request, and authorization token, either overriding a possibly existing value, offering a fallback in case of a missing existing value, and providing a global fallback on the accessor in case no other value exists. This allows to adjust the behavior of these accessors as needed, for example, to simplify testing.

Please note that the previously existing JwtBasedRequestContextExecutor has been removed, being replaced with the respective executeWithAuthToken method of AuthTokenAccessor. You will also now find the new class AuthTokenBuilder, offering you convenient ways to construct an AuthToken instance to pass as an argument.

For example, in order to run code on behalf of another tenant, you can use the following code now:
TenantAccessor.executeWithTenant(new ScpCfTenant("some-tenant", "some-subdomain"), () -> {
// TenantAccessor.getCurrentTenant() will return the tenant "some-tenant".
});

In order to use an existing tenant and only fallback to another tenant if no other tenant is available, you can use:
TenantAccessor.executeWithFallbackTenant(new ScpCfTenant("fallback-tenant", "some-subdomain"), () -> {
// TenantAccessor.getCurrentTenant() will return the tenant that is already defined,
// or the tenant "fallback-tenant" if no tenant is otherwise available.
});

Finally, in order to define a global fallback, you can use:
// define a global fallback for TenantAccessor
TenantAccessor.setFallbackTenant(() -> new ScpCfTenant("global-fallback-tenant", "some-subdomain"));

// remove the global fallback
TenantAccessor.setFallbackTenant(null);

ThreadContext


Move from RequestContext to ThreadContext


Before version 3, the SAP Cloud SDK offered a so-called RequestContext for storing the current request across different threads. In order to better decouple this concept from the servlet standard, version 3 introduces ThreadContext to replace this concept. A ThreadContext no longer holds a reference to the current request, but instead only provides a generic map of properties to store across threads.

One major functional change of the ThreadContext is that it is now possible to nest such contexts transparently, whereas before you had received an exception that indicated that this was not possible without a specific call to withParentRequestContext in the RequestContextExecutor.

Similar to the RequestContextAccessor in previous versions, ThreadContextAccessor offers the tryGetCurrentContext and getCurrentContext methods to access the current ThreadContext, and RequestAccessor which allows to access the current request. Internally, the RequestAccessor uses a ThreadContextListener to store the current request as a property of the current ThreadContext.

In addition to this change, the ThreadContextListeners, which are used to manage the properties in a ThreadContext, are extended in version 3 of the SDK to allow not only to hook into the initialization and destruction of the ThreadContext, but also get notified with a beforeInitializeafterInitializebeforeDestroy, and afterDestroy methods.

Furthermore, version 3 introduces ThreadContextDecorator, which offers the possibility to also wrap the callable that is passed to the ThreadContextExecutor, which replaces the previously existing RequestContextExecutor, allowing to wrap a piece of code into a specific context. This is used, for example, for the SAP CP Neo environment, which requires to manage some thread-specific state when switching to threads outside of container-managed threads.

Running Code with a Specific ThreadContext


In order to wrap some piece of code into a specific context, you can use the ThreadContextExecutor, similar to the RequestContextExecutor before.

So, the following code:
new RequestContextExecutor().execute(() -> {
// compute some value
return someValue;
});

will now look like this:
new ThreadContextExecutor().execute(() -> {
// compute some value
return someValue;
});

As you can see, no specific ThreadContext is provided here. This means that the call will either inherit and use an existing parent ThreadContext, if one is present. Otherwise, a new ThreadContext will be created.

If you want to make sure that a specific ThreadContext is used, you can provided it as follows:
new ThreadContextExecutor().withThreadContext(customThreadContext).execute(() -> {
// compute some value
return someValue;
});

This will ignore any existing contexts and simply use the ThreadContext that is provided in withThreadContext.

Environment Variables


With version 3, the following global variables have been removed and are no longer considered at runtime:

  • USE_MOCKED_TENANT

  • USE_MOCKED_USER

  • ALLOW_MOCKED_AUTH_HEADER


This is the result of making the Cloud SDK more lenient towards versatile application scopes. The runtime no longer requires the context to provide correct values for tenant and principal (user). Instead tenant and principal are taken into account only when available. This is reflected in the default resilience isolation mode TENANT_AND_USER_OPTIONAL.

If the API consumer insists on the tenant or principal separation, they need to specify the respective isolation mode in the ResilienceConfiguration for each resilient call. For example, the application will throw an exception when TENANT_AND_USER_REQUIRED is chosen but one of them is not resolvable from the thread context during runtime.

Remove Unwanted Methods and Behavior from MockUtil


The test utility class MockUtil no longer mocks the platform-dependent instance of the respective interface. For instance, in version 2.x, mockTenant() would have mocked an instance of either ScpCfTenant or ScpNeoTenant, depending on the used environment. While the intention here was to simplify testing, we noticed that this actually complicated the usage and facades such as TenantFacade. Therefore, version 3 again only mocks an instance of the interface, in our example, Tenant.

In addition, version 3 removes all methods from MockUtil that relied on the implicit destination name "ErpQueryEndpoint", for example the method mockDestination() which has no additional parameters. Now, the corresponding remaining methods require that you specify a destination name. Within existing tests, you can therefore safely add the missing argument by specifying "ErpQueryEndpoint" as destination name, or adjust your test to use another name of your choice.
17 Comments