I use Jersey and Spring in my project. 'jersey-spring3' is used for integration between them. I would like to make my resource classes more flexible and use properties inside @Path
annotations, like:
@Path("${some.property}/abc/def")
But Spring can't inject some.property
to Jersey's annotations @Path
and @ApplicationPath
.
Is there any way to have some configurable (using property files) value inside @Path
value of Jersey's resource?
(I realize that it would be easier to replace Jersey by Spring MVC, but in my case unfortunately I don't have this choice.)
So here's half the answer (or maybe a full answer depending on how important resolving @ApplicationPath
is to you).
To understand the below solution, you should first understand a little about Jersey's internals. When we load our application, Jersey builds a model of all the resources. All the information for the resource are encapsulated in this model. Jersey uses this model for processing requests, rather than try and process the resource on each request, it's quicker to keep all the information about the resource in a model, and process the model.
With this architecture, Jersey also allows us to build resources programmatically, using the same APIs that it uses internally to hold the model properties. Aside from just building resource models, we can also modify existing models, using a ModelProcessor
s.
In the ModelProcessor
, we can inject Spring's PropertyResolver
, and then programmatically resolve the placeholders, and replace the old resource model path, with the resolved one. For example
@Autowired
private PropertyResolver propertyResolver;
private ResourceModel processResourceModel(ResourceModel resourceModel) {
ResourceModel.Builder newResourceModelBuilder = new ResourceModel.Builder(false);
for (final Resource resource : resourceModel.getResources()) {
final Resource.Builder resourceBuilder = Resource.builder(resource);
String resolvedResourcePath = processPropertyPlaceholder(resource);
resourceBuilder.path(resolvedResourcePath);
// handle child resources
for (Resource childResource : resource.getChildResources()) {
String resolvedChildPath = processPropertyPlaceholder(childResource);
final Resource.Builder childResourceBuilder = Resource.builder(childResource);
childResourceBuilder.path(resolvedChildPath);
resourceBuilder.addChildResource(childResourceBuilder.build());
}
newResourceModelBuilder.addResource(resourceBuilder.build());
}
return newResourceModelBuilder.build();
}
private String processPropertyPlaceholder(Resource resource) {
String ogPath = resource.getPath();
return propertyResolver.resolvePlaceholders(ogPath);
}
As far as the resource model APIs are concerned
This is a Resource
@Path("resource")
public class SomeResource {
@GET
public String get() {}
}
Its resource methods that are not annotated with @Path
are ResourceMethod
s
This is a child Resource
of the above Resource
because it is annotated with @Path
.
@GET
@Path("child-resource")
public String get() {}
This information should give you some idea about how the above implementation works.
Below is a complete test, using Jersey Test Framework. The following classpath properties file is used
app.properties
resource=resource
sub.resource=sub-resource
sub.resource.locator=sub-resource-locator
You can run the following like any other JUnit test.
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.ModelProcessor;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.PropertyResolver;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
/**
* Stack Overflow http://stackoverflow.com/q/34943650/2587435
*
* Run it like any other JUnit test. Required dependencies are as follows:
*
* <dependency>
* <groupId>org.glassfish.jersey.test-framework.providers</groupId>
* <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
* <version>2.22.1</version>
* <scope>test</scope>
* </dependency>
* <dependency>
* <groupId>org.glassfish.jersey.ext</groupId>
* <artifactId>jersey-spring3</artifactId>
* <version>2.22.1</version>
* <scope>test</scope>
* </dependency>
* <dependency>
* <groupId>commons-logging</groupId>
* <artifactId>commons-logging</artifactId>
* <version>1.1</version>
* <scope>test</scope>
* </dependency>
*
* @author Paul Samsotha
*/
public class SpringPathResolverTest extends JerseyTest {
@Path("${resource}")
public static class TestResource {
@GET
public String get() {
return "Resource Success!";
}
@GET
@Path("${sub.resource}")
public String getSubMethod() {
return "Sub-Resource Success!";
}
@Path("${sub.resource.locator}")
public SubResourceLocator getSubResourceLocator() {
return new SubResourceLocator();
}
public static class SubResourceLocator {
@GET
public String get() {
return "Sub-Resource-Locator Success!";
}
}
}
@Configuration
@PropertySource("classpath:/app.properties")
public static class SpringConfig {
}
public static class PropertyPlaceholderPathResolvingModelProcessor
implements ModelProcessor {
@Autowired
private PropertyResolver propertyResolver;
@Override
public ResourceModel processResourceModel(ResourceModel resourceModel,
javax.ws.rs.core.Configuration configuration) {
return processResourceModel(resourceModel);
}
@Override
public ResourceModel processSubResource(ResourceModel subResourceModel,
javax.ws.rs.core.Configuration configuration) {
return subResourceModel;
}
private ResourceModel processResourceModel(ResourceModel resourceModel) {
ResourceModel.Builder newResourceModelBuilder = new ResourceModel.Builder(false);
for (final Resource resource : resourceModel.getResources()) {
final Resource.Builder resourceBuilder = Resource.builder(resource);
String resolvedResourcePath = processPropertyPlaceholder(resource);
resourceBuilder.path(resolvedResourcePath);
// handle child resources
for (Resource childResource : resource.getChildResources()) {
String resolvedChildPath = processPropertyPlaceholder(childResource);
final Resource.Builder childResourceBuilder = Resource.builder(childResource);
childResourceBuilder.path(resolvedChildPath);
resourceBuilder.addChildResource(childResourceBuilder.build());
}
newResourceModelBuilder.addResource(resourceBuilder.build());
}
return newResourceModelBuilder.build();
}
private String processPropertyPlaceholder(Resource resource) {
String ogPath = resource.getPath();
return propertyResolver.resolvePlaceholders(ogPath);
}
}
@Override
public ResourceConfig configure() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
return new ResourceConfig(TestResource.class)
.property("contextConfig", ctx)
.register(PropertyPlaceholderPathResolvingModelProcessor.class)
.register(new LoggingFilter(Logger.getAnonymousLogger(), true));
}
@Test
public void pathPlaceholderShouldBeResolved() {
Response response = target("resource").request().get();
assertThat(response.getStatus(), is(200));
assertThat(response.readEntity(String.class), is(equalTo("Resource Success!")));
response.close();
response = target("resource/sub-resource").request().get();
assertThat(response.getStatus(), is(200));
assertThat(response.readEntity(String.class), is(equalTo("Sub-Resource Success!")));
response.close();
response = target("resource/sub-resource-locator").request().get();
assertThat(response.getStatus(), is(200));
assertThat(response.readEntity(String.class), is(equalTo("Sub-Resource-Locator Success!")));
response.close();
}
}
Also now that I think about it, I can see a way to use resolve the @ApplicationPath
, but it involves creating the Jersey servlet container
programmatically in a Spring WebAppInitializer
. Honestly, I think it would be more trouble than it's worth. I'd just suck it up, and leave the @ApplicationPath
as a static string.
UDPATE
If you are using Spring boot, then the application path is definitely configurable, through the spring.jersey.applicationPath
property. The way Spring boot loads up Jersey is pretty much the idea I had in mind with the above paragraph, where you create the Jersey servlet container yourself, and set the servlet mapping. This is how it is configurable with Spring Boot.