23

I'm trying to implement a ContainerRequestFilter that does custom validation of a request's parameters. I need to look up the resource method that will be matched to the URI so that I can scrape custom annotations from the method's parameters.

Based on this answer I should be able to inject ExtendedUriInfo and then use it to match the method:

public final class MyRequestFilter implements ContainerRequestFilter {

    @Context private ExtendedUriInfo uriInfo;

    @Override
    public ContainerRequest filter(ContainerRequest containerRequest) {

        System.out.println(uriInfo.getMatchedMethod());

        return containerRequest;
    }
}

But getMatchedMethod apparently returns null, all the way up until the method is actually invoked (at which point it's too late for me to do validation).

How can I retrieve the Method that will be matched to a given URI, before the resource method is invoked?


For those interested, I'm trying to roll my own required parameter validation, as described in JERSEY-351.

Community
  • 1
  • 1
Paul Bellora
  • 54,340
  • 18
  • 130
  • 181
  • Does this have to be a Jersey-only solution or would you consider using AOP of some kind? – condit May 03 '13 at 17:15
  • @condit I'm currently exploring using AOP with Guice. I can "accept" any solution, but I'd like to award the bounty to a Jersey-only one, even if it's hackish. – Paul Bellora May 03 '13 at 18:02
  • Are you looking for a Jersey 1.x or 2.x solution? – condit May 03 '13 at 23:18
  • @condit I'm confined to the latest Jersey 1.x, specifically 1.17. If I understand correctly 2.x isn't production-ready yet (esp. since JAX-RS 2.0 isn't finalized). Out of curiosity, do you know if Jersey 2.0 does/will do the kind of parameter validation I'm looking for? – Paul Bellora May 04 '13 at 00:23
  • Yes - 2.0 is not production ready but JAX-RS 2.0 is supposed to support standard bean validation. – condit May 04 '13 at 01:34
  • @PaulBellora - `ExtendedURIInfo` will have no matched method data populated for requests because the matching hasn't occurred at that point yet (it ***is*** populated for responses). Theoretically you would utilize a [`ResourceFilter`](http://jersey.java.net/nonav/apidocs/1.17/jersey/com/sun/jersey/spi/container/ResourceFilter.html) instead, but that still doesn't give you access to the actual method parameters. If you are tied to Jersey then you will either need to create a custom reader, or throw a third party library in to the mix. – Perception May 05 '13 at 13:43
  • If you're not tied to Jersey then I would recommend RESTEasy, which integrates JSR303 right out the box (which will transition naturally to JAX-RS 2.0 when its finalized). – Perception May 05 '13 at 13:43
  • @Perception That makes sense, thanks for your comments. It's unfortunate that there's no way to do filtering between the method being matched and its invocation. It doesn't seem like that obscure of a use case for a filter to want to know about the method the request is going to be matched to. – Paul Bellora May 05 '13 at 15:32

4 Answers4

22

Actually, you should try to inject ResourceInfo into your custom request filter. I have tried it with RESTEasy and it works there. The advantage is that you code against the JSR interfaces and not the Jersey implementation.

public class MyFilter implements ContainerRequestFilter
{
    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext)
            throws IOException
    {
        Method theMethod = resourceInfo.getResourceMethod();
        return;
    }
}
  • 6
    [`ResourceInfo`](http://docs.oracle.com/javaee/7/api/javax/ws/rs/container/ResourceInfo.html) class is part of jax-rs 2.0. The above woudn't work with Jersey 1.x – botchniaque Feb 02 '15 at 14:31
  • 2
    I tried this in my AuthenticationFilter (which implements ContainerRequestFilter), but I keep getting `null` for getResourceMethod(). Is this only possible for non-PreMatching providers? – Johanneke Aug 11 '15 at 10:59
  • Worked fine to my case. I needed checked if an specific annotation was present from resource method. I used this: Annotation securityCheckedAnnotation = resourceInfo.getResourceMethod().getDeclaredAnnotation(SecurityChecked.class); – Marcelo Rebouças Jun 28 '18 at 13:48
16

I figured out how to solve my problem using only Jersey. There's apparently no way to match a request's URI to the method that will be matched before that method is invoked, at least in Jersey 1.x. However, I was able to use a ResourceFilterFactory to create a ResourceFilter for each individual resource method - that way these filters can know about the destination method ahead of time.

Here's my solution, including the validation for required query params (uses Guava and JSR 305):

public final class ValidationFilterFactory implements ResourceFilterFactory {

    @Override
    public List<ResourceFilter> create(AbstractMethod abstractMethod) {

        //keep track of required query param names
        final ImmutableSet.Builder<String> requiredQueryParamsBuilder =
                ImmutableSet.builder();

        //get the list of params from the resource method
        final ImmutableList<Parameter> params =
                Invokable.from(abstractMethod.getMethod()).getParameters();

        for (Parameter param : params) {
            //if the param isn't marked as @Nullable,
            if (!param.isAnnotationPresent(Nullable.class)) {
                //try getting the @QueryParam value
                @Nullable final QueryParam queryParam =
                        param.getAnnotation(QueryParam.class);
                //if it's present, add its value to the set
                if (queryParam != null) {
                    requiredQueryParamsBuilder.add(queryParam.value());
                }
            }
        }

        //return the new validation filter for this resource method
        return Collections.<ResourceFilter>singletonList(
                new ValidationFilter(requiredQueryParamsBuilder.build())
        );
    }

    private static final class ValidationFilter implements ResourceFilter {

        final ImmutableSet<String> requiredQueryParams;

        private ValidationFilter(ImmutableSet<String> requiredQueryParams) {
            this.requiredQueryParams = requiredQueryParams;
        }

        @Override
        public ContainerRequestFilter getRequestFilter() {
            return new ContainerRequestFilter() {
                @Override
                public ContainerRequest filter(ContainerRequest request) {

                    final Collection<String> missingRequiredParams =
                            Sets.difference(
                                    requiredQueryParams,
                                    request.getQueryParameters().keySet()
                            );

                    if (!missingRequiredParams.isEmpty()) {

                        final String message =
                                "Required query params missing: " +
                                Joiner.on(", ").join(missingRequiredParams);

                        final Response response = Response
                                .status(Status.BAD_REQUEST)
                                .entity(message)
                                .build();

                        throw new WebApplicationException(response);
                    }

                    return request;
                }
            };
        }

        @Override
        public ContainerResponseFilter getResponseFilter() {
            return null;
        }
    }
}

And the ResourceFilterFactory is registered with Jersey as an init param of the servlet in web.xml:

<init-param>
    <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
    <param-value>my.package.name.ValidationFilterFactory</param-value>
</init-param>

At startup, ValidationFilterFactory.create gets called for each resource method detected by Jersey.

Credit goes to this post for getting me on the right track: How can I get resource annotations in a Jersey ContainerResponseFilter

Community
  • 1
  • 1
Paul Bellora
  • 54,340
  • 18
  • 130
  • 181
  • You can also [register this filter in code as illustrated](https://stackoverflow.com/a/30824289/978264) using `environment.jersey().getResourceConfig().getResourceFilterFactories().add(new ValidationFilterFactory());` – Stokedout Aug 18 '20 at 14:56
3

I know you're looking for a Jersey only solution but here's a Guice approach that should get things working:

public class Config extends GuiceServletContextListener {

  @Override
  protected Injector getInjector() {
    return Guice.createInjector(
        new JerseyServletModule() {
          @Override
          protected void configureServlets() {
            bindInterceptor(Matchers.inSubpackage("org.example"), Matchers.any(), new ValidationInterceptor());
            bind(Service.class);

            Map<String, String> params = Maps.newHashMap();
            params.put(PackagesResourceConfig.PROPERTY_PACKAGES, "org.example");
            serve("/*").with(GuiceContainer.class, params);
          }
        });
  }

  public static class ValidationInterceptor implements MethodInterceptor {    
    public Object invoke(MethodInvocation method) throws Throwable {
      System.out.println("Validating: " + method.getMethod());
      return method.proceed();
    }
  }

}
@Path("/")
public class Service {

  @GET
  @Path("service")
  @Produces({MediaType.TEXT_PLAIN})
  public String service(@QueryParam("name") String name) {
    return "Service " + name;
  }

}

EDIT: A performance comparison:

public class AopPerformanceTest {

  @Test
  public void testAopPerformance() {
    Service service = Guice.createInjector(
        new AbstractModule() {
          @Override
          protected void configure() { bindInterceptor(Matchers.inSubpackage("org.example"), Matchers.any(), new ValidationInterceptor()); }
        }).getInstance(Service.class);
    System.out.println("Total time with AOP: " + timeService(service) + "ns");
  }

  @Test
  public void testNonAopPerformance() {
    System.out.println("Total time without AOP: " + timeService(new Service()) + "ns");
  }

  public long timeService(Service service) {
    long sum = 0L;
    long iterations = 1000000L;
    for (int i = 0; i < iterations; i++) {
      long start = System.nanoTime();
      service.service(null);
      sum += (System.nanoTime() - start);
    }
    return sum / iterations;
  }

}
condit
  • 10,852
  • 2
  • 41
  • 60
  • Hmm, I seem to be running into some serious performance penalties by using Guice AOP. Do you know how much this is to be expected and whether there is Guice documentation addressing it? My searches haven't turned up many results specific to Guice AOP performance. – Paul Bellora May 06 '13 at 18:36
  • I'm not aware of such an issue. I added a performance test to the answer. On my localhost I get: `Total time with AOP: 109ns Total time without AOP: 31ns`. Are you certain it's not the code running in the interceptor that's causing the issue? – condit May 06 '13 at 20:56
  • I ruled out the interceptor - it's actually the *body* of the intercepted method that runs slower with interception. I'm seeing an average of about 600 millis of that method body alone, where about 90 millis is normal. However, I tried it with a different resource method that is essentially empty and saw no significant added cost. I think the key difference might be that the resource method I choose to test with is calling other methods in the same class. Just guessing, but since Guice AOP works by dynamically subclassing, this might be part of it. Haven't had time to experiment further. – Paul Bellora May 07 '13 at 02:03
  • You could try changing `bindInterceptor(Matchers.inSubpackage("org.example"), Matchers.any(), new ValidationInterceptor());` to match an annotation that you supply instead of matching all methods in the package. If all the methods are in the same package they could be slowed down by the validation code. – condit May 07 '13 at 02:07
  • That is in fact what I'm doing already :( I'm going to try inlining everything in the resource method to see if my theory pans out - I'll let you know how it goes. – Paul Bellora May 07 '13 at 02:17
  • I figured out what the issue was. I was using `@PostConstruct`- and `@PreDestroy`-annotated methods to set up/tear down a thread-local caching service for resource methods. These were no longer getting called once I bound the resource using Guice. I'll just need to switch over to whatever Guice-equivalent there is to do that sort of thing. – Paul Bellora May 07 '13 at 15:58
  • I realized how to solve this without Guice - see my answer. Still, thanks for all the help with your proof of concept. It was helpful to check my work against, and I'll probably use Guice in the future. – Paul Bellora May 08 '13 at 17:35
0

In resteasy-jaxrs-3.0.5, you can retrieve a ResourceMethodInvoker representing the matched resource method from ContainerRequestContext.getProperty() inside a ContainerRequestFilter:

   import org.jboss.resteasy.core.ResourceMethodInvoker;

   public class MyRequestFilter implements ContainerRequestFilter
   {
       public void filter(ContainerRequestContext request) throws IOException
       {
            String propName = "org.jboss.resteasy.core.ResourceMethodInvoker";
            ResourceMethodInvoker invoker = (ResourceMethodInvoker)request.getProperty();
            invoker.getMethod().getParameterTypes()....
       }
   }
dthorpe
  • 35,318
  • 5
  • 75
  • 119