You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ni...@apache.org on 2021/11/09 15:14:10 UTC

[ignite] branch master updated: IGNITE-15801 Annotation-based injection of ServiceContext. (#9542)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 7785089  IGNITE-15801 Annotation-based injection of ServiceContext. (#9542)
7785089 is described below

commit 77850899d7326dd630e2d0ef6624da1e38b5d996
Author: Pavel Pereslegin <xx...@gmail.com>
AuthorDate: Tue Nov 9 18:13:38 2021 +0300

    IGNITE-15801 Annotation-based injection of ServiceContext. (#9542)
---
 docs/_docs/services/services.adoc                  |   6 +-
 .../examples/servicegrid/SimpleMapServiceImpl.java |  11 +-
 .../processors/resource/GridResourceIoc.java       |   6 +-
 .../processors/resource/GridResourceProcessor.java |   7 +-
 .../processors/service/GridServiceProcessor.java   |  10 +-
 .../processors/service/IgniteServiceProcessor.java |  10 +-
 .../ignite/resources/ServiceContextResource.java   |  65 ++++++
 .../java/org/apache/ignite/services/Service.java   |  75 ++++++-
 .../org/apache/ignite/services/ServiceContext.java |   8 +-
 .../GridServiceContextInjectionSelfTest.java       | 246 +++++++++++++++++++++
 .../testsuites/IgniteResourceSelfTestSuite.java    |   2 +
 11 files changed, 418 insertions(+), 28 deletions(-)

diff --git a/docs/_docs/services/services.adoc b/docs/_docs/services/services.adoc
index 1823473..dfdf114 100644
--- a/docs/_docs/services/services.adoc
+++ b/docs/_docs/services/services.adoc
@@ -45,9 +45,9 @@ Refer to a service example implementation in the Apache Ignite link:{githubUrl}/
 A service implements the javadoc:org.apache.ignite.services.Service[Service] interface.
 The `Service` interface has three methods:
 
-* `init(ServiceContext)`: this method is called by Ignite before the service is deployed (and before the `execute()` method is called)
-* `execute(ServiceContext)`: starts execution of the service
-* `cancel(ServiceContext)`:  cancels service execution
+* `init()`: this method is called by Ignite before the service is deployed (and before the `execute()` method is called)
+* `execute()`: starts execution of the service
+* `cancel()`:  cancels service execution
 
 //The service must be available in the classpash of all server nodes. *TODO: deployment options*
 
diff --git a/examples/src/main/java/org/apache/ignite/examples/servicegrid/SimpleMapServiceImpl.java b/examples/src/main/java/org/apache/ignite/examples/servicegrid/SimpleMapServiceImpl.java
index 7ede239..1ac27b1 100644
--- a/examples/src/main/java/org/apache/ignite/examples/servicegrid/SimpleMapServiceImpl.java
+++ b/examples/src/main/java/org/apache/ignite/examples/servicegrid/SimpleMapServiceImpl.java
@@ -21,6 +21,7 @@ import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.resources.IgniteInstanceResource;
+import org.apache.ignite.resources.ServiceContextResource;
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceContext;
 
@@ -36,6 +37,10 @@ public class SimpleMapServiceImpl<K, V> implements Service, SimpleMapService<K,
     @IgniteInstanceResource
     private Ignite ignite;
 
+    /** Service context. */
+    @ServiceContextResource
+    private ServiceContext ctx;
+
     /** Underlying cache map. */
     private IgniteCache<K, V> cache;
 
@@ -60,14 +65,14 @@ public class SimpleMapServiceImpl<K, V> implements Service, SimpleMapService<K,
     }
 
     /** {@inheritDoc} */
-    @Override public void cancel(ServiceContext ctx) {
+    @Override public void cancel() {
         ignite.destroyCache(ctx.name());
 
         System.out.println("Service was cancelled: " + ctx.name());
     }
 
     /** {@inheritDoc} */
-    @Override public void init(ServiceContext ctx) throws Exception {
+    @Override public void init() {
         // Create a new cache for every service deployment.
         // Note that we use service name as cache name, which allows
         // for each service deployment to use its own isolated cache.
@@ -77,7 +82,7 @@ public class SimpleMapServiceImpl<K, V> implements Service, SimpleMapService<K,
     }
 
     /** {@inheritDoc} */
-    @Override public void execute(ServiceContext ctx) throws Exception {
+    @Override public void execute() throws Exception {
         System.out.println("Executing distributed service: " + ctx.name());
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceIoc.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceIoc.java
index 9bc47fb..e748be6 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceIoc.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceIoc.java
@@ -42,6 +42,7 @@ import org.apache.ignite.resources.IgniteInstanceResource;
 import org.apache.ignite.resources.JobContextResource;
 import org.apache.ignite.resources.LoadBalancerResource;
 import org.apache.ignite.resources.LoggerResource;
+import org.apache.ignite.resources.ServiceContextResource;
 import org.apache.ignite.resources.ServiceResource;
 import org.apache.ignite.resources.SpringApplicationContextResource;
 import org.apache.ignite.resources.SpringResource;
@@ -506,7 +507,10 @@ public class GridResourceIoc {
         JOB_CONTEXT(JobContextResource.class),
 
         /** */
-        CACHE_STORE_SESSION(CacheStoreSessionResource.class);
+        CACHE_STORE_SESSION(CacheStoreSessionResource.class),
+
+        /** */
+        SERVICE_CONTEXT(ServiceContextResource.class);
 
         /** */
         public final Class<? extends Annotation> clazz;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceProcessor.java
index 891eeb1..e3c16e7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/resource/GridResourceProcessor.java
@@ -39,6 +39,7 @@ import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.lifecycle.LifecycleBean;
 import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.spi.IgniteSpi;
 import org.jetbrains.annotations.Nullable;
 
@@ -329,6 +330,7 @@ public class GridResourceProcessor extends GridProcessorAdapter {
             case LOAD_BALANCER:
             case TASK_CONTINUOUS_MAPPER:
             case CACHE_STORE_SESSION:
+            case SERVICE_CONTEXT:
                 res = new GridResourceBasicInjector<>(param);
                 break;
 
@@ -514,10 +516,13 @@ public class GridResourceProcessor extends GridProcessorAdapter {
      * Injects resources into service.
      *
      * @param svc Service to inject.
+     * @param svcCtx Service context to be injected into the service.
      * @throws IgniteCheckedException If failed.
      */
-    public void inject(Service svc) throws IgniteCheckedException {
+    public void inject(Service svc, ServiceContext svcCtx) throws IgniteCheckedException {
         injectGeneric(svc);
+
+        inject(svc, GridResourceIoc.ResourceAnnotation.SERVICE_CONTEXT, null, null, svcCtx);
     }
 
     /**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProcessor.java
index 4836991..13b1f02 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/GridServiceProcessor.java
@@ -103,6 +103,7 @@ import org.apache.ignite.resources.JobContextResource;
 import org.apache.ignite.resources.LoggerResource;
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceConfiguration;
+import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.services.ServiceDeploymentException;
 import org.apache.ignite.services.ServiceDescriptor;
 import org.apache.ignite.thread.IgniteThreadFactory;
@@ -1298,7 +1299,7 @@ public class GridServiceProcessor extends ServiceProcessorAdapter implements Ign
             final Service svc;
 
             try {
-                svc = copyAndInject(assigns.configuration());
+                svc = copyAndInject(assigns.configuration(), svcCtx);
 
                 // Initialize service.
                 svc.init(svcCtx);
@@ -1368,10 +1369,11 @@ public class GridServiceProcessor extends ServiceProcessorAdapter implements Ign
 
     /**
      * @param cfg Service configuration.
+     * @param svcCtx Service context to be injected into the service.
      * @return Copy of service.
      * @throws IgniteCheckedException If failed.
      */
-    private Service copyAndInject(ServiceConfiguration cfg) throws IgniteCheckedException {
+    private Service copyAndInject(ServiceConfiguration cfg, ServiceContext svcCtx) throws IgniteCheckedException {
         Marshaller m = ctx.config().getMarshaller();
 
         if (cfg instanceof LazyServiceConfiguration) {
@@ -1379,7 +1381,7 @@ public class GridServiceProcessor extends ServiceProcessorAdapter implements Ign
 
             Service srvc = U.unmarshal(m, bytes, U.resolveClassLoader(null, ctx.config()));
 
-            ctx.resource().inject(srvc);
+            ctx.resource().inject(srvc, svcCtx);
 
             return srvc;
         }
@@ -1391,7 +1393,7 @@ public class GridServiceProcessor extends ServiceProcessorAdapter implements Ign
 
                 Service cp = U.unmarshal(m, bytes, U.resolveClassLoader(svc.getClass().getClassLoader(), ctx.config()));
 
-                ctx.resource().inject(cp);
+                ctx.resource().inject(cp, svcCtx);
 
                 return cp;
             }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
index e3ad553..c353460 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/service/IgniteServiceProcessor.java
@@ -79,6 +79,7 @@ import org.apache.ignite.marshaller.jdk.JdkMarshaller;
 import org.apache.ignite.plugin.security.SecurityPermission;
 import org.apache.ignite.services.Service;
 import org.apache.ignite.services.ServiceConfiguration;
+import org.apache.ignite.services.ServiceContext;
 import org.apache.ignite.services.ServiceDeploymentException;
 import org.apache.ignite.services.ServiceDescriptor;
 import org.apache.ignite.spi.communication.CommunicationSpi;
@@ -1188,7 +1189,7 @@ public class IgniteServiceProcessor extends ServiceProcessorAdapter implements I
             final Service srvc;
 
             try {
-                srvc = copyAndInject(cfg);
+                srvc = copyAndInject(cfg, srvcCtx);
 
                 // Initialize service.
                 srvc.init(srvcCtx);
@@ -1253,10 +1254,11 @@ public class IgniteServiceProcessor extends ServiceProcessorAdapter implements I
 
     /**
      * @param cfg Service configuration.
+     * @param svcCtx Service context to be injected into the service.
      * @return Copy of service.
      * @throws IgniteCheckedException If failed.
      */
-    private Service copyAndInject(ServiceConfiguration cfg) throws IgniteCheckedException {
+    private Service copyAndInject(ServiceConfiguration cfg, ServiceContext svcCtx) throws IgniteCheckedException {
         if (cfg instanceof LazyServiceConfiguration) {
             LazyServiceConfiguration srvcCfg = (LazyServiceConfiguration)cfg;
 
@@ -1267,7 +1269,7 @@ public class IgniteServiceProcessor extends ServiceProcessorAdapter implements I
             Service srvc = U.unmarshal(marsh, bytes,
                 U.resolveClassLoader(srvcDep != null ? srvcDep.classLoader() : null, ctx.config()));
 
-            ctx.resource().inject(srvc);
+            ctx.resource().inject(srvc, svcCtx);
 
             return srvc;
         }
@@ -1279,7 +1281,7 @@ public class IgniteServiceProcessor extends ServiceProcessorAdapter implements I
 
                 Service cp = U.unmarshal(marsh, bytes, U.resolveClassLoader(srvc.getClass().getClassLoader(), ctx.config()));
 
-                ctx.resource().inject(cp);
+                ctx.resource().inject(cp, svcCtx);
 
                 return cp;
             }
diff --git a/modules/core/src/main/java/org/apache/ignite/resources/ServiceContextResource.java b/modules/core/src/main/java/org/apache/ignite/resources/ServiceContextResource.java
new file mode 100644
index 0000000..564e538
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/resources/ServiceContextResource.java
@@ -0,0 +1,65 @@
+/*
+ * 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.ignite.resources;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * Annotates a field or a setter method for injecting a {@link ServiceContext service context} into a {@link Service
+ * service} instance.
+ * <p>
+ * It is guaranteed that context will be injected before calling the {@link Service#init()} method.
+ * <p>
+ * Here is how injection would typically happen:
+ * <pre>{@code
+ * public class MyServiceImpl implements MyService {
+ *      ...
+ *      @ServiceContextResource
+ *      private ServiceContext ctx;
+ *      ...
+ *  }
+ * }</pre>
+ * or attach the same annotation to the method:
+ * <pre>{@code
+ * public class MyServiceImpl implements MyService {
+ *     ...
+ *     private ServiceContext ctx;
+ *     ...
+ *     @ServiceContextResource
+ *     public void setServiceContext(ServiceContext ctx) {
+ *          this.ctx = ctx;
+ *     }
+ *     ...
+ * }
+ * }</pre>
+ *
+ * @see Service
+ * @see ServiceContext
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface ServiceContextResource {
+    // No-op.
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/services/Service.java b/modules/core/src/main/java/org/apache/ignite/services/Service.java
index e82f6c3..3d8838f 100644
--- a/modules/core/src/main/java/org/apache/ignite/services/Service.java
+++ b/modules/core/src/main/java/org/apache/ignite/services/Service.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.services;
 
 import java.io.Serializable;
+import org.apache.ignite.resources.ServiceContextResource;
 
 /**
  * An instance of grid-managed service. Grid-managed services may be deployed from
@@ -26,7 +27,7 @@ import java.io.Serializable;
  * Whenever service is deployed, Ignite will automatically calculate how many
  * instances of this service should be deployed on each node within the cluster.
  * Whenever service is deployed on a cluster node, Ignite will call
- * {@link #execute(ServiceContext)} method on that service. It is up to the user
+ * {@link #execute()} method on that service. It is up to the user
  * to control whenever the service should exit from the {@code execute} method.
  * For example, user may choose to implement service as follows:
  * <pre name="code" class="java">
@@ -36,12 +37,15 @@ import java.io.Serializable;
  *      // You should inject resources only as needed.
  *      &#64;IgniteInstanceResource
  *      private Ignite ignite;
+ *
+ *      &#64;ServiceContextResource
+ *      private ServiceContext ctx;
  *      ...
- *      &#64;Override public void cancel(ServiceContext ctx) {
+ *      &#64;Override public void cancel() {
  *          // No-op.
  *      }
  *
- *      &#64;Override public void execute(ServiceContext ctx) {
+ *      &#64;Override public void execute() {
  *          // Loop until service is cancelled.
  *          while (!ctx.isCancelled()) {
  *              // Do something.
@@ -76,10 +80,10 @@ import java.io.Serializable;
  * <h1 class="header">Cancellation</h1>
  * Services can be cancelled by calling any of the {@code cancel} methods on {@link org.apache.ignite.IgniteServices} API.
  * Whenever a deployed service is cancelled, Ignite will automatically call
- * {@link Service#cancel(ServiceContext)} method on that service.
+ * {@link #cancel()} method on that service.
  * <p>
- * Note that Ignite cannot guarantee that the service exits from {@link Service#execute(ServiceContext)}
- * method whenever {@link #cancel(ServiceContext)} is called. It is up to the user to
+ * Note that Ignite cannot guarantee that the service exits from {@link #execute()}
+ * method whenever {@link #cancel()} is called. It is up to the user to
  * make sure that the service code properly reacts to cancellations.
  */
 public interface Service extends Serializable {
@@ -87,13 +91,58 @@ public interface Service extends Serializable {
      * Cancels this service. Ignite will automatically call this method whenever any of the
      * {@code cancel} methods on {@link org.apache.ignite.IgniteServices} API are called.
      * <p>
+     * Note that Ignite cannot guarantee that the service exits from {@link #execute()}
+     * method whenever {@code cancel()} method is called. It is up to the user to
+     * make sure that the service code properly reacts to cancellations.
+     *
+     * @see ServiceContextResource
+     */
+    public default void cancel() {
+        // No-op.
+    }
+
+    /**
+     * Pre-initializes service before execution. This method is guaranteed to be called before
+     * service deployment is complete (this guarantees that this method will be called
+     * before method {@link #execute()} is called).
+     *
+     * @see ServiceContextResource
+     * @throws Exception If service initialization failed.
+     */
+    public default void init() throws Exception {
+        // No-op.
+    }
+
+    /**
+     * Starts execution of this service. This method is automatically invoked whenever an instance of the service
+     * is deployed on a grid node. Note that service is considered deployed even after it exits the {@code execute}
+     * method and can be cancelled (or undeployed) only by calling any of the {@code cancel} methods on
+     * {@link org.apache.ignite.IgniteServices} API. Also note that service is not required to exit from {@code execute} method until
+     * {@link #cancel()} method was called.
+     *
+     * @see ServiceContextResource
+     * @throws Exception If service execution failed. Not that service will still remain deployed, until
+     *      {@link org.apache.ignite.IgniteServices#cancel(String)} method will be called.
+     */
+    public default void execute() throws Exception {
+        // No-op.
+    }
+
+    /**
+     * Cancels this service. Ignite will automatically call this method whenever any of the
+     * {@code cancel} methods on {@link org.apache.ignite.IgniteServices} API are called.
+     * <p>
      * Note that Ignite cannot guarantee that the service exits from {@link #execute(ServiceContext)}
      * method whenever {@code cancel(ServiceContext)} method is called. It is up to the user to
      * make sure that the service code properly reacts to cancellations.
      *
      * @param ctx Service execution context.
+     * @deprecated Use {@link #cancel()} instead.
      */
-    public void cancel(ServiceContext ctx);
+    @Deprecated
+    public default void cancel(ServiceContext ctx) {
+        cancel();
+    }
 
     /**
      * Pre-initializes service before execution. This method is guaranteed to be called before
@@ -102,8 +151,12 @@ public interface Service extends Serializable {
      *
      * @param ctx Service execution context.
      * @throws Exception If service initialization failed.
+     * @deprecated Use {@link #init()} instead.
      */
-    public void init(ServiceContext ctx) throws Exception;
+    @Deprecated
+    public default void init(ServiceContext ctx) throws Exception {
+        init();
+    }
 
     /**
      * Starts execution of this service. This method is automatically invoked whenever an instance of the service
@@ -115,6 +168,10 @@ public interface Service extends Serializable {
      * @param ctx Service execution context.
      * @throws Exception If service execution failed. Not that service will still remain deployed, until
      *      {@link org.apache.ignite.IgniteServices#cancel(String)} method will be called.
+     * @deprecated Use {@link #execute()} instead.
      */
-    public void execute(ServiceContext ctx) throws Exception;
+    @Deprecated
+    public default void execute(ServiceContext ctx) throws Exception {
+        execute();
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/services/ServiceContext.java b/modules/core/src/main/java/org/apache/ignite/services/ServiceContext.java
index 1ba64a9..bebf77a 100644
--- a/modules/core/src/main/java/org/apache/ignite/services/ServiceContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/services/ServiceContext.java
@@ -19,12 +19,14 @@ package org.apache.ignite.services;
 
 import java.io.Serializable;
 import java.util.UUID;
+import org.apache.ignite.resources.ServiceContextResource;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Service execution context. Execution context is provided into {@link Service#execute(ServiceContext)}
- * and {@link Service#cancel(ServiceContext)} methods and contains information about specific service
- * execution.
+ * Service execution context. This context is provided using {@link ServiceContextResource} annotation and contains
+ * information about specific service execution.
+ *
+ * @see ServiceContextResource
  */
 public interface ServiceContext extends Serializable {
     /**
diff --git a/modules/spring/src/test/java/org/apache/ignite/internal/processors/resource/GridServiceContextInjectionSelfTest.java b/modules/spring/src/test/java/org/apache/ignite/internal/processors/resource/GridServiceContextInjectionSelfTest.java
new file mode 100644
index 0000000..3281366
--- /dev/null
+++ b/modules/spring/src/test/java/org/apache/ignite/internal/processors/resource/GridServiceContextInjectionSelfTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.ignite.internal.processors.resource;
+
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.resources.ServiceContextResource;
+import org.apache.ignite.services.Service;
+import org.apache.ignite.services.ServiceContext;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+/**
+ * Tests for injected service context.
+ */
+public class GridServiceContextInjectionSelfTest extends GridCommonAbstractTest {
+    /** Service name. */
+    private static final String DUMMY_SERVICE = "dummy";
+
+    /** Compatibiity service name. */
+    private static final String COMPATIBILITY_SERVICE = "compatibility";
+
+    /** Error handler. */
+    private static final ErrorHandler errHnd = new ErrorHandler();
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        startGrids(2);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        grid(0).context().service().cancelAll();
+
+        errHnd.reset();
+    }
+
+    /**
+     * Checks that the context is correctly injected into the service.
+     */
+    @Test
+    public void testInjection() {
+        grid(0).services().deployClusterSingleton(DUMMY_SERVICE, new DummyServiceImpl());
+
+        doLifeCycleTest(DUMMY_SERVICE);
+    }
+
+    /**
+     * Ensures that the injected context instance is the same as obtained in legacy methods.
+     */
+    @Test
+    public void testCompatibility() {
+        grid(0).services().deployClusterSingleton(COMPATIBILITY_SERVICE, new CompatibilityServiceImpl());
+
+        doLifeCycleTest(COMPATIBILITY_SERVICE);
+    }
+
+    /**
+     * @param svcName Service name.
+     */
+    private void doLifeCycleTest(String svcName) {
+        DummyService svc = grid(1).services().serviceProxy(svcName, DummyService.class, false);
+
+        svc.method();
+
+        grid(0).services().cancel(svcName);
+
+        errHnd.validate();
+    }
+
+    /**
+     * Dummy service.
+     */
+    public interface DummyService {
+        /** */
+        public void method();
+    }
+
+    /** */
+    public static class DummyServiceImpl implements DummyService, Service {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** Service context. */
+        @ServiceContextResource
+        private ServiceContext ctxField;
+
+        /** Service context. */
+        private ServiceContext ctxSetter;
+
+        /** Service initialized flag. */
+        private volatile boolean initialized;
+
+        /** Service executed flag. */
+        private volatile boolean executed;
+
+        /** Service canceled flag. */
+        private volatile boolean canceled;
+
+        /**
+         * @param ctx Service context.
+         */
+        @ServiceContextResource
+        private void serviceContext(ServiceContext ctx) {
+            ctxSetter = ctx;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void init() {
+            errHnd.ensure(ctxSetter != null);
+            errHnd.ensure(ctxSetter == ctxField);
+
+            errHnd.ensure(!initialized);
+            errHnd.ensure(!executed);
+            errHnd.ensure(!canceled);
+
+            initialized = true;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void execute() {
+            errHnd.ensure(ctxSetter != null);
+            errHnd.ensure(ctxSetter == ctxField);
+
+            errHnd.ensure(initialized);
+            errHnd.ensure(!executed);
+            errHnd.ensure(!canceled);
+
+            executed = true;
+
+            try {
+                GridTestUtils.waitForCondition(() -> ctxSetter.isCancelled(), GridTestUtils.DFLT_TEST_TIMEOUT);
+            } catch (IgniteInterruptedCheckedException ignore) {
+                // Execution interrupted when service is cancelled.
+            }
+
+            errHnd.ensure(ctxSetter.isCancelled());
+            errHnd.ensure(canceled);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void method() {
+            assertTrue(initialized);
+            assertTrue(executed);
+            assertFalse(canceled);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void cancel() {
+            errHnd.ensure(ctxSetter != null);
+            errHnd.ensure(ctxSetter == ctxField);
+
+            errHnd.ensure(initialized);
+            errHnd.ensure(executed);
+            errHnd.ensure(!canceled);
+
+            canceled = true;
+        }
+    }
+
+    /**
+     * Service for checking compatibility of the injected service context.
+     */
+    public static class CompatibilityServiceImpl implements DummyService, Service {
+        /** */
+        private static final long serialVersionUID = 0L;
+
+        /** Service context. */
+        @ServiceContextResource
+        private ServiceContext ctx;
+
+        /** {@inheritDoc} */
+        @Override public void cancel(ServiceContext ctx) {
+            errHnd.ensure(ctx == this.ctx);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void init(ServiceContext ctx) {
+            errHnd.ensure(ctx == this.ctx);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void execute(ServiceContext ctx) {
+            errHnd.ensure(ctx == this.ctx);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void method() {
+            // No-op.
+        }
+    }
+
+    /** */
+    private static class ErrorHandler {
+        /** Error reference. */
+        private final AtomicReference<AssertionError> errRef = new AtomicReference<>();
+
+        /**
+         * @param condition Assertion condition to check.
+         */
+        void ensure(boolean condition) {
+            if (!condition) {
+                AssertionError err = new AssertionError();
+
+                errRef.compareAndSet(null, err);
+
+                throw err;
+            }
+        }
+
+        /**
+         * @throws AssertionError If there was an error.
+         */
+        void validate() throws AssertionError {
+            Error err = errRef.get();
+
+            if (errRef.get() != null)
+                throw err;
+        }
+
+        /** */
+        void reset() {
+            errRef.set(null);
+        }
+    }
+}
diff --git a/modules/spring/src/test/java/org/apache/ignite/testsuites/IgniteResourceSelfTestSuite.java b/modules/spring/src/test/java/org/apache/ignite/testsuites/IgniteResourceSelfTestSuite.java
index 0a9f82d..382a8f2 100644
--- a/modules/spring/src/test/java/org/apache/ignite/testsuites/IgniteResourceSelfTestSuite.java
+++ b/modules/spring/src/test/java/org/apache/ignite/testsuites/IgniteResourceSelfTestSuite.java
@@ -19,6 +19,7 @@ package org.apache.ignite.testsuites;
 
 import org.apache.ignite.internal.processors.resource.GridLoggerInjectionSelfTest;
 import org.apache.ignite.internal.processors.resource.GridResourceProcessorSelfTest;
+import org.apache.ignite.internal.processors.resource.GridServiceContextInjectionSelfTest;
 import org.apache.ignite.internal.processors.resource.GridServiceInjectionSelfTest;
 import org.apache.ignite.internal.processors.resource.GridSpringResourceInjectionSelfTest;
 import org.junit.runner.RunWith;
@@ -33,6 +34,7 @@ import org.junit.runners.Suite;
     GridLoggerInjectionSelfTest.class,
     GridServiceInjectionSelfTest.class,
     GridSpringResourceInjectionSelfTest.class,
+    GridServiceContextInjectionSelfTest.class,
 })
 public class IgniteResourceSelfTestSuite {
 }