eclipse-ee4j / jersey

Eclipse Jersey Project - Read our Wiki:
https://github.com/eclipse-ee4j/jersey/wiki
Other
692 stars 355 forks source link

Jersey 2 with guice bind is not able to bind resources #5357

Open virajjasani opened 1 year ago

virajjasani commented 1 year ago

As part of migrating Jersey from 1.19.4 to 2.39.1, we are facing some issues with guice bind.

Here is the draft PR: https://github.com/apache/hadoop/pull/5768

All updated dependencies are defined here: https://github.com/apache/hadoop/pull/5768/files#diff-df2aa66a3757d73c93849b3a279d42a4634b0aed3550cdd2ead32d2fa06bf49e (hadoop-project/pom.xml)

We have a test TestWebApp https://github.com/apache/hadoop/blob/trunk/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/TestWebApp.java#L245

  @Test
  void testCustomRoutes() throws Exception {
    WebApp app =
        WebApps.$for("test", TestWebApp.class, this, "ws").start(new WebApp() {
          @Override
          public void setup() {
            bind(MyTestJAXBContextResolver.class);
            bind(MyTestWebService.class);

            route("/:foo", FooController.class);
            route("/bar/foo", FooController.class, "bar");
            route("/foo/:foo", DefaultController.class);
            route("/foo/bar/:foo", DefaultController.class, "index");
          }
        });
    String baseUrl = baseUrl(app);
    try {
      assertEquals("foo", getContent(baseUrl).trim());
      assertEquals("foo", getContent(baseUrl + "test").trim());
      assertEquals("foo1", getContent(baseUrl + "test/1").trim());
      assertEquals("bar", getContent(baseUrl + "test/bar/foo").trim());
      assertEquals("default", getContent(baseUrl + "test/foo/bar").trim());
      assertEquals("default1", getContent(baseUrl + "test/foo/1").trim());
      assertEquals("default2", getContent(baseUrl + "test/foo/bar/2").trim());
      assertEquals(404, getResponseCode(baseUrl + "test/goo"));
      assertEquals(200, getResponseCode(baseUrl + "ws/v1/test"));
      assertTrue(getContent(baseUrl + "ws/v1/test").contains("myInfo"));
    } finally {
      app.stop();
    }
  }

The idea here is to ensure that custom routes are served with the above mentioned endpoints.

Moreover, MyTestJAXBContextResolver and MyTestWebService are used to bind requests starting with "ws/v1/test".

The changes done for MyTestJAXBContextResolver: https://github.com/apache/hadoop/pull/5768/files#diff-52936fdf3657b9acf3cffa439cf5c0b71737d51d8d473329843e66ca462aa306

and MyTestWebService (without changes): https://github.com/apache/hadoop/blob/trunk/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/MyTestWebService.java

Since guice bind is not able to bind the classes, getResponseCode(baseUrl + "ws/v1/test") always results into 404.

i have also tried adding the classes/packages with ResourceConfig but it is only able to instantiate MyTestJAXBContextResolver, it is still not able to redirect ws/v1/test GET to MyTestWebService.

diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/MyTestWebService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/MyTestWebService.java
index 1d0a01ea53d..553389feca4 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/MyTestWebService.java
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/MyTestWebService.java
@@ -31,7 +31,7 @@
 import org.apache.hadoop.http.JettyUtils;

 @Singleton
-@Path("/ws/v1/test")
+@Path("")
 public class MyTestWebService {
   @GET
   @Produces({ MediaType.APPLICATION_XML + "; " + JettyUtils.UTF_8 })
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/TestWebApp.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/TestWebApp.java
index 7d7a1575b47..cadf0e720ae 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/TestWebApp.java
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/webapp/TestWebApp.java
@@ -22,14 +22,17 @@
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLEncoder;
+import java.util.HashMap;

 import com.google.inject.Inject;
+import org.glassfish.jersey.server.ResourceConfig;
 import org.junit.jupiter.api.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;

 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.http.HttpServer2;
 import org.apache.hadoop.net.ServerSocketUtil;
 import org.apache.hadoop.yarn.MockApps;
 import org.apache.hadoop.yarn.webapp.view.HtmlPage;
@@ -256,6 +259,16 @@ public void setup() {
             route("/foo/:foo", DefaultController.class);
             route("/foo/bar/:foo", DefaultController.class, "index");
           }
+
+          @Override
+          public void addJerseyResourceConfigs(HttpServer2 server) {
+            ResourceConfig resourceConfig = new ResourceConfig();
+            resourceConfig.packages(MyTestWebService.class.getPackage().getName() + ";"
+                + MyTestJAXBContextResolver.class.getPackage().getName());
+            resourceConfig.register(MyTestJAXBContextResolver.class);
+            resourceConfig.register(MyTestWebService.class);
+            server.addJerseyResourceConfig(resourceConfig, "/ws/v1/test", new HashMap<>());
+          }
         });
     String baseUrl = baseUrl(app);
     try {

guice-hk2 is in the classpath. the test belongs to hadoop-yarn-common module.

Edit: additional reference: we create embedded Jetty server to server http requests in HttpServer2 Changes for the class in the PR: https://github.com/apache/hadoop/pull/5768/files#diff-4e9d7dccc4530205e71b54fe7f967135aeca170cff5ace98b5b7f04304153813

jansupol commented 1 year ago

Currently, I am not aware of any projects that would use guice. The support should be handled by the guice-hk2 bridge as you mentioned, so perhaps it is more a question for the HK2 project. But maybe someone from the community has an experience with guice and will be able to help.

virajjasani commented 1 year ago

Thank you @jansupol, appreciate your comment. i have also tried using ResourceConfig as per the patch i mentioned above:

+
+          @Override
+          public void addJerseyResourceConfigs(HttpServer2 server) {
+            ResourceConfig resourceConfig = new ResourceConfig();
+            resourceConfig.packages(MyTestWebService.class.getPackage().getName() + ";"
+                + MyTestJAXBContextResolver.class.getPackage().getName());
+            resourceConfig.register(MyTestJAXBContextResolver.class);
+            resourceConfig.register(MyTestWebService.class);
+            server.addJerseyResourceConfig(resourceConfig, "/ws/v1/test", new HashMap<>());
+          }
         });

however, it only helps instantiating MyTestJAXBContextResolver (by jersey-hk2), which is ContextResolver for MyTestWebService. MyTestWebService is not executed when http call to /ws/v1/test is made.

virajjasani commented 1 year ago

i am also curious why ContextResolver#getContext is not getting called.

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.inject.Singleton;

import org.apache.hadoop.yarn.webapp.resource.MyTestWebService.MyInfo;

@Singleton
@Provider
public class MyTestObjectWriterContextResolver implements ContextResolver<ObjectWriter> {

  private ObjectWriter context;
  private final Set<Class> types;

  // you have to specify all the dao classes here
  private final Class[] cTypes = { MyInfo.class };

  public MyTestObjectWriterContextResolver() {
    this.types = new HashSet<>(Arrays.asList(cTypes));
    this.context = new ObjectMapper().writerFor(MyInfo.class);
  }

  @Override
  public ObjectWriter getContext(Class<?> objectType) {
    return (types.contains(objectType)) ? context : null;
  }
}
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;

import org.apache.hadoop.http.JettyUtils;

@Path("")
public class MyTestWebService {
  @GET
  @Produces({ MediaType.APPLICATION_XML + "; " + JettyUtils.UTF_8 })
  public MyInfo get() {
    return new MyInfo();
  }

  @XmlRootElement(name = "myInfo")
  @XmlAccessorType(XmlAccessType.FIELD)
  public static class MyInfo {
    public MyInfo() {

    }
  }
}
            ResourceConfig resourceConfig = new ResourceConfig();
            resourceConfig.register(MyTestObjectWriterContextResolver.class);
            resourceConfig.register(MyTestWebService.class);
            server.addJerseyResourceConfig(resourceConfig, "/ws1/v1/test/*", new HashMap<>());

the implementation of addJerseyResourceConfig:

  protected final WebAppContext webAppContext;
...
...
...

  /**
   * Add a Jersey resource config.
   * @param config The Jersey ResourceConfig to be registered.
   * @param pathSpec The path spec for the servlet
   * @param params properties and features for ResourceConfig
   */
  public void addJerseyResourceConfig(final ResourceConfig config,
      final String pathSpec, Map<String, String> params) {
    LOG.info("addJerseryResourceConfig: pathSpec={}", pathSpec);
    final ServletHolder sh = new ServletHolder(new ServletContainer(config));
    for (Map.Entry<String, String> entry : params.entrySet()) {
      sh.setInitParameter(entry.getKey(), entry.getValue());
    }
    webAppContext.addServlet(sh, pathSpec);
  }

Here, when GET endpoint ws1/v1/test is called, somehow MyTestObjectWriterContextResolver#getContext is not even getting executed.

jansupol commented 1 year ago

ContextResolver is not invoked by default; you need something like

@Context
Providers providers;

public ObjectWriter getObjectWriter(Class<?> objectType) {
   ContextResolver<ObjectWriter> ctx = providers.getContextResolver(ObjectWriter.class, MediaType.APPLICATION_XML_TYPE);
   return ctx.getContext(objectType);
}

in your provider.

jansupol commented 1 year ago

For the record, here is the documentation for HK2 - Guice bridge.