Multiple endpoints with Resteasy

user504451 picture user504451 · Dec 19, 2013 · Viewed 7.5k times · Source

I have two separate handfuls of REST services in one application. Let's say a main "people" service and a secondary "management" service. What I want is to expose them in separate paths on the server. I am using JAX-RS, RESTEasy and Spring.

Example:

@Path("/people")
public interface PeopleService {
  // Stuff
}

@Path("/management")
public interface ManagementService {
  // Stuff
}

In web.xml I currently have the following set-up:

<listener>
    <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
</listener>

<listener>
    <listener-class>org.jboss.resteasy.plugins.spring.SpringContextLoaderListener</listener-class>
</listener>

<context-param>
    <param-name>resteasy.servlet.mapping.prefix</param-name>
    <param-value>/public</param-value>
</context-param>

<servlet>
    <servlet-name>Resteasy</servlet-name>
    <servlet-class>
        org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
    </servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Resteasy</servlet-name>
    <url-pattern>/public/*</url-pattern>
</servlet-mapping>

The PeopleService and ManagementService implementations are just Spring beans. Above web.xml configuration will expose them both on /public (so having /public/people and /public/management respectively).

What I want to accomplish is to expose the PeopleService on /public, so that the full path would become /public/people and expose the ManagementService on /internal, so that its full path would become /internal/management.

Unfortunately, I cannot change the value of the @Path annotation.

How should I do that?

Answer

Anton picture Anton · Aug 25, 2014

actually you can. After few hours of debugging I came up with this:

1) Declare multiple resteasy servlets in your web.xml (two in my case)

<servlet>
    <servlet-name>resteasy-servlet</servlet-name>
    <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
    <init-param>
        <param-name>resteasy.servlet.mapping.prefix</param-name>
        <param-value>/openrest</param-value>
    </init-param>       
    <init-param>
        <param-name>resteasy.resources</param-name>
        <param-value>com.mycompany.rest.PublicService</param-value>
    </init-param>
</servlet>

    <servlet>
    <servlet-name>private-resteasy-servlet</servlet-name>
    <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
    <init-param>
        <param-name>resteasy.servlet.mapping.prefix</param-name>
        <param-value>/protectedrest</param-value>
    </init-param>       
    <init-param>
        <param-name>resteasy.resources</param-name>
        <param-value>com.mycompany.rest.PrivateService</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>private-resteasy-servlet</servlet-name>
    <url-pattern>/protectedrest/*</url-pattern>
</servlet-mapping>      

<servlet-mapping>
    <servlet-name>resteasy-servlet</servlet-name>
    <url-pattern>/openrest/*</url-pattern>
</servlet-mapping>  

Please pay attention to the fact that we initialize personal resteasy.servlet.mapping.prefix and resteasy.resources for each our servlet. Please don't forget to NOT include any botstrap classes as filters or servlets! And disable autoscan as well.

2) Create a filter that cleans up application from the RESTeasy's global information that it saves in context:

public class ResteasyCleanupFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // TODO Auto-generated method stub

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        request.getServletContext().setAttribute(ResteasyProviderFactory.class.getName(), null);
        request.getServletContext().setAttribute(Dispatcher.class.getName(), null);
        chain.doFilter(request, response);


    }

    @Override
    public void destroy() {
        // TODO Auto-generated method stub

    }

}

Register it for any request to your services (here I used it for all requests for simplisity):

<filter>
    <filter-name>CleanupFilter</filter-name>
    <filter-class>com.mycompany.ResteasyCleanupFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CleanupFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping> 

Thats it!Now you have two different REST services which lays under different prefixes : /openrest which meant to service all public requests and /protectedrest that takes care about all the private stuff in the app.

So why does it work (or why it does not work otherwise)?

When you call openrest instance for the first time it tries to initalize itself and when done saves the state in the global servletContext like this :

 servletContext.setAttribute(ResteasyProviderFactory.class.getName(), deployment.getProviderFactory());
 servletContext.setAttribute(Dispatcher.class.getName(), deployment.getDispatcher());

And if you will let it be your call to your second /protectedrest will get the SAME configuration! That is why you need to clean up this information some where. That is why we used our CleanupFilter which empty the context so brand new rest servlet could initialize itself with all the init parameters we declared.

This is a hack, but it does the trick.

This solution was tested for RESTEasy 2.3.6

EDITED

Works with 3.0.9.final as well!