Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .design/design-970.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Design: Delegation Hook

Addresses [requirements-970.md](requirements-970.md).

## Requirements

See `requirements-970.md`, R1 to R10.

## Technical Solution

### Overview

A new package `org.osgi.framework.hooks.delegation` (version 1.0) contains one `@ConsumerType` service interface, `DelegationHook`.
Bundles register it with the service registry.
The framework calls it from the bundle class loader as an additional last step of the Overall Search Order.

```
loadClass / getResource / getResources
|
Search Order (java.*, boot delegation, imports, required bundles,
class path, dynamic imports)
|
found? --yes--> return (getResources: continue to hooks and append)
|no
DelegationHook services, in ranking order
```

### API

```java
@ConsumerType
public interface DelegationHook {
Class<?> findClass(BundleWiring wiring, String name) throws ClassNotFoundException;
URL findResource(BundleWiring wiring, String name);
Collection<URL> findResources(BundleWiring wiring, String name);
}
```

- `wiring` is the wiring of the Asking Bundle, which gives the hook access to the bundle, its class loader, its capabilities and its wires.
- `findClass` returns the class or `null`.
The class may be defined by any class loader.
- `findResource` returns a URL or `null`.
- `findResources` returns the URLs to append, or `null` or an empty collection.

### When the framework calls the hook

- `findClass` and `findResource` are called only if the Search Order found nothing.
- `findResources` is called always, and the result is appended after the resources of the Search Order.
- No call is made for names in `java.*`.
- No call is made when the package of the name is imported, required, or exported by the Asking Bundle, because wiring is authoritative.
For a resource the package is derived from the path, like it is for dynamic imports.
- Hooks are called in service ranking order.
The first non-null result of `findClass` and `findResource` wins.
- A hook is not called again for the same wiring and name on the same thread while it is running.

### Failure and isolation

- The framework calls hooks without holding its own locks, in a privileged block.
- A hook that throws is ignored for this request and a `FrameworkEvent.ERROR` is published.
The framework should not call that registration again, like weaving hooks.

### Refresh dependency

If `findClass` returns a class whose class loader belongs to another bundle revision, the framework records the Asking Bundle as dependent on that provider.
A refresh of the provider then includes the Asking Bundle in the dependency closure, like a dynamic import does.
No wire is created.

### Deployment

The hook is an ordinary service.
A framework extension bundle may register it from its `ExtensionBundle-Activator`, so it is active before the first bundle is started.

### Security

Registering the service requires `ServicePermission[DelegationHook, REGISTER]`.
The framework is the only consumer of the service and needs no `GET` permission.
Defined classes remain subject to the permissions of the bundle that provided them.

## Alternatives

- **Hooks before the Search Order** (Equinox `preFindClass`).
Can hide what the wiring provides, so they break the module layer and are rejected.
- **Weaving**.
Does not help for resources, and requires every consumer to be woven.
- **Adding a wire** for classes obtained through the hook.
Gives visibility in the wiring API and a standard refresh behavior, but a wire needs a capability on the provider side, and the packages in question are by definition not exported.
This is an open point; the design uses a plain dependency.
- **Passing the caller or thread context class loader to the hook**.
Dropped, see Out of Scope in the requirements.

## Data Transfer Objects

None.

## Open Points

- Wire versus plain dependency for provider classes, see above.
- Whether the hook should be called for `findResources` if the package is exported by the Asking Bundle but the resource is not in it.
- The chapter number of the new specification, 58 is a placeholder.
130 changes: 130 additions & 0 deletions .design/requirements-970.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Requirements for a Delegation Hook in the OSGi Framework

- **Issue**: [osgi/osgi#970](https://github.com/osgi/osgi/issues/970)
- **Related**: [osgi/osgi#372](https://github.com/osgi/osgi/issues/372) (Java SPI support), Core Framework chapter 3.9 (class loading), Weaving Hook Service Specification
- **Prior work**: the internal `ClassLoaderHook` of Eclipse Equinox, used by its Service Loader Mediator implementation (`org.eclipse.equinox.spi`)

## Terminology

- **Search Order** -
The sequence of steps a bundle class loader follows to find a class or resource, see the Overall Search Order in the Core Framework specification.

- **Asking Bundle** -
The bundle whose class loader was asked for a class or resource.

- **Delegation Hook** -
A service registered by a bundle that is consulted by the framework to contribute a class or resource after the Search Order did not provide it.

- **Provider Bundle** -
A bundle whose class loader supplies a class to an Asking Bundle on behalf of a Delegation Hook.

## Problem Description

The Search Order is closed.
A bundle can see exactly what its wiring, its class path and its dynamic imports give it.
Extensions that need to make more visible to a bundle, without that bundle declaring anything, have no specified way to do so.

Every such extension today uses a framework specific mechanism:

- **Bytecode weaving** rewrites call sites of the bundles that use the extended feature.
It is fragile against new Java versions and language constructs.
- **Framework internals** such as the Equinox `ClassLoaderHook` are only reachable at framework construction time, and are not portable to other frameworks.
- **Launcher configuration** such as the Felix `felix.bootdelegation.classloaders` can only be applied by whoever launches the framework.

The Service Loader Mediator in Equinox shows that two interception points are enough for a rich feature: answer a resource request with resources of other bundles, and answer a class load miss with a class from another bundle.
Nothing in that feature is specific to `java.util.ServiceLoader`.
The same two points serve, for example, supplementer bundles, compatibility shims for renamed packages and resource overlays.

## Use Cases

### UC1: Java SPI Across Bundles

A consumer bundle calls `ServiceLoader.load(Greeter.class, loader)` with its own class loader.
A provider bundle ships `META-INF/services/org.example.Greeter` and the implementation class in a package it does not export.
An extension bundle makes the file visible to the consumer and loads the named class from the provider bundle.
Neither bundle carries OSGi specific metadata, and the extension does not weave any bundle.

### UC2: Extension Active Before Any Bundle Starts

The extension of UC1 is installed as a framework extension bundle.
It must be able to take part in class loading from the first class load of the first bundle, independent of the launcher.

### UC3: Compatibility Shim

A library renamed a package.
Bundles that still use the old name get the classes of the new package from a shim bundle, without the shim exporting the old package.

### UC4: Resource Overlay

A bundle asks for `META-INF/overlay/config.properties`.
The framework finds the bundle's own file.
An extension additionally contributes the files of other bundles, in a defined order.

### UC5: Wiring Stays Authoritative

A bundle imports `org.example.api`.
An extension must not be able to replace, hide or add to what the import provides.

### UC6: Provider Is Refreshed

The provider bundle of UC1 is updated and refreshed.
A class from the old provider revision must not stay in use by the consumer.

## Requirements

### R1: Additive Extension Point

The framework must provide a service based extension point that is consulted after the Search Order found nothing.
It must only be able to add classes and resources, never to replace or hide what the Search Order finds.

### R2: Classes, Resources and Resource Enumerations

The extension point must cover the three requests a bundle class loader answers: a single class, a single resource and the enumeration of all resources with a name.
For an enumeration the contributed resources must be appended to the resources the Search Order found.

### R3: Wiring Remains Authoritative

The extension point must not be consulted for `java.*` packages, nor for packages the Asking Bundle imports, requires or exports.

### R4: Multiple Hooks, Defined Order

Multiple hooks must be possible.
They must be consulted in service ranking order.
For a single class or resource the first result wins; for an enumeration all results are appended.

### R5: Refresh Dependency

If a class supplied by a hook was defined by the class loader of another bundle, the Asking Bundle must depend on that Provider Bundle for the purposes of refreshing and of the dependency closure, like it does for a dynamic import.

### R6: Robust Invocation

The framework must not hold its locks while it calls a hook.
A hook must not be called recursively for the same bundle wiring and name on the same thread.
A hook that throws must not break class loading: a framework error event must be published and the request continues as if the hook returned nothing.

### R7: Security

Registering a hook must require a permission.
Classes and resources supplied by a hook remain subject to the permissions of the Asking Bundle.

### R8: No Impact Without Hooks

Class and resource loading must be unchanged while no hook is registered.
The extension point must not add cost to successful lookups.

### R9: Available As Framework Extension

The extension point must be usable by a framework extension bundle that registers the service from its `ExtensionBundle-Activator`, as well as by an ordinary bundle.
No framework specific launcher configuration may be needed.

### R10: Technique Neutrality

The extension point must be defined by what the framework consults and what the hook may answer.
It must not prescribe how a hook finds its answer, and it must not require the framework to inspect call stacks or the thread context class loader.

## Out of Scope

- The observable behavior of Java SPI support in the framework (osgi/osgi#372) and a property to switch it off.
- Determining the calling bundle of a request that does not arrive through a bundle class loader, for example `ServiceLoader.load(Class)` with the thread context class loader.
The core specification does not define the thread context class loader, and a shared one cannot select a provider per caller.
- Hooks that run before the Search Order, as in the internal Equinox `pre` points.
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*******************************************************************************
* Copyright (c) Contributors to the Eclipse Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/

package org.osgi.framework.hooks.delegation;

import java.net.URL;
import java.util.Collection;

import org.osgi.annotation.versioning.ConsumerType;
import org.osgi.framework.wiring.BundleWiring;

/**
* OSGi Framework Delegation Hook Service.
*
* <p>
* Bundles registering this service are called by the framework as the last step
* of the search order of a bundle class loader, to contribute classes and
* resources the search order did not provide. A hook can only add; it cannot
* replace or hide what the wiring of the asking bundle provides.
* </p>
*
* <p>
* The framework does not call a hook for classes and resources in
* {@code java.*} packages, nor for packages the asking bundle imports,
* requires or exports.
* </p>
*
* <p>
* Hooks are called in service ranking order. The framework must not hold any
* locks while calling a hook, and must not call a hook for the same wiring and
* name while the hook is already running on the same thread. If a hook throws
* an exception, the framework publishes a
* {@link org.osgi.framework.FrameworkEvent#ERROR} and continues as if the hook
* had returned nothing.
* </p>
*
* <p>
* If a class returned by {@link #findClass(BundleWiring, String)} is defined by
* the class loader of another bundle, the asking bundle depends on that bundle
* for the purposes of refreshing, like it does for a dynamic import.
* </p>
*
* @ThreadSafe
* @since 1.0
*/
@ConsumerType
public interface DelegationHook {

/**
* Find a class the search order of the asking bundle did not find.
*
* <p>
* This method is only called if the search order found no class.
* </p>
*
* @param wiring The wiring of the asking bundle. Not {@code null}.
* @param name The binary name of the class. Not {@code null}.
* @return The class, or {@code null} if this hook does not provide it. The
* class may be defined by any class loader.
* @throws ClassNotFoundException If the hook knows the class but cannot load
* it.
*/
Class<?> findClass(BundleWiring wiring, String name) throws ClassNotFoundException;

/**
* Find a resource the search order of the asking bundle did not find.
*
* <p>
* This method is only called if the search order found no resource.
* </p>
*
* @param wiring The wiring of the asking bundle. Not {@code null}.
* @param name The resource name. Not {@code null}.
* @return The URL of the resource, or {@code null} if this hook does not
* provide it.
*/
URL findResource(BundleWiring wiring, String name);

/**
* Find resources to add to those the search order of the asking bundle
* found.
*
* <p>
* This method is called whether or not the search order found resources.
* The returned URLs are appended after those the search order found, and
* after those of hooks earlier in the call order.
* </p>
*
* @param wiring The wiring of the asking bundle. Not {@code null}.
* @param name The resource name. Not {@code null}.
* @return The URLs of the resources to add. May be {@code null} or empty.
*/
Collection<URL> findResources(BundleWiring wiring, String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*******************************************************************************
* Copyright (c) Contributors to the Eclipse Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/

/**
* Framework Delegation Hooks Package Version 1.0.
*
* <p>
* Bundles wishing to use this package must list the package in the
* Import-Package header of the bundle's manifest.
* </p>
*
* <p>
* Example import for consumers using the API in this package:
* </p>
* <p>
* {@code Import-Package: org.osgi.framework.hooks.delegation; version="[1.0,2.0)"}
* </p>
*/

@Version("1.0")
package org.osgi.framework.hooks.delegation;

import org.osgi.annotation.versioning.Version;
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<module>spec/org.osgi.spec.framework.bundlehooks</module>
<module>spec/org.osgi.spec.framework.servicehooks</module>
<module>spec/org.osgi.spec.framework.weavinghooks</module>
<module>spec/org.osgi.spec.framework.delegationhooks</module>
<module>spec/org.osgi.spec.framework.connect</module>
<module>spec/org.osgi.spec.framework.dto</module>
</modules>
Expand Down
Loading
Loading