You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by jb...@apache.org on 2020/05/03 04:56:38 UTC

[karaf] branch master updated: interceptor support on top on OSGi services

This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/master by this push:
     new a9a60e7  interceptor support on top on OSGi services
     new d983b75  Merge pull request #993 from rmannibucau/interceptor-poc
a9a60e7 is described below

commit a9a60e789d34939e3b707752267ac3b915ed41a1
Author: Romain Manni-Bucau <rm...@apache.org>
AuthorDate: Mon Nov 11 19:27:23 2019 +0100

    interceptor support on top on OSGi services
---
 .../src/main/asciidoc/user-guide/interceptor.adoc  | 112 ++++
 pom.xml                                            |   1 +
 scr/state/pom.xml                                  |   2 +-
 services/interceptor/api/pom.xml                   |  58 ++
 .../service/interceptor/api/AroundInvoke.java      |  40 ++
 .../interceptor/api/EnableInterceptors.java        |  34 ++
 .../karaf/service/interceptor/api/Interceptor.java |  33 +
 .../interceptor/api/InterceptorBinding.java        |  28 +
 .../service/interceptor/api/InvocationContext.java |  34 ++
 .../interceptor/api/RequireInterceptorImpl.java    |  35 ++
 services/interceptor/impl/pom.xml                  | 231 +++++++
 .../impl/activator/InterceptorActivator.java       |  96 +++
 .../impl/runtime/ComponentProperties.java          |  22 +
 .../interceptor/impl/runtime/Exceptions.java       |  36 ++
 .../impl/runtime/PropertiesManager.java            |  38 ++
 .../interceptor/impl/runtime/ProxiesManager.java   | 133 +++++
 .../runtime/hook/InterceptedInstancesHooks.java    |  63 ++
 .../impl/runtime/hook/InterceptorInstance.java     |  89 +++
 .../invoker/InterceptorInvocationContext.java      | 106 ++++
 .../impl/runtime/invoker/package-info.java         |  19 +
 .../impl/runtime/proxy/AsmProxyFactory.java        | 663 +++++++++++++++++++++
 .../impl/runtime/proxy/ProxyFactory.java           | 222 +++++++
 .../registry/InterceptedServiceRegistry.java       | 142 +++++
 .../impl/runtime/registry/InterceptorRegistry.java |  97 +++
 .../karaf/service/interceptor/impl/E2ETest.java    | 118 ++++
 .../impl/runtime/proxy/AsmProxyFactoryTest.java    |  72 +++
 .../interceptor/impl/test/InterceptedService.java  |  34 ++
 .../service/interceptor/impl/test/Suffix.java      |  32 +
 .../impl/test/SuffixingInterceptor.java            |  32 +
 .../karaf/service/interceptor/impl/test/Wrap.java  |  31 +
 .../interceptor/impl/test/WrappingInterceptor.java |  32 +
 ...ce.interceptor.impl.test.InterceptedService.xml |  25 +
 ....interceptor.impl.test.SuffixingInterceptor.xml |  25 +
 ...e.interceptor.impl.test.WrappingInterceptor.xml |  25 +
 services/interceptor/pom.xml                       |  40 ++
 services/pom.xml                                   |   1 +
 36 files changed, 2800 insertions(+), 1 deletion(-)

diff --git a/manual/src/main/asciidoc/user-guide/interceptor.adoc b/manual/src/main/asciidoc/user-guide/interceptor.adoc
new file mode 100644
index 0000000..8291de7
--- /dev/null
+++ b/manual/src/main/asciidoc/user-guide/interceptor.adoc
@@ -0,0 +1,112 @@
+//
+// 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.
+//
+
+= Interceptor
+
+Interceptor module is inspired from JavaEE/JakartaEE interceptor API but adapted to OSGi services.
+
+It enables to proxy any service and execute code around the service methods.
+
+== Dependencies
+
+[source,xml]
+----
+<dependency>
+  <groupId>org.apache.karaf.services</groupId>
+  <artifactId>org.apache.karaf.services.interceptor.api</artifactId>
+</dependency>
+----
+
+== Defining an interceptor
+
+An interceptor is simply an OSGi service marked with `@Interceptor` and having an interceptor binding which is nothing more than an annotation marked with `@InterceptorBinding`.
+
+Here is a binding:
+
+[source,java]
+----
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@InterceptorBinding
+public @interface Suffix {
+}
+----
+
+And here is an associated interceptor:
+
+[source,java]
+----
+@Suffix
+@Interceptor
+@Component(service = SuffixingInterceptor.class)
+public class SuffixingInterceptor {
+    // ...
+}
+----
+
+TIP: the examples are using SCR but there is no requirement to do so, you can do it with `context.registerService()` as well.
+
+For the interceptor to do something, you must define an `@AroundInvoke` method which will intercept method calls in intercepted services.
+It must takes a single parameter of type `InvocationContext`:
+
+[source,java]
+----
+@AroundInvoke
+public Object around(final InvocationContext context) throws Exception {
+    return context.proceed() + "(suffixed)";
+}
+----
+
+== Using interceptors
+
+Assuming you have an interceptor library (it is commong for transversal concerns like security, auditing, tracing, metrics, etc...), you can enable the interceptor usages with these few steps:
+
+. Ensure you register your service as an OSGi service,
+. Mark the service with `@EnableInterceptors`,
+. Mark the class or method with the interceptor bindings you want
+
+TIP: if you put a binding on a class is it available for all methods and is called after method level interceptors.
+
+As an example speaks better than 1000 words, here is a service using our previous suffixing interceptor:
+
+[source,java]
+----
+@EnableInterceptors
+@Component(service = InterceptedService.class)
+public class InterceptedService {
+    @Suffix
+    public String doStuff(final String value) {
+        return "'" + value + "'";
+    }
+}
+----
+
+You can notice that it is equivalent to the following example which just moved the interceptor at class level:
+
+
+[source,java]
+----
+@Suffix
+@EnableInterceptors
+@Component(service = InterceptedService.class)
+public class InterceptedService {
+    public String doStuff(final String value) {
+        return "'" + value + "'";
+    }
+}
+----
+
+== Proxying implementation
+
+If possible, the proxying will use `java.lang.reflect.Proxy` but if there is a class to proxy and not only interfaces, `asm` must be available for the proxy to suceed to be created.
diff --git a/pom.xml b/pom.xml
index 0e52375..076b155 100644
--- a/pom.xml
+++ b/pom.xml
@@ -321,6 +321,7 @@
         <websocket.version>1.1</websocket.version>
         <winsw.version>2.3.0</winsw.version>
 
+        <osgi-component-annotations.version>1.4.0</osgi-component-annotations.version>
 
         <surefire.argLine />
 
diff --git a/scr/state/pom.xml b/scr/state/pom.xml
index 265aef0..cf4348d 100644
--- a/scr/state/pom.xml
+++ b/scr/state/pom.xml
@@ -62,7 +62,7 @@
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.service.component.annotations</artifactId>
-            <version>1.3.0</version>
+            <version>${osgi-component-annotations.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.felix</groupId>
diff --git a/services/interceptor/api/pom.xml b/services/interceptor/api/pom.xml
new file mode 100644
index 0000000..e8e6dda
--- /dev/null
+++ b/services/interceptor/api/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <!--
+
+      Licensed to the Apache Software Foundation (ASF) under one or more
+      contributor license agreements.  See the NOTICE file distributed with
+      this work for additional information regarding copyright ownership.
+      The ASF licenses this file to You 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.
+  -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf.services</groupId>
+        <artifactId>org.apache.karaf.services.interceptor</artifactId>
+        <version>4.3.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.karaf.services.interceptor.api</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Karaf :: Services :: Interceptor :: API</name>
+    <description>Interceptor API.</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <version>${osgi-component-annotations.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                  <instructions>
+                    <Export-Package>org.apache.karaf.service.interceptor.api</Export-Package>
+                  </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/AroundInvoke.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/AroundInvoke.java
new file mode 100644
index 0000000..a2e7306
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/AroundInvoke.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marks an interceptor public method as the intercepting one.
+ *
+ * It must be:
+ * <ul>
+ *     <li>public</li>
+ *     <li>have a single {@link InvocationContext} parameter</li>
+ * </ul>
+ *
+ * It can optionally throw {@link Exception}.
+ */
+@Target(METHOD)
+@Retention(RUNTIME) // no support of methods in SCR so we use plain reflection
+@RequireInterceptorImpl
+public @interface AroundInvoke {
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/EnableInterceptors.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/EnableInterceptors.java
new file mode 100644
index 0000000..c07b09e
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/EnableInterceptors.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.osgi.service.component.annotations.ComponentPropertyType;
+
+// note: would be better to make it a stereotype but bnd does not support it yet so let's make it a wrapper
+@Target(TYPE)
+@Retention(CLASS)
+@ComponentPropertyType
+@RequireInterceptorImpl
+public @interface EnableInterceptors {
+    String PREFIX_ = "apache.karaf."; // we don't want just "interceptor" as property key
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/Interceptor.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/Interceptor.java
new file mode 100644
index 0000000..c3c5993
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/Interceptor.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.osgi.service.component.annotations.ComponentPropertyType;
+
+@Target(TYPE)
+@Retention(CLASS)
+@ComponentPropertyType
+@RequireInterceptorImpl
+public @interface Interceptor {
+    String PREFIX_ = "apache.karaf."; // we don't want just "interceptor" as property key
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InterceptorBinding.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InterceptorBinding.java
new file mode 100644
index 0000000..d084451
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InterceptorBinding.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target(ANNOTATION_TYPE)
+public @interface InterceptorBinding {
+}
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InvocationContext.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InvocationContext.java
new file mode 100644
index 0000000..a157726
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/InvocationContext.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.api;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+public interface InvocationContext {
+    Object getTarget();
+
+    Method getMethod();
+
+    Object[] getParameters();
+
+    void setParameters(Object[] var1);
+
+    Map<String, Object> getContextData();
+
+    Object proceed() throws Exception;
+}
\ No newline at end of file
diff --git a/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/RequireInterceptorImpl.java b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/RequireInterceptorImpl.java
new file mode 100644
index 0000000..25b4ef1
--- /dev/null
+++ b/services/interceptor/api/src/main/java/org/apache/karaf/service/interceptor/api/RequireInterceptorImpl.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.api;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(CLASS)
+@Target(TYPE)
+/* when on r7
+@Requirement(
+    namespace = "osgi.implementation",
+    name = "org.apache.karaf.service.interceptor.impl",
+    version = "${project.version}" // java-template plugin?
+)
+*/
+@interface RequireInterceptorImpl {
+}
\ No newline at end of file
diff --git a/services/interceptor/impl/pom.xml b/services/interceptor/impl/pom.xml
new file mode 100644
index 0000000..66915b9
--- /dev/null
+++ b/services/interceptor/impl/pom.xml
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <!--
+
+      Licensed to the Apache Software Foundation (ASF) under one or more
+      contributor license agreements.  See the NOTICE file distributed with
+      this work for additional information regarding copyright ownership.
+      The ASF licenses this file to You 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.
+  -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf.services</groupId>
+        <artifactId>org.apache.karaf.services.interceptor</artifactId>
+        <version>4.3.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.karaf.services.interceptor.impl</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Karaf :: Services :: Interceptor :: Implementation</name>
+    <description>Interceptor implementation.</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>org.apache.karaf.services.interceptor.api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ow2.asm</groupId>
+            <artifactId>asm</artifactId>
+            <version>${asm.version}</version>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <version>${osgi-component-annotations.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+            <version>${pax.exam.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.felix</groupId>
+                    <artifactId>org.apache.felix.configadmin</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-karaf</artifactId>
+            <version>${pax.exam.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.felix</groupId>
+                    <artifactId>org.apache.felix.configadmin</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf</groupId>
+            <artifactId>apache-karaf</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+            <type>tar.gz</type>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.geronimo.specs</groupId>
+            <artifactId>geronimo-atinject_1.0_spec</artifactId>
+            <version>1.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.specs</groupId>
+            <artifactId>org.apache.karaf.specs.locator</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>java9-plus</id>
+            <activation>
+                <jdk>[9,)</jdk>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>jakarta.xml.bind</groupId>
+                    <artifactId>jakarta.xml.bind-api</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>org.glassfish.jaxb</groupId>
+                    <artifactId>jaxb-runtime</artifactId>
+                </dependency>
+            </dependencies>
+        </profile>
+    </profiles>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <version>3.1.1</version>
+                <executions>
+                    <execution>
+                        <id>copy</id>
+                        <phase>generate-test-resources</phase>
+                        <goals>
+                            <goal>copy</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <artifactItems>
+                        <artifactItem>
+                            <groupId>org.ow2.asm</groupId>
+                            <artifactId>asm</artifactId>
+                            <version>${asm.version}</version>
+                            <type>jar</type>
+                            <overWrite>true</overWrite>
+                            <outputDirectory>${project.build.directory}/libs</outputDirectory>
+                            <destFileName>asm.jar</destFileName>
+                        </artifactItem>
+                        <artifactItem>
+                            <groupId>org.apache.karaf</groupId>
+                            <artifactId>apache-karaf</artifactId>
+                            <version>${project.version}</version>
+                            <type>tar.gz</type>
+                            <overWrite>true</overWrite>
+                            <outputDirectory>${project.build.directory}/libs</outputDirectory>
+                            <destFileName>karaf.tar.gz</destFileName>
+                        </artifactItem>
+                    </artifactItems>
+                    <overWriteReleases>false</overWriteReleases>
+                    <overWriteSnapshots>true</overWriteSnapshots>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Bundle-Activator>org.apache.karaf.service.interceptor.impl.activator.InterceptorActivator</Bundle-Activator>
+                        <Export-Package>org.apache.karaf.service.interceptor.impl.activator</Export-Package>
+                        <Private-Package>
+                          org.apache.karaf.service.interceptor.impl.runtime,
+                          org.apache.karaf.service.interceptor.impl.runtime.hook,
+                          org.apache.karaf.service.interceptor.impl.runtime.invoker,
+                          org.apache.karaf.service.interceptor.impl.runtime.proxy,
+                          org.apache.karaf.service.interceptor.impl.runtime.registry
+                        </Private-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>default-test</id>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <configuration>
+                            <excludes>
+                                <exclude>**/E2E*</exclude>
+                            </excludes>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>e2e</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <configuration>
+                            <includes>
+                                <include>**/E2E*</include>
+                            </includes>
+                        </configuration>
+                    </execution>
+                </executions>
+                <configuration>
+                    <trimStackTrace>false</trimStackTrace>
+                    <systemPropertyVariables>
+                        <java.util.logging.SimpleFormatter.format>%1$tF %1$tT [%4$s] [%2$-89s] %5$s%6$s%n</java.util.logging.SimpleFormatter.format>
+                        <karaf.version>${project.version}</karaf.version>
+                    </systemPropertyVariables>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/activator/InterceptorActivator.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/activator/InterceptorActivator.java
new file mode 100644
index 0000000..8fd01ba
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/activator/InterceptorActivator.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.activator;
+
+import static java.util.Optional.ofNullable;
+import static org.apache.karaf.service.interceptor.impl.runtime.ComponentProperties.INTERCEPTORS_PROPERTY;
+import static org.apache.karaf.service.interceptor.impl.runtime.ComponentProperties.INTERCEPTOR_PROPERTY;
+
+import java.util.Hashtable;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.apache.karaf.service.interceptor.impl.runtime.ProxiesManager;
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptedInstancesHooks;
+import org.apache.karaf.service.interceptor.impl.runtime.proxy.ProxyFactory;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptedServiceRegistry;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptorRegistry;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+
+public class InterceptorActivator implements BundleActivator {
+    private InterceptorRegistry interceptorRegistry;
+    private InterceptedServiceRegistry interceptedServiceRegistry;
+    private ProxiesManager proxiesManager;
+
+    private ServiceRegistration<?> hooksRegistration;
+
+    @Override
+    public void start(final BundleContext context) throws InvalidSyntaxException {
+        final PropertiesManager propertiesManager = new PropertiesManager();
+        // todo: decouple these three services with a bus? here we use the activator to keep it simple
+        interceptedServiceRegistry = new InterceptedServiceRegistry(this::onServiceAddition, this::onServiceRemoval, propertiesManager);
+        interceptorRegistry = new InterceptorRegistry(this::onInterceptorAddition, this::onInterceptorRemoval, propertiesManager);
+        proxiesManager = new ProxiesManager(interceptorRegistry, interceptedServiceRegistry, new ProxyFactory(), propertiesManager);
+
+        // listen for interceptors and intercepted instances to be able to react on (un)registrations
+        context.addServiceListener(interceptedServiceRegistry, "(" + INTERCEPTORS_PROPERTY + "=true)");
+        context.addServiceListener(interceptorRegistry, "(" + INTERCEPTOR_PROPERTY + "=true)");
+
+        // register existing services/interceptors
+        ofNullable(context.getAllServiceReferences(null, "(" + INTERCEPTORS_PROPERTY + "=true)"))
+                .ifPresent(refs -> Stream.of(refs).forEach(ref -> interceptedServiceRegistry.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, ref))));
+        ofNullable(context.getAllServiceReferences(null, "(" + INTERCEPTOR_PROPERTY + "=true)"))
+                .ifPresent(refs -> Stream.of(refs).forEach(ref -> interceptorRegistry.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, ref))));
+
+        // ensure we filter out the proxied services to only return proxies
+        hooksRegistration = context.registerService(
+                new String[]{FindHook.class.getName(), EventListenerHook.class.getName()},
+                new InterceptedInstancesHooks(context.getBundle().getBundleId()),
+                new Hashtable<>());
+    }
+
+    @Override
+    public void stop(final BundleContext context) {
+        context.removeServiceListener(interceptorRegistry);
+        context.removeServiceListener(interceptedServiceRegistry);
+        hooksRegistration.unregister();
+        proxiesManager.stop();
+    }
+
+    private void onServiceAddition(final ServiceReference<?> ref) {
+        proxiesManager.onInterceptedInstanceAddition(ref);
+    }
+
+    private void onServiceRemoval(final ServiceReference<?> ref) {
+        proxiesManager.onInterceptedInstanceRemoval(ref);
+    }
+
+    private void onInterceptorAddition(final Class<?> aClass) {
+        proxiesManager.onInterceptorAddition(aClass);
+    }
+
+    private void onInterceptorRemoval(final Class<?> aClass) {
+        proxiesManager.onInterceptorRemoval(aClass);
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ComponentProperties.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ComponentProperties.java
new file mode 100644
index 0000000..ff842be
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ComponentProperties.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime;
+
+public interface ComponentProperties {
+    String INTERCEPTORS_PROPERTY = "apache.karaf.enable.interceptors";
+    String INTERCEPTOR_PROPERTY = "apache.karaf.interceptor";
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/Exceptions.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/Exceptions.java
new file mode 100644
index 0000000..37fa376
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/Exceptions.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime;
+
+import java.lang.reflect.InvocationTargetException;
+
+public final class Exceptions {
+    private Exceptions() {
+        // no-op
+    }
+
+    public static Object unwrap(final InvocationTargetException ite) throws Exception {
+        final Throwable targetException = ite.getTargetException();
+        if (Exception.class.isInstance(targetException)) {
+            throw Exception.class.cast(targetException);
+        }
+        if (Error.class.isInstance(targetException)) {
+            throw Error.class.cast(targetException);
+        }
+        throw ite; // quite unlikely
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/PropertiesManager.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/PropertiesManager.java
new file mode 100644
index 0000000..f610cec
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/PropertiesManager.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime;
+
+import java.util.Hashtable;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import org.osgi.framework.ServiceReference;
+
+public class PropertiesManager {
+    public Stream<String> unflattenStringValues(final Object it) {
+        return String[].class.isInstance(it) ? Stream.of(String[].class.cast(it)) : Stream.of(String.class.cast(it));
+    }
+
+    public <T> Hashtable<String, Object> collectProperties(final ServiceReference<T> ref) {
+        return Stream.of(ref.getPropertyKeys())
+                .filter(it -> !ComponentProperties.INTERCEPTORS_PROPERTY.equals(it))
+                .collect(Collector.of(Hashtable::new, (h, p) -> h.put(p, ref.getProperty(p)), (p1, p2) -> {
+                    p1.putAll(p2);
+                    return p1;
+                }));
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ProxiesManager.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ProxiesManager.java
new file mode 100644
index 0000000..efa5e46
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/ProxiesManager.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime;
+
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.toList;
+
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.impl.runtime.proxy.ProxyFactory;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptedServiceRegistry;
+import org.apache.karaf.service.interceptor.impl.runtime.registry.InterceptorRegistry;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+public class ProxiesManager {
+    private final ProxyFactory proxyFactory;
+    private final PropertiesManager propertiesManager;
+    private final InterceptorRegistry interceptors;
+    private final InterceptedServiceRegistry services;
+
+    private final Map<ServiceReference<?>, ServiceRegistration<?>> registrationPerReference = new ConcurrentHashMap<>();
+    private final Map<ServiceReference<?>, List<Class<?>>> bindingPerReference = new ConcurrentHashMap<>();
+    private final Map<Class<?>, Collection<ServiceReference<?>>> referencesPerBinding = new ConcurrentHashMap<>();
+
+    public ProxiesManager(final InterceptorRegistry interceptorRegistry,
+                          final InterceptedServiceRegistry services,
+                          final ProxyFactory proxyFactory,
+                          final PropertiesManager propertiesManager) {
+        this.interceptors = interceptorRegistry;
+        this.services = services;
+        this.proxyFactory = proxyFactory;
+        this.propertiesManager = propertiesManager;
+    }
+
+    // check out all services not yet proxied which can now be proxied and register the proxy
+    public void onInterceptorAddition(final Class<?> bindingClass) {
+        ofNullable(referencesPerBinding.get(bindingClass))
+                .ifPresent(references -> references.stream()
+                        .filter(ref -> !registrationPerReference.containsKey(ref)) // already proxied so skip
+                        .filter(ref -> ofNullable(bindingPerReference.get(ref))
+                                .map(b -> interceptors.areBindingsAvailable(b.stream()))
+                                .orElse(false))
+                        .forEach(ref -> registrationPerReference.put(ref, registerProxy(ref))));
+    }
+
+    // remove registered proxies since one of the interceptor is no more available
+    public void onInterceptorRemoval(final Class<?> bindingClass) {
+        ofNullable(referencesPerBinding.get(bindingClass))
+                .ifPresent(references -> references.stream()
+                        .filter(registrationPerReference::containsKey)
+                        .forEach(ref -> ofNullable(registrationPerReference.remove(ref))
+                                .ifPresent(ServiceRegistration::unregister)));
+    }
+
+    public <T> void onInterceptedInstanceAddition(final ServiceReference<T> ref) {
+        final List<Class<?>> bindings = toBindings(ref).collect(toList());
+        bindings.forEach(binding -> referencesPerBinding.computeIfAbsent(binding, k -> new CopyOnWriteArraySet<>()).add(ref));
+        bindingPerReference.put(ref, bindings);
+        if (interceptors.areBindingsAvailable(bindings.stream())) {
+            registrationPerReference.put(ref, registerProxy(ref));
+        }
+    }
+
+    public <T> void onInterceptedInstanceRemoval(final ServiceReference<T> ref) {
+        toBindings(ref).filter(referencesPerBinding::containsKey).forEach(binding -> {
+            final Collection<ServiceReference<?>> refs = referencesPerBinding.get(binding);
+            refs.remove(ref);
+            if (refs.isEmpty()) {
+                referencesPerBinding.remove(binding);
+            }
+        });
+        bindingPerReference.remove(ref);
+        ofNullable(registrationPerReference.remove(ref))
+                .ifPresent(ServiceRegistration::unregister);
+    }
+
+    private <T> Stream<? extends Class<?>> toBindings(final ServiceReference<T> ref) {
+        return services.getBindings(ref);
+    }
+
+    private <T> ServiceRegistration<?> registerProxy(final ServiceReference<T> ref) {
+        final BundleContext context = ref.getBundle().getBundleContext();
+        final Object classProperty = ref.getProperty(Constants.OBJECTCLASS);
+        final List<Class<?>> classes = Stream.of(classProperty)
+                .flatMap(propertiesManager::unflattenStringValues)
+                .map(it -> {
+                    try {
+                        return context.getBundle().loadClass(it);
+                    } catch (final ClassNotFoundException e) {
+                        throw new IllegalStateException(e);
+                    }
+                })
+                .collect(toList());
+
+        // drop interceptors property to let it be forwarded
+        final Hashtable<String, Object> properties = propertiesManager.collectProperties(ref);
+        final T proxy = proxyFactory.create(
+                ref, classes,
+                interceptors.getInterceptors(bindingPerReference.get(ref)),
+                services.getInterceptorsPerMethod(ref));
+        return context.registerService(classes.stream().map(Class::getName).toArray(String[]::new), proxy, properties);
+    }
+
+    public void stop() {
+        registrationPerReference.values().forEach(ServiceRegistration::unregister);
+        bindingPerReference.clear();
+        referencesPerBinding.clear();
+        registrationPerReference.clear();
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptedInstancesHooks.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptedInstancesHooks.java
new file mode 100644
index 0000000..55d4a57
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptedInstancesHooks.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.hook;
+
+import static org.apache.karaf.service.interceptor.impl.runtime.ComponentProperties.INTERCEPTORS_PROPERTY;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+import org.osgi.framework.hooks.service.ListenerHook;
+
+public class InterceptedInstancesHooks implements FindHook, EventListenerHook {
+    private final long bundleId;
+
+    public InterceptedInstancesHooks(final long bundleId) {
+        this.bundleId = bundleId;
+    }
+
+    // replaced services are not forward to listeners except the bundle owning the replacer and #0 (optional for the test)
+    @Override
+    public void event(final ServiceEvent event, final Map<BundleContext, Collection<ListenerHook.ListenerInfo>> listeners) {
+        if (isIntercepted(event.getServiceReference())) {
+            listeners.keySet().removeIf(this::isNeitherFrameworkNorSelf);
+        }
+    }
+
+    // remove replaced services to keep only replacements
+    @Override
+    public void find(final BundleContext context, final String name, final String filter,
+                     final boolean allServices, final Collection<ServiceReference<?>> references) {
+        if (isNeitherFrameworkNorSelf(context)) {
+            references.removeIf(this::isIntercepted);
+        }
+    }
+
+    private boolean isNeitherFrameworkNorSelf(final BundleContext b) {
+        final long id = b.getBundle().getBundleId();
+        return id != 0 && id != bundleId;
+    }
+
+    private boolean isIntercepted(final ServiceReference<?> serviceReference) {
+        return Boolean.parseBoolean(String.valueOf(serviceReference.getProperty(INTERCEPTORS_PROPERTY)));
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptorInstance.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptorInstance.java
new file mode 100644
index 0000000..204b3e2
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/hook/InterceptorInstance.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.hook;
+
+import static java.util.stream.Collectors.toList;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.api.AroundInvoke;
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.apache.karaf.service.interceptor.impl.runtime.Exceptions;
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class InterceptorInstance<T> {
+    private final ServiceReference<T> reference;
+    private final BundleContext context;
+    private final Method method;
+    private final Class<?> binding;
+
+    public InterceptorInstance(final ServiceReference<T> reference, final Class<?> binding, final PropertiesManager propertiesManager) {
+        this.reference = reference;
+        this.context = reference.getBundle().getBundleContext();
+        this.method = propertiesManager.unflattenStringValues(reference.getProperty(Constants.OBJECTCLASS))
+            .map(this::findAroundInvoke)
+            .filter(Objects::nonNull)
+            .findFirst()
+            .orElse(null);
+        this.binding = binding;
+    }
+
+    public Class<?> getBinding() {
+        return binding;
+    }
+
+    public Object intercept(final InvocationContext invocationContext) throws Exception {
+        final T service = context.getService(reference);
+        if (service == null) {
+            throw new IllegalStateException("'" + reference + "' no more available");
+        }
+        try {
+            return method == null ? invocationContext.proceed() : method.invoke(service, invocationContext);
+        } catch (final InvocationTargetException ite) {
+            return Exceptions.unwrap(ite);
+        } finally {
+            context.ungetService(reference);
+        }
+    }
+
+    private Method findAroundInvoke(final String clazz) {
+        try {
+            final List<Method> interceptingMethods = Stream.of(context.getBundle().loadClass(clazz))
+                    .flatMap(c -> Stream.of(c.getMethods()))
+                    .filter(m -> m.isAnnotationPresent(AroundInvoke.class))
+                    .collect(toList());
+            switch (interceptingMethods.size()) {
+                case 0: // we can add @AroundConstruct later so let's already tolerate that
+                    return null;
+                case 1:
+                    return interceptingMethods.iterator().next();
+                default:
+                    throw new IllegalArgumentException("'" + clazz + "' must have a single @AroundInvoke method, found " + interceptingMethods);
+            }
+        } catch (final ClassNotFoundException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+}
+
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/InterceptorInvocationContext.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/InterceptorInvocationContext.java
new file mode 100644
index 0000000..ffa4acf
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/InterceptorInvocationContext.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.invoker;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.apache.karaf.service.interceptor.impl.runtime.Exceptions;
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptorInstance;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+public class InterceptorInvocationContext<T> implements InvocationContext {
+    private final ServiceReference<T> interceptedReference;
+    private final Method method;
+    private final List<InterceptorInstance<?>> interceptors;
+
+    private T target;
+    private Map<String, Object> contextData;
+    private Object[] parameters;
+    private int index;
+
+    public InterceptorInvocationContext(final ServiceReference<T> reference,
+                                        final List<InterceptorInstance<?>> interceptors,
+                                        final Method method, final Object[] parameters) {
+        this.interceptedReference = reference;
+        this.method = method;
+        this.parameters = parameters;
+        this.interceptors = interceptors;
+    }
+
+    @Override
+    public Object proceed() throws Exception {
+        try {
+            if (index < interceptors.size()) {
+                final InterceptorInstance<?> interceptor = interceptors.get(index++);
+                try {
+                    return interceptor.intercept(this);
+                } catch (final Exception e) {
+                    index--;
+                    throw e;
+                }
+            }
+            try {
+                return getMethod().invoke(getTarget(), getParameters());
+            } catch (final InvocationTargetException ite) {
+                return Exceptions.unwrap(ite);
+            }
+        } finally {
+            if (target != null) { // todo: check scope and optimize it?
+                interceptedReference.getBundle().getBundleContext().ungetService(interceptedReference);
+            }
+        }
+    }
+
+    @Override
+    public T getTarget() {
+        final BundleContext context = interceptedReference.getBundle().getBundleContext();
+        target = context.getService(interceptedReference);
+        if (target == null) {
+            throw new IllegalStateException("service no more available (" + interceptedReference + ")");
+        }
+        return target;
+    }
+
+    @Override
+    public Method getMethod() {
+        return method;
+    }
+
+    @Override
+    public Object[] getParameters() {
+        return parameters;
+    }
+
+    @Override
+    public void setParameters(final Object[] parameters) {
+        this.parameters = parameters;
+    }
+
+    @Override
+    public Map<String, Object> getContextData() {
+        if (contextData == null) {
+            contextData = new HashMap<>();
+        }
+        return contextData;
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/package-info.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/package-info.java
new file mode 100644
index 0000000..96d06a1
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/invoker/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// this is mainly inspired from OWB
+package org.apache.karaf.service.interceptor.impl.runtime.invoker;
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactory.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactory.java
new file mode 100644
index 0000000..ff07fd6
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactory.java
@@ -0,0 +1,663 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.proxy;
+
+import static org.objectweb.asm.ClassReader.SKIP_CODE;
+import static org.objectweb.asm.ClassReader.SKIP_DEBUG;
+import static org.objectweb.asm.ClassReader.SKIP_FRAMES;
+import static org.objectweb.asm.Opcodes.AALOAD;
+import static org.objectweb.asm.Opcodes.AASTORE;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ACC_VARARGS;
+import static org.objectweb.asm.Opcodes.ACONST_NULL;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ANEWARRAY;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.ASM7;
+import static org.objectweb.asm.Opcodes.ASTORE;
+import static org.objectweb.asm.Opcodes.ATHROW;
+import static org.objectweb.asm.Opcodes.BIPUSH;
+import static org.objectweb.asm.Opcodes.CHECKCAST;
+import static org.objectweb.asm.Opcodes.DLOAD;
+import static org.objectweb.asm.Opcodes.DRETURN;
+import static org.objectweb.asm.Opcodes.DUP;
+import static org.objectweb.asm.Opcodes.FLOAD;
+import static org.objectweb.asm.Opcodes.FRETURN;
+import static org.objectweb.asm.Opcodes.GETFIELD;
+import static org.objectweb.asm.Opcodes.GETSTATIC;
+import static org.objectweb.asm.Opcodes.ICONST_0;
+import static org.objectweb.asm.Opcodes.ICONST_1;
+import static org.objectweb.asm.Opcodes.ICONST_2;
+import static org.objectweb.asm.Opcodes.ICONST_3;
+import static org.objectweb.asm.Opcodes.ICONST_4;
+import static org.objectweb.asm.Opcodes.ICONST_5;
+import static org.objectweb.asm.Opcodes.IFEQ;
+import static org.objectweb.asm.Opcodes.ILOAD;
+import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+import static org.objectweb.asm.Opcodes.IRETURN;
+import static org.objectweb.asm.Opcodes.LLOAD;
+import static org.objectweb.asm.Opcodes.LRETURN;
+import static org.objectweb.asm.Opcodes.NEW;
+import static org.objectweb.asm.Opcodes.POP;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.SIPUSH;
+import static org.objectweb.asm.Opcodes.V10;
+import static org.objectweb.asm.Opcodes.V11;
+import static org.objectweb.asm.Opcodes.V12;
+import static org.objectweb.asm.Opcodes.V13;
+import static org.objectweb.asm.Opcodes.V1_8;
+import static org.objectweb.asm.Opcodes.V9;
+
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+// forked from OWB
+public class AsmProxyFactory {
+    private static final Method[] EMPTY_METHODS = new Method[0];
+
+    private static final String FIELD_INTERCEPTOR_HANDLER = "karafInterceptorProxyHandler";
+    private static final String FIELD_INTERCEPTED_METHODS = "karafInterceptorProxyMethods";
+
+    public <T> T create(final Class<?> clazz, final InterceptorHandler handler) {
+        try {
+            final T proxy = (T) clazz.getConstructor().newInstance();
+            final Field invocationHandlerField = clazz.getDeclaredField(FIELD_INTERCEPTOR_HANDLER);
+            invocationHandlerField.setAccessible(true);
+            invocationHandlerField.set(proxy, handler);
+            return proxy;
+        } catch (final IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InstantiationException e) {
+            throw new IllegalStateException(e);
+        } catch (final InvocationTargetException ite) {
+            throw new IllegalStateException(ite.getTargetException());
+        }
+    }
+
+    public <T> Class<T> createProxyClass(final ProxyFactory.ProxyClassLoader classLoader,
+                                         final String proxyClassName, final Class<?>[] classesToProxy,
+                                         final Method[] interceptedMethods) {
+        try {
+            return (Class<T>) Class.forName(proxyClassName, true, classLoader);
+        } catch (final ClassNotFoundException cnfe) {
+            return doCreateProxyClass(classLoader, proxyClassName, classesToProxy, interceptedMethods);
+        }
+    }
+
+    private <T> Class<T> doCreateProxyClass(final ProxyFactory.ProxyClassLoader classLoader, final String proxyClassName,
+                                            final Class<?>[] classesToProxy, final Method[] interceptedMethods) {
+        final String proxyClassFileName = proxyClassName.replace('.', '/');
+        final byte[] proxyBytes = generateProxy(classesToProxy, proxyClassFileName, sortOutDuplicateMethods(interceptedMethods));
+        final Class<T> proxyCLass = classLoader.getOrRegister(proxyClassName, proxyBytes, classesToProxy[0].getPackage(), classesToProxy[0].getProtectionDomain());
+        try {
+            final Field interceptedMethodsField = proxyCLass.getDeclaredField(FIELD_INTERCEPTED_METHODS);
+            interceptedMethodsField.setAccessible(true);
+            interceptedMethodsField.set(null, interceptedMethods);
+        } catch (final Exception e) {
+            throw new IllegalStateException(e);
+        }
+        return proxyCLass;
+    }
+
+    private Method[] sortOutDuplicateMethods(final Method[] methods) {
+        if (methods == null || methods.length == 0) {
+            return null;
+        }
+
+        final List<Method> duplicates = new ArrayList<>();
+        for (final Method outer : methods) {
+            for (final Method inner : methods) {
+                if (inner != outer
+                        && hasSameSignature(outer, inner)
+                        && !(duplicates.contains(outer) || duplicates.contains(inner))) {
+                    duplicates.add(inner);
+                }
+            }
+        }
+
+        final List<Method> outsorted = new ArrayList<>(Arrays.asList(methods));
+        outsorted.removeAll(duplicates);
+        return outsorted.toArray(EMPTY_METHODS);
+    }
+
+    private boolean hasSameSignature(Method a, Method b) {
+        return a.getName().equals(b.getName())
+                && a.getReturnType().equals(b.getReturnType())
+                && Arrays.equals(a.getParameterTypes(), b.getParameterTypes());
+    }
+
+    private void createConstructor(final ClassWriter cw, final String proxyClassFileName, final Class<?> classToProxy,
+                                   final String classFileName) {
+        Constructor superDefaultCt;
+        String parentClassFileName = classFileName;
+        String descriptor = "()V";
+
+        try {
+            if (classToProxy.isInterface()) {
+                parentClassFileName = Type.getInternalName(Object.class);
+                superDefaultCt = Object.class.getConstructor(null);
+                descriptor = Type.getConstructorDescriptor(superDefaultCt);
+            }
+        } catch (final NoSuchMethodException nsme) {
+            // no worries
+        }
+
+        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", descriptor, null, null);
+        mv.visitCode();
+        mv.visitVarInsn(ALOAD, 0);
+        mv.visitMethodInsn(INVOKESPECIAL, parentClassFileName, "<init>", descriptor, false);
+
+        mv.visitVarInsn(ALOAD, 0);
+        mv.visitInsn(ACONST_NULL);
+        mv.visitFieldInsn(PUTFIELD, proxyClassFileName, FIELD_INTERCEPTOR_HANDLER, Type.getDescriptor(InterceptorHandler.class));
+
+        mv.visitInsn(RETURN);
+        mv.visitMaxs(-1, -1);
+        mv.visitEnd();
+    }
+
+    private byte[] generateProxy(final Class<?>[] classesToProxy, final String proxyClassFileName, final Method[] interceptedMethods) {
+        final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+        final String classFileName = classesToProxy[0].getName().replace('.', '/');
+
+        final String[] interfaces = Stream.of(classesToProxy)
+                .filter(Class::isInterface)
+                .map(Type::getInternalName)
+                .toArray(String[]::new);
+        final String superClassName;
+        if (interfaces.length == classesToProxy.length) {
+            superClassName = Type.getInternalName(Object.class);
+        } else {
+            superClassName = Type.getInternalName(classesToProxy[0]);
+        }
+
+        cw.visit(findJavaVersion(classesToProxy[0]), ACC_PUBLIC + ACC_SUPER + ACC_SYNTHETIC, proxyClassFileName, null, superClassName, interfaces);
+        cw.visitSource(classFileName + ".java", null);
+        createInstanceVariables(cw);
+        createConstructor(cw, proxyClassFileName, classesToProxy[0], classFileName);
+        if (interceptedMethods != null) {
+            delegateInterceptedMethods(cw, proxyClassFileName, classesToProxy[0], interceptedMethods);
+        }
+        return cw.toByteArray();
+    }
+
+    private void createInstanceVariables(final ClassWriter cw) {
+        cw.visitField(ACC_PRIVATE,
+                FIELD_INTERCEPTOR_HANDLER, Type.getDescriptor(InterceptorHandler.class), null, null).visitEnd();
+        cw.visitField(ACC_PRIVATE | ACC_STATIC,
+                FIELD_INTERCEPTED_METHODS, Type.getDescriptor(Method[].class), null, null).visitEnd();
+    }
+
+    private void delegateInterceptedMethods(final ClassWriter cw,
+                                            final String proxyClassFileName, final Class<?> classToProxy,
+                                            final Method[] interceptedMethods) {
+        for (int i = 0; i < interceptedMethods.length; i++) {
+            if (!unproxyableMethod(interceptedMethods[i])) {
+                generateInterceptorHandledMethod(cw, interceptedMethods[i], i, classToProxy, proxyClassFileName);
+            }
+        }
+    }
+
+    private void generateInterceptorHandledMethod(final ClassWriter cw, final Method method, final int methodIndex,
+                                                  final Class<?> classToProxy, final String proxyClassFileName) {
+        if ("<init>".equals(method.getName())) {
+            return;
+        }
+
+        final Class<?> returnType = method.getReturnType();
+        final Class<?>[] parameterTypes = method.getParameterTypes();
+        final Class<?>[] exceptionTypes = method.getExceptionTypes();
+        final int modifiers = method.getModifiers();
+        if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {
+            throw new IllegalStateException("It's not possible to proxy a final or static method: " + classToProxy.getName() + " " + method.getName());
+        }
+
+        // push the method definition
+        final int modifier = modifiers & (ACC_PUBLIC | ACC_PROTECTED | ACC_VARARGS);
+
+        final MethodVisitor mv = cw.visitMethod(modifier, method.getName(), Type.getMethodDescriptor(method), null, null);
+        mv.visitCode();
+        // push try/catch block, to catch declared exceptions, and to catch java.lang.Throwable
+        final Label l0 = new Label();
+        final Label l1 = new Label();
+        final Label l2 = new Label();
+
+        if (exceptionTypes.length > 0) {
+            mv.visitTryCatchBlock(l0, l1, l2, "java/lang/reflect/InvocationTargetException");
+        }
+
+        // push try code
+        mv.visitLabel(l0);
+        final String classNameToOverride = method.getDeclaringClass().getName().replace('.', '/');
+        mv.visitLdcInsn(Type.getType("L" + classNameToOverride + ";"));
+
+        // the following code generates the bytecode for this line of Java:
+        // Method method = <proxy>.class.getMethod("add", new Class[] { <array of function argument classes> });
+
+        // get the method name to invoke, and push to stack
+        mv.visitLdcInsn(method.getName());
+
+        // create the Class[]
+        createArrayDefinition(mv, parameterTypes.length, Class.class);
+
+        int length = 1;
+
+        // push parameters into array
+        for (int i = 0; i < parameterTypes.length; i++) {
+            // keep copy of array on stack
+            mv.visitInsn(DUP);
+
+            final Class<?> parameterType = parameterTypes[i];
+
+            // push number onto stack
+            pushIntOntoStack(mv, i);
+
+            if (parameterType.isPrimitive()) {
+                String wrapperType = getWrapperType(parameterType);
+                mv.visitFieldInsn(GETSTATIC, wrapperType, "TYPE", "Ljava/lang/Class;");
+            } else {
+                mv.visitLdcInsn(Type.getType(parameterType));
+            }
+
+            mv.visitInsn(AASTORE);
+
+            if (Long.TYPE.equals(parameterType) || Double.TYPE.equals(parameterType)) {
+                length += 2;
+            } else {
+                length++;
+            }
+        }
+
+        // the following code generates bytecode equivalent to:
+        // return ((<returntype>) invocationHandler.invoke(this, {methodIndex}, new Object[] { <function arguments }))[.<primitive>Value()];
+
+        final Label l4 = new Label();
+        mv.visitLabel(l4);
+        mv.visitVarInsn(ALOAD, 0);
+        mv.visitFieldInsn(GETFIELD, proxyClassFileName, FIELD_INTERCEPTOR_HANDLER, Type.getDescriptor(InterceptorHandler.class));
+        mv.visitFieldInsn(GETSTATIC, proxyClassFileName, FIELD_INTERCEPTED_METHODS, Type.getDescriptor(Method[].class));
+        if (methodIndex < 128) {
+            mv.visitIntInsn(BIPUSH, methodIndex);
+        } else if (methodIndex < 32267) {
+            mv.visitIntInsn(SIPUSH, methodIndex);
+        } else {
+            throw new IllegalStateException("Sorry, we only support Classes with 2^15 methods...");
+        }
+
+        mv.visitInsn(AALOAD);
+        pushMethodParameterArray(mv, parameterTypes);
+        mv.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(InterceptorHandler.class), "invoke",
+                "(Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;", true);
+        mv.visitTypeInsn(CHECKCAST, getCastType(returnType));
+        if (returnType.isPrimitive() && (!Void.TYPE.equals(returnType))) {
+            // get the primitive value
+            mv.visitMethodInsn(INVOKEVIRTUAL, getWrapperType(returnType), getPrimitiveMethod(returnType),
+                    "()" + Type.getDescriptor(returnType), false);
+        }
+
+        mv.visitLabel(l1);
+        if (!Void.TYPE.equals(returnType)) {
+            mv.visitInsn(getReturnInsn(returnType));
+        } else {
+            mv.visitInsn(POP);
+            mv.visitInsn(RETURN);
+        }
+
+        // catch InvocationTargetException
+        if (exceptionTypes.length > 0) {
+            mv.visitLabel(l2);
+            mv.visitVarInsn(ASTORE, length);
+
+            Label l5 = new Label();
+            mv.visitLabel(l5);
+
+            for (int i = 0; i < exceptionTypes.length; i++) {
+                Class<?> exceptionType = exceptionTypes[i];
+
+                mv.visitLdcInsn(Type.getType("L" + exceptionType.getCanonicalName().replace('.', '/') + ";"));
+                mv.visitVarInsn(ALOAD, length);
+                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/InvocationTargetException", "getCause",
+                        "()Ljava/lang/Throwable;", false);
+                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z", false);
+
+                Label l6 = new Label();
+                mv.visitJumpInsn(IFEQ, l6);
+
+                Label l7 = new Label();
+                mv.visitLabel(l7);
+
+                mv.visitVarInsn(ALOAD, length);
+                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/InvocationTargetException", "getCause",
+                        "()Ljava/lang/Throwable;", false);
+                mv.visitTypeInsn(CHECKCAST, getCastType(exceptionType));
+                mv.visitInsn(ATHROW);
+                mv.visitLabel(l6);
+
+                if (i == (exceptionTypes.length - 1)) {
+                    mv.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException");
+                    mv.visitInsn(DUP);
+                    mv.visitVarInsn(ALOAD, length);
+                    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException", "<init>",
+                            "(Ljava/lang/Throwable;)V", false);
+                    mv.visitInsn(ATHROW);
+                }
+            }
+        }
+
+        mv.visitMaxs(0, 0);
+        mv.visitEnd();
+    }
+
+    private int findJavaVersion(final Class<?> from) {
+        final String resource = from.getName().replace('.', '/') + ".class";
+        try (final InputStream stream = from.getClassLoader().getResourceAsStream(resource)) {
+            if (stream == null) {
+                return V1_8;
+            }
+            final ClassReader reader = new ClassReader(stream);
+            final VersionVisitor visitor = new VersionVisitor();
+            reader.accept(visitor, SKIP_DEBUG + SKIP_CODE + SKIP_FRAMES);
+            if (visitor.version != 0) {
+                return visitor.version;
+            }
+        } catch (final Exception e) {
+            // no-op
+        }
+        // mainly for JVM classes - outside the classloader, find to fallback on the JVM version
+        final String javaVersionProp = System.getProperty("java.version", "1.8");
+        if (javaVersionProp.startsWith("1.8")) {
+            return V1_8;
+        } else if (javaVersionProp.startsWith("9") || javaVersionProp.startsWith("1.9")) {
+            return V9;
+        } else if (javaVersionProp.startsWith("10")) {
+            return V10;
+        } else if (javaVersionProp.startsWith("11")) {
+            return V11;
+        } else if (javaVersionProp.startsWith("12")) {
+            return V12;
+        } else if (javaVersionProp.startsWith("13")) {
+            return V13;
+        }
+        try {
+            final int i = Integer.parseInt(javaVersionProp);
+            if (i > 13) {
+                return V13 + (i - 13);
+            }
+            return V1_8;
+        } catch (final NumberFormatException nfe) {
+            return V1_8;
+        }
+    }
+
+    private boolean unproxyableMethod(final Method delegatedMethod) {
+        final int modifiers = delegatedMethod.getModifiers();
+        return (modifiers & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL | Modifier.NATIVE)) > 0 ||
+                "finalize".equals(delegatedMethod.getName()) || delegatedMethod.isBridge();
+    }
+
+    /**
+     * @return the wrapper type for a primitive, e.g. java.lang.Integer for int
+     */
+    private String getWrapperType(Class<?> type) {
+        if (Integer.TYPE.equals(type)) {
+            return Integer.class.getCanonicalName().replace('.', '/');
+        } else if (Boolean.TYPE.equals(type)) {
+            return Boolean.class.getCanonicalName().replace('.', '/');
+        } else if (Character.TYPE.equals(type)) {
+            return Character.class.getCanonicalName().replace('.', '/');
+        } else if (Byte.TYPE.equals(type)) {
+            return Byte.class.getCanonicalName().replace('.', '/');
+        } else if (Short.TYPE.equals(type)) {
+            return Short.class.getCanonicalName().replace('.', '/');
+        } else if (Float.TYPE.equals(type)) {
+            return Float.class.getCanonicalName().replace('.', '/');
+        } else if (Long.TYPE.equals(type)) {
+            return Long.class.getCanonicalName().replace('.', '/');
+        } else if (Double.TYPE.equals(type)) {
+            return Double.class.getCanonicalName().replace('.', '/');
+        } else if (Void.TYPE.equals(type)) {
+            return Void.class.getCanonicalName().replace('.', '/');
+        }
+
+        throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
+    }
+
+    /**
+     * Returns the appropriate bytecode instruction to load a value from a variable to the stack
+     *
+     * @param type Type to load
+     * @return Bytecode instruction to use
+     */
+    private int getVarInsn(Class<?> type) {
+        if (type.isPrimitive()) {
+            if (Integer.TYPE.equals(type)) {
+                return ILOAD;
+            } else if (Boolean.TYPE.equals(type)) {
+                return ILOAD;
+            } else if (Character.TYPE.equals(type)) {
+                return ILOAD;
+            } else if (Byte.TYPE.equals(type)) {
+                return ILOAD;
+            } else if (Short.TYPE.equals(type)) {
+                return ILOAD;
+            } else if (Float.TYPE.equals(type)) {
+                return FLOAD;
+            } else if (Long.TYPE.equals(type)) {
+                return LLOAD;
+            } else if (Double.TYPE.equals(type)) {
+                return DLOAD;
+            }
+        }
+
+        throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
+    }
+
+    /**
+     * Invokes the most appropriate bytecode instruction to put a number on the stack
+     *
+     * @param mv
+     * @param i
+     */
+    private void pushIntOntoStack(final MethodVisitor mv, final int i) {
+        if (i == 0) {
+            mv.visitInsn(ICONST_0);
+        } else if (i == 1) {
+            mv.visitInsn(ICONST_1);
+        } else if (i == 2) {
+            mv.visitInsn(ICONST_2);
+        } else if (i == 3) {
+            mv.visitInsn(ICONST_3);
+        } else if (i == 4) {
+            mv.visitInsn(ICONST_4);
+        } else if (i == 5) {
+            mv.visitInsn(ICONST_5);
+        } else if (i > 5 && i <= 255) {
+            mv.visitIntInsn(BIPUSH, i);
+        } else {
+            mv.visitIntInsn(SIPUSH, i);
+        }
+    }
+
+    /**
+     * Gets the appropriate bytecode instruction for RETURN, according to what type we need to return
+     *
+     * @param type Type the needs to be returned
+     * @return The matching bytecode instruction
+     */
+    private int getReturnInsn(final Class<?> type) {
+        if (type.isPrimitive()) {
+            if (Void.TYPE.equals(type)) {
+                return RETURN;
+            }
+            if (Integer.TYPE.equals(type)) {
+                return IRETURN;
+            } else if (Boolean.TYPE.equals(type)) {
+                return IRETURN;
+            } else if (Character.TYPE.equals(type)) {
+                return IRETURN;
+            } else if (Byte.TYPE.equals(type)) {
+                return IRETURN;
+            } else if (Short.TYPE.equals(type)) {
+                return IRETURN;
+            } else if (Float.TYPE.equals(type)) {
+                return FRETURN;
+            } else if (Long.TYPE.equals(type)) {
+                return LRETURN;
+            } else if (Double.TYPE.equals(type)) {
+                return DRETURN;
+            }
+        }
+        return ARETURN;
+    }
+
+    /**
+     * Gets the string to use for CHECKCAST instruction, returning the correct value for any type, including primitives and arrays
+     *
+     * @param returnType The type to cast to with CHECKCAST
+     * @return CHECKCAST parameter
+     */
+    private String getCastType(Class<?> returnType) {
+        if (returnType.isPrimitive()) {
+            return getWrapperType(returnType);
+        } else {
+            return Type.getInternalName(returnType);
+        }
+    }
+
+    /**
+     * Returns the name of the Java method to call to get the primitive value from an Object - e.g. intValue for java.lang.Integer
+     *
+     * @param type Type whose primitive method we want to lookup
+     * @return The name of the method to use
+     */
+    private String getPrimitiveMethod(final Class<?> type) {
+        if (Integer.TYPE.equals(type)) {
+            return "intValue";
+        } else if (Boolean.TYPE.equals(type)) {
+            return "booleanValue";
+        } else if (Character.TYPE.equals(type)) {
+            return "charValue";
+        } else if (Byte.TYPE.equals(type)) {
+            return "byteValue";
+        } else if (Short.TYPE.equals(type)) {
+            return "shortValue";
+        } else if (Float.TYPE.equals(type)) {
+            return "floatValue";
+        } else if (Long.TYPE.equals(type)) {
+            return "longValue";
+        } else if (Double.TYPE.equals(type)) {
+            return "doubleValue";
+        }
+
+        throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
+    }
+
+    private void generateReturn(final MethodVisitor mv, final Method delegatedMethod) {
+        final Class<?> returnType = delegatedMethod.getReturnType();
+        mv.visitInsn(getReturnInsn(returnType));
+    }
+
+    /**
+     * Create an Object[] parameter which contains all the parameters of the currently invoked method
+     * and store this array for use in the call stack.
+     *
+     * @param mv
+     * @param parameterTypes
+     */
+    private void pushMethodParameterArray(MethodVisitor mv, Class<?>[] parameterTypes) {
+        // need to construct the array of objects passed in
+        // create the Object[]
+        createArrayDefinition(mv, parameterTypes.length, Object.class);
+
+        int index = 1;
+        for (int i = 0; i < parameterTypes.length; i++) {
+            // keep copy of array on stack
+            mv.visitInsn(DUP);
+
+            final Class<?> parameterType = parameterTypes[i];
+            pushIntOntoStack(mv, i);
+
+            if (parameterType.isPrimitive()) {
+                final String wrapperType = getWrapperType(parameterType);
+                mv.visitVarInsn(getVarInsn(parameterType), index);
+                mv.visitMethodInsn(INVOKESTATIC, wrapperType, "valueOf",
+                        "(" + Type.getDescriptor(parameterType) + ")L" + wrapperType + ";", false);
+                mv.visitInsn(AASTORE);
+
+                if (Long.TYPE.equals(parameterType) || Double.TYPE.equals(parameterType)) {
+                    index += 2;
+                } else {
+                    index++;
+                }
+            } else {
+                mv.visitVarInsn(ALOAD, index);
+                mv.visitInsn(AASTORE);
+                index++;
+            }
+        }
+    }
+
+    private void createArrayDefinition(final MethodVisitor mv, final int size, final Class<?> type) {
+        if (size < 0) {
+            throw new IllegalStateException("Array size cannot be less than zero");
+        }
+        pushIntOntoStack(mv, size);
+        mv.visitTypeInsn(ANEWARRAY, type.getCanonicalName().replace('.', '/'));
+    }
+
+
+    private static class VersionVisitor extends ClassVisitor {
+        private int version;
+
+        private VersionVisitor() {
+            super(ASM7);
+        }
+
+        @Override
+        public void visit(final int version, final int access, final String name,
+                          final String signature, final String superName, final String[] interfaces) {
+            this.version = version;
+        }
+    }
+
+    public interface InterceptorHandler {
+        Object invoke(Method method, Object[] args) throws Exception;
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/ProxyFactory.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/ProxyFactory.java
new file mode 100644
index 0000000..0fe2479
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/ProxyFactory.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.proxy;
+
+import static java.util.Collections.emptyList;
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
+import java.net.URL;
+import java.security.ProtectionDomain;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptorInstance;
+import org.apache.karaf.service.interceptor.impl.runtime.invoker.InterceptorInvocationContext;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+public class ProxyFactory {
+
+    private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];
+
+    public <T> T create(final ServiceReference<T> ref, final List<Class<?>> classes,
+                        final List<InterceptorInstance<?>> interceptors,
+                        final Map<Method, List<Class<?>>> interceptorsPerMethod) {
+        if (classes.isEmpty()) {
+            throw new IllegalArgumentException("Can't proxy an empty list of type: " + ref);
+        }
+
+        final Map<Method, List<InterceptorInstance<?>>> interceptorInstancePerMethod = interceptorsPerMethod.entrySet().stream()
+            .collect(toMap(Map.Entry::getKey, m -> m.getValue().stream()
+                    .map(binding -> interceptors.stream().filter(i -> i.getBinding() == binding).findFirst().orElse(null))
+                    .collect(toList())));
+
+        final ProxyClassLoader loader = new ProxyClassLoader(Thread.currentThread().getContextClassLoader(), ref.getBundle());
+        if (classes.stream().allMatch(Class::isInterface)) {
+            final Object proxyInstance = Proxy.newProxyInstance(
+                    loader,
+                    classes.toArray(EMPTY_CLASSES),
+                    (proxy, method, args) -> doInvoke(ref, method, args, interceptorInstancePerMethod));
+            return (T) proxyInstance;
+        }
+        final AsmProxyFactory asm = new AsmProxyFactory();
+        final Class<?> proxyClass = asm.createProxyClass(
+                loader,
+                getProxyClassName(classes),
+                classes.stream().sorted(this::compareClasses).toArray(Class<?>[]::new),
+                findInterceptedMethods(classes));
+        return asm.create(proxyClass, (method, args) -> doInvoke(ref, method, args, interceptorInstancePerMethod));
+    }
+
+    private <T> Object doInvoke(final ServiceReference<T> ref,
+                                final Method method, final Object[] args,
+                                final Map<Method, List<InterceptorInstance<?>>> interceptorsPerMethod) throws Exception {
+        final List<InterceptorInstance<?>> methodInterceptors = interceptorsPerMethod.getOrDefault(method, emptyList());
+        return new InterceptorInvocationContext<>(ref, methodInterceptors, method, args).proceed();
+    }
+
+    private int compareClasses(final Class<?> c1, final Class<?> c2) {
+        if (c1 == c2) {
+            return 0;
+        }
+        if (c1.isAssignableFrom(c2)) {
+            return 1;
+        }
+        if (c2.isAssignableFrom(c1)) {
+            return -1;
+        }
+        if (c1.isInterface() && !c2.isInterface()) {
+            return 1;
+        }
+        if (c2.isInterface() && !c1.isInterface()) {
+            return -1;
+        }
+        if (!c1.isInterface() && !c2.isInterface()) {
+            throw new IllegalArgumentException("No common class between " + c1 + " and " + c2);
+        }
+        return c1.getName().compareTo(c2.getName()); // just to be deterministic
+    }
+
+    private Method[] findInterceptedMethods(final List<Class<?>> classes) {
+        return classes.stream()
+                .flatMap(c -> c.isInterface() ? Stream.of(c.getMethods()) : findMethods(c))
+                .distinct()
+                .filter(method -> Modifier.isPublic(method.getModifiers())) // todo: enable protected? not that scr friendly but doable
+                .toArray(Method[]::new);
+    }
+
+    private Stream<Method> findMethods(final Class<?> clazz) {
+        return clazz == null || Object.class == clazz ?
+                Stream.empty() :
+                Stream.concat(Stream.of(clazz.getDeclaredMethods()), findMethods(clazz.getSuperclass()));
+    }
+
+    private String getProxyClassName(final List<Class<?>> classes) {
+        return classes.iterator().next().getName() + "$$KarafInterceptorProxy" +
+                classes.stream().skip(1).map(c -> c.getName().replace(".", "_").replace("$", "")).collect(joining("__"));
+    }
+
+    static class ProxyClassLoader extends ClassLoader {
+        private final Bundle bundle;
+        private final Map<String, Class<?>> classes = new ConcurrentHashMap<>();
+
+        ProxyClassLoader(final ClassLoader parent, final Bundle bundle) {
+            super(parent);
+            this.bundle = bundle;
+        }
+
+        @Override
+        protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
+            final Class<?> clazz = classes.get(name);
+            if (clazz != null) {
+                return clazz;
+            }
+            if (bundle != null) {
+                try {
+                    return bundle.loadClass(name);
+                } catch (final ClassNotFoundException cnfe) {
+                    if (name != null && name.startsWith("org.apache.karaf.service.interceptor.")) {
+                        return getClass().getClassLoader().loadClass(name);
+                    }
+                    throw cnfe;
+                }
+            }
+            return super.loadClass(name, resolve);
+        }
+
+        @Override
+        public URL getResource(final String name) {
+            return bundle.getResource(name);
+        }
+
+        @Override
+        public Enumeration<URL> getResources(final String name) throws IOException {
+            return bundle.getResources(name);
+        }
+
+        @Override
+        public InputStream getResourceAsStream(final String name) {
+            return ofNullable(getResource(name)).map(u -> {
+                try {
+                    return u.openStream();
+                } catch (final IOException e) {
+                    throw new IllegalStateException(e);
+                }
+            }).orElse(null);
+        }
+
+        public <T> Class<T> getOrRegister(final String proxyClassName, final byte[] proxyBytes,
+                                          final Package pck, final ProtectionDomain protectionDomain) {
+            final String key = proxyClassName.replace('/', '.');
+            Class<?> existing = classes.get(key);
+            if (existing == null) {
+                synchronized (this) {
+                    existing = classes.get(key);
+                    if (existing == null) {
+                        definePackageFor(pck, protectionDomain);
+                        existing = super.defineClass(proxyClassName, proxyBytes, 0, proxyBytes.length);
+                        resolveClass(existing);
+                        classes.put(key, existing);
+                    }
+                }
+            }
+            return (Class<T>) existing;
+        }
+
+        private void definePackageFor(final Package model, final ProtectionDomain protectionDomain) {
+            if (model == null) {
+                return;
+            }
+            if (getPackage(model.getName()) == null) {
+                if (model.isSealed() && protectionDomain != null &&
+                        protectionDomain.getCodeSource() != null &&
+                        protectionDomain.getCodeSource().getLocation() != null) {
+                    definePackage(
+                            model.getName(),
+                            model.getSpecificationTitle(),
+                            model.getSpecificationVersion(),
+                            model.getSpecificationVendor(),
+                            model.getImplementationTitle(),
+                            model.getImplementationVersion(),
+                            model.getImplementationVendor(),
+                            protectionDomain.getCodeSource().getLocation());
+                } else {
+                    definePackage(
+                            model.getName(),
+                            model.getSpecificationTitle(),
+                            model.getSpecificationVersion(),
+                            model.getSpecificationVendor(),
+                            model.getImplementationTitle(),
+                            model.getImplementationVersion(),
+                            model.getImplementationVendor(),
+                            null);
+                }
+            }
+        }
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptedServiceRegistry.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptedServiceRegistry.java
new file mode 100644
index 0000000..f9da914
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptedServiceRegistry.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.registry;
+
+import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+public class InterceptedServiceRegistry implements ServiceListener {
+    private final PropertiesManager propertiesManager;
+    private final Consumer<ServiceReference<?>> onServiceAddition;
+    private final Consumer<ServiceReference<?>> onServiceRemoval;
+    private final Map<ServiceReference<?>, RegistrationState> references = new ConcurrentHashMap<>();
+
+    public InterceptedServiceRegistry(final Consumer<ServiceReference<?>> onServiceAddition,
+                                      final Consumer<ServiceReference<?>> onServiceRemoval,
+                                      final PropertiesManager propertiesManager) {
+        this.onServiceAddition = onServiceAddition;
+        this.onServiceRemoval = onServiceRemoval;
+        this.propertiesManager = propertiesManager;
+    }
+
+    @Override
+    public void serviceChanged(final ServiceEvent serviceEvent) {
+        final ServiceReference<?> ref = serviceEvent.getServiceReference();
+        switch (serviceEvent.getType()) {
+            case ServiceEvent.REGISTERED:
+                doRegister(ref);
+                break;
+            case ServiceEvent.MODIFIED_ENDMATCH:
+            case ServiceEvent.UNREGISTERING:
+                doRemove(ref);
+                break;
+            case ServiceEvent.MODIFIED:
+                ofNullable(references.get(ref))
+                        .filter(reg -> didChange(ref, reg))
+                        .ifPresent(reg -> {
+                            doRemove(ref);
+                            doRegister(ref);
+                        });
+            default:
+        }
+    }
+
+    private boolean didChange(final ServiceReference<?> ref, final RegistrationState reg) {
+        return !reg.registrationProperties.equals(propertiesManager.collectProperties(ref)) ||
+                !reg.bindingsPerMethod.equals(computeBindings(ref));
+    }
+
+    private void doRegister(final ServiceReference<?> ref) {
+        references.put(ref, new RegistrationState(propertiesManager.collectProperties(ref), computeBindings(ref)));
+        onServiceAddition.accept(ref);
+    }
+
+    private void doRemove(final ServiceReference<?> ref) {
+        onServiceRemoval.accept(ref);
+        references.remove(ref);
+    }
+
+    private Map<Method, List<Class<?>>> computeBindings(final ServiceReference<?> ref) {
+        final List<Class<?>> types = propertiesManager.unflattenStringValues(ref.getProperty(Constants.OBJECTCLASS))
+                .map(it -> {
+                    try {
+                        return ref.getBundle().loadClass(it);
+                    } catch (final ClassNotFoundException e) {
+                        throw new IllegalStateException(e);
+                    }
+                })
+                .distinct()
+                .collect(toList());
+        final Collection<Annotation> globalInterceptors = types.stream()
+                .flatMap(type -> Stream.of(type.getAnnotations()))
+                .filter(methodAnnotation -> methodAnnotation.annotationType().isAnnotationPresent(InterceptorBinding.class))
+                .distinct()
+                .collect(toList());
+        return types.stream()
+                .flatMap(type -> Stream.of(type.getMethods()))
+                .collect(toMap(identity(), m -> Stream.concat(
+                        globalInterceptors.stream(),
+                        Stream.of(m.getAnnotations()))
+                        .filter(methodAnnotation -> methodAnnotation.annotationType().isAnnotationPresent(InterceptorBinding.class))
+                        .distinct()
+                        .map(Annotation::annotationType) // todo: keep Annotation with values
+                        .collect(toList())));
+    }
+
+    public <T> Stream<Class<?>> getBindings(final ServiceReference<T> ref) {
+        return ofNullable(references.get(ref))
+                .map(reg -> reg.bindingsPerMethod.values().stream().flatMap(Collection::stream).distinct())
+                .orElseGet(Stream::empty);
+    }
+
+    public <T> Map<Method, List<Class<?>>> getInterceptorsPerMethod(final ServiceReference<T> ref) {
+        return ofNullable(references.get(ref))
+                .map(reg -> reg.bindingsPerMethod)
+                .orElseGet(Collections::emptyMap);
+    }
+
+    private static class RegistrationState {
+        private final Hashtable<String, Object> registrationProperties;
+        private final Map<Method, List<Class<?>>> bindingsPerMethod;
+
+        private RegistrationState(final Hashtable<String, Object> registrationProperties,
+                                  final Map<Method, List<Class<?>>> bindingsPerMethod) {
+            this.registrationProperties = registrationProperties;
+            this.bindingsPerMethod = bindingsPerMethod;
+        }
+    }
+}
diff --git a/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptorRegistry.java b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptorRegistry.java
new file mode 100644
index 0000000..accca67
--- /dev/null
+++ b/services/interceptor/impl/src/main/java/org/apache/karaf/service/interceptor/impl/runtime/registry/InterceptorRegistry.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.registry;
+
+import static java.util.stream.Collectors.toList;
+
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+import org.apache.karaf.service.interceptor.impl.runtime.PropertiesManager;
+import org.apache.karaf.service.interceptor.impl.runtime.hook.InterceptorInstance;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+
+public class InterceptorRegistry implements ServiceListener {
+    private final Consumer<Class<?>> onAddition;
+    private final Consumer<Class<?>> onRemoval;
+    private final PropertiesManager propertiesManager;
+    private final Map<Class<?>, InterceptorInstance<?>> interceptors = new ConcurrentHashMap<>();
+
+    public InterceptorRegistry(final Consumer<Class<?>> onAddition,
+                               final Consumer<Class<?>> onRemoval,
+                               final PropertiesManager propertiesManager) {
+        this.onAddition = onAddition;
+        this.onRemoval = onRemoval;
+        this.propertiesManager = propertiesManager;
+    }
+
+    public boolean areBindingsAvailable(final Stream<Class<?>> bindings) {
+        return bindings.allMatch(binding -> binding != null && interceptors.containsKey(binding));
+    }
+
+    public List<InterceptorInstance<?>> getInterceptors(final List<Class<?>> bindings) {
+        return bindings.stream().map(interceptors::get).distinct().collect(toList());
+    }
+
+    @Override
+    public void serviceChanged(final ServiceEvent serviceEvent) {
+        final Class<? extends Annotation> bindingClass = getInterceptorBinding(serviceEvent);
+        switch (serviceEvent.getType()) {
+            case ServiceEvent.REGISTERED: {
+                interceptors.put(bindingClass, new InterceptorInstance<>(
+                        serviceEvent.getServiceReference(), bindingClass, propertiesManager));
+                onAddition.accept(bindingClass);
+                break;
+            }
+            case ServiceEvent.MODIFIED_ENDMATCH:
+            case ServiceEvent.UNREGISTERING: {
+                interceptors.remove(bindingClass);
+                onRemoval.accept(bindingClass);
+                break;
+            }
+            case ServiceEvent.MODIFIED:
+            default:
+        }
+    }
+
+    private Class<? extends Annotation> getInterceptorBinding(final ServiceEvent serviceEvent) {
+        final List<Annotation> bindings = propertiesManager.unflattenStringValues(serviceEvent.getServiceReference().getProperty(Constants.OBJECTCLASS))
+                .map(it -> {
+                    try {
+                        return serviceEvent.getServiceReference().getBundle().loadClass(it);
+                    } catch (final ClassNotFoundException e) {
+                        throw new IllegalStateException(e);
+                    }
+                })
+                .flatMap(it -> Stream.of(it.getAnnotations()))
+                .filter(it -> it.annotationType().isAnnotationPresent(InterceptorBinding.class))
+                .distinct()
+                .collect(toList());
+        if (bindings.size() != 1) {
+            throw new IllegalArgumentException("A single @InterceptorBinding on " + serviceEvent + " is required, found: " + bindings);
+        }
+        // todo: keep annotation instance to support binding values?
+        return bindings.iterator().next().annotationType();
+    }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/E2ETest.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/E2ETest.java
new file mode 100644
index 0000000..048422c
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/E2ETest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.ops4j.pax.exam.CoreOptions.bundle;
+import static org.ops4j.pax.exam.CoreOptions.systemTimeout;
+import static org.ops4j.pax.exam.CoreOptions.url;
+import static org.ops4j.pax.exam.container.remote.RBCRemoteTargetOptions.waitForRBCFor;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureSecurity;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.karafDistributionConfiguration;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.keepRuntimeFolder;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.karaf.service.interceptor.impl.test.InterceptedService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.ProbeBuilder;
+import org.ops4j.pax.exam.TestProbeBuilder;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.karaf.options.LogLevelOption;
+import org.ops4j.pax.exam.options.UrlProvisionOption;
+import org.ops4j.pax.exam.options.UrlReference;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.Constants;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class E2ETest {
+    @Inject
+    private InterceptedService interceptedService;
+
+    @Test
+    public void run() {
+        assertTrue(interceptedService.getClass().getName().contains("$$KarafInterceptorProxy"));
+        assertEquals("wrapped>from 'org.apache.karaf.service.interceptor.impl.test.InterceptedService'<", interceptedService.wrap());
+        assertEquals("wrapped>'bar'(suffixed)<", interceptedService.wrapAndSuffix("bar"));
+    }
+
+    @ProbeBuilder
+    public TestProbeBuilder probeConfiguration(final TestProbeBuilder probe) {
+        probe.setHeader(Constants.EXPORT_PACKAGE, "org.apache.karaf.service.interceptor.impl.test");
+        probe.setHeader("Service-Component",
+                "OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml," +
+                "OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml," +
+                "OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml");
+        return probe;
+    }
+
+    @Configuration
+    public Option[] config() throws MalformedURLException {
+        final String localRepository = System.getProperty("org.ops4j.pax.url.mvn.localRepository", "");
+        final UrlReference karafUrl = url(new File("target/libs/karaf.tar.gz").toURI().toURL().toExternalForm());
+        final UrlReference asmUrl = url(new File("target/libs/asm.jar").toURI().toURL().toExternalForm());
+        final UrlProvisionOption apiBundle = url(Optional.ofNullable(new File("../api/target")
+                .listFiles((dir, name) -> name.startsWith("org.apache.karaf.services.interceptor.api-") && isNotReleaseArtifact(name)))
+            .map(files -> files[0])
+            .orElseThrow(() -> new IllegalArgumentException("No interceptor api bundle found, ensure api module was built"))
+            .toURI().toURL().toExternalForm());
+        final UrlProvisionOption implBundle = url(Optional.ofNullable(new File("target")
+                .listFiles((dir, name) -> name.startsWith("org.apache.karaf.services.interceptor.impl-") && isNotReleaseArtifact(name)))
+            .map(files -> files[0])
+            .orElseThrow(() -> new IllegalArgumentException("No interceptor impl bundle found, ensure impl module was built"))
+            .toURI().toURL().toExternalForm());
+        return new Option[]{
+                karafDistributionConfiguration()
+                        .frameworkUrl(karafUrl.getURL())
+                        .name("Apache Karaf")
+                        .runEmbedded(true)
+                        .unpackDirectory(new File("target/exam")),
+                configureSecurity().disableKarafMBeanServerBuilder(),
+                configureConsole().ignoreLocalConsole(),
+                keepRuntimeFolder(),
+                logLevel(LogLevelOption.LogLevel.INFO),
+                systemTimeout(3600000),
+                waitForRBCFor(3600000),
+                editConfigurationFilePut("etc/org.apache.karaf.features.cfg", "updateSnapshots", "none"),
+                editConfigurationFilePut("etc/org.ops4j.pax.url.mvn.cfg", "org.ops4j.pax.url.mvn.localRepository", localRepository),
+                editConfigurationFilePut("etc/branding.properties", "welcome", ""), // No welcome banner
+                editConfigurationFilePut("etc/branding-ssh.properties", "welcome", ""),
+                features("mvn:org.apache.karaf.features/standard/" + System.getProperty("karaf.version") + "/xml/features", "scr"),
+                bundle(asmUrl.getURL()),
+                bundle(apiBundle.getURL()),
+                bundle(implBundle.getURL())
+        };
+    }
+
+    private boolean isNotReleaseArtifact(final String name) {
+        return name.endsWith(".jar") && !name.contains("-sources") && !name.contains("javadoc");
+    }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactoryTest.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactoryTest.java
new file mode 100644
index 0000000..03bcc3f
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/runtime/proxy/AsmProxyFactoryTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.runtime.proxy;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+public class AsmProxyFactoryTest {
+    @Test
+    public void proxy() {
+        final ProxyFactory.ProxyClassLoader classLoader = new ProxyFactory.ProxyClassLoader(Thread.currentThread().getContextClassLoader(), null);
+        final AsmProxyFactory factory = new AsmProxyFactory();
+        final Class<?> proxyClass = factory.createProxyClass(
+                classLoader, Foo.class.getName() + "$$ProxyTestProxy1",
+                new Class<?>[]{Foo.class},
+                Foo.class.getDeclaredMethods());
+        assertNotNull(proxyClass);
+
+        final Foo instance = Foo.class.cast(factory.create(proxyClass, (method, args) -> {
+            switch (method.getName()) {
+                case "fail":
+                    throw new IOException("it must be a checked exception to ensure it is well propagated");
+                default:
+                    return method.getName() + "(" + asList(args) + ")";
+            }
+        }));
+        assertEquals("foo1([])", instance.foo1());
+        assertEquals("foo2([param])", instance.foo2("param"));
+        assertTrue(instance.toString().startsWith(Foo.class.getName() + "$$ProxyTestProxy1@"));
+        try {
+            instance.fail();
+            fail();
+        } catch (final IOException e) {
+            assertEquals("it must be a checked exception to ensure it is well propagated", e.getMessage());
+        }
+    }
+
+    public static class Foo {
+        public String foo1() {
+            return "first";
+        }
+
+        public String foo2(final String some) {
+            return "second<" + some + ">";
+        }
+
+        public String fail() throws IOException {
+            return "ok";
+        }
+    }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/InterceptedService.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/InterceptedService.java
new file mode 100644
index 0000000..6d5eaec
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/InterceptedService.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.test;
+
+import org.apache.karaf.service.interceptor.api.EnableInterceptors;
+import org.osgi.service.component.annotations.Component;
+
+@Wrap
+@EnableInterceptors
+@Component(service = InterceptedService.class)
+public class InterceptedService {
+    @Suffix
+    public String wrapAndSuffix(final String value) {
+        return "'" + value + "'";
+    }
+
+    public String wrap() {
+        return "from '" + getClass().getName() + "'";
+    }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Suffix.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Suffix.java
new file mode 100644
index 0000000..d62b735
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Suffix.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.test;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+@InterceptorBinding
+public @interface Suffix {
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/SuffixingInterceptor.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/SuffixingInterceptor.java
new file mode 100644
index 0000000..4418a2e
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/SuffixingInterceptor.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.test;
+
+import org.apache.karaf.service.interceptor.api.AroundInvoke;
+import org.apache.karaf.service.interceptor.api.Interceptor;
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.osgi.service.component.annotations.Component;
+
+@Suffix
+@Interceptor
+@Component(service = SuffixingInterceptor.class)
+public class SuffixingInterceptor {
+    @AroundInvoke
+    public Object around(final InvocationContext context) throws Exception {
+        return context.proceed() + "(suffixed)";
+    }
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Wrap.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Wrap.java
new file mode 100644
index 0000000..6280fdf
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/Wrap.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.test;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.apache.karaf.service.interceptor.api.InterceptorBinding;
+
+@Target(TYPE)
+@Retention(RUNTIME)
+@InterceptorBinding
+public @interface Wrap {
+}
diff --git a/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/WrappingInterceptor.java b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/WrappingInterceptor.java
new file mode 100644
index 0000000..0653c0e
--- /dev/null
+++ b/services/interceptor/impl/src/test/java/org/apache/karaf/service/interceptor/impl/test/WrappingInterceptor.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.karaf.service.interceptor.impl.test;
+
+import org.apache.karaf.service.interceptor.api.AroundInvoke;
+import org.apache.karaf.service.interceptor.api.Interceptor;
+import org.apache.karaf.service.interceptor.api.InvocationContext;
+import org.osgi.service.component.annotations.Component;
+
+@Wrap
+@Interceptor
+@Component(service = WrappingInterceptor.class)
+public class WrappingInterceptor {
+    @AroundInvoke
+    public Object around(final InvocationContext context) throws Exception {
+        return "wrapped>" + context.proceed() + "<";
+    }
+}
diff --git a/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml
new file mode 100644
index 0000000..919a970
--- /dev/null
+++ b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.InterceptedService.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.apache.karaf.service.interceptor.impl.test.InterceptedService">
+  <property name="apache.karaf.enable.interceptors" type="Boolean" value="true"/>
+  <implementation class="org.apache.karaf.service.interceptor.impl.test.InterceptedService" />
+  <service>
+    <scr:provide interface="org.apache.karaf.service.interceptor.impl.test.InterceptedService"/>
+  </service>
+</scr:component>
diff --git a/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml
new file mode 100644
index 0000000..89d057e
--- /dev/null
+++ b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor">
+  <property name="apache.karaf.interceptor" type="Boolean" value="true"/>
+  <implementation class="org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor"/>
+  <service>
+    <scr:provide interface="org.apache.karaf.service.interceptor.impl.test.SuffixingInterceptor"/>
+  </service>
+</scr:component>
diff --git a/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml
new file mode 100644
index 0000000..47e8071
--- /dev/null
+++ b/services/interceptor/impl/src/test/resources/OSGI-INF/org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor">
+  <property name="apache.karaf.interceptor" type="Boolean" value="true"/>
+  <implementation class="org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor"/>
+  <service>
+    <scr:provide interface="org.apache.karaf.service.interceptor.impl.test.WrappingInterceptor"/>
+  </service>
+</scr:component>
diff --git a/services/interceptor/pom.xml b/services/interceptor/pom.xml
new file mode 100644
index 0000000..71035e0
--- /dev/null
+++ b/services/interceptor/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <!--
+
+      Licensed to the Apache Software Foundation (ASF) under one or more
+      contributor license agreements.  See the NOTICE file distributed with
+      this work for additional information regarding copyright ownership.
+      The ASF licenses this file to You 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.
+  -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf.services</groupId>
+        <artifactId>services</artifactId>
+        <version>4.3.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.karaf.services.interceptor</artifactId>
+    <packaging>pom</packaging>
+    <name>Apache Karaf :: Services :: Interceptor</name>
+    <description>Interceptor support (inspired from JavaEE/JakartaEE) on top of SCR.</description>
+
+    <modules>
+        <module>api</module>
+        <module>impl</module>
+    </modules>
+</project>
diff --git a/services/pom.xml b/services/pom.xml
index 6916d86..228fad6 100644
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -37,6 +37,7 @@
         <module>coordinator</module>
         <module>eventadmin</module>
         <module>staticcm</module>
+        <module>interceptor</module>
     </modules>
 
 </project>