在 Jersey @Path 和 @ApplicationPath 中使用 Spring 属性 占位符

Using Spring property placeholders with Jersey @Path and @ApplicationPath

我在项目中使用 Jersey 和 Spring。 'jersey-spring3'用于它们之间的整合。我想让我的资源 类 更灵活,并在 @Path 注释中使用属性,例如:

@Path("${some.property}/abc/def")

但是Spring无法将some.property注入Jersey的注解@Path@ApplicationPath

有没有什么方法可以在 Jersey 资源的 @Path 值中包含一些可配置的(使用 属性 文件)值?

(我意识到用 Spring MVC 替换 Jersey 会更容易,但不幸的是,就我而言,我没有这个选择。)

所以这是一半的答案(或者可能是完整的答案,这取决于解决 @ApplicationPath 对您的重要性)。

要理解下面的解决方案,您应该首先了解一些 Jersey 的内部结构。当我们加载应用程序时,Jersey 会构建所有资源的模型。资源的所有信息都封装在这个模型中。 Jersey 使用这个模型来处理请求,而不是尝试在每个请求上处理资源,将有关资源的所有信息保存在模型中并处理模型会更快。

有了这个架构,Jersey 还允许我们 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 ModelProcessors。

ModelProcessor中,我们可以注入Spring的PropertyResolver,然后以编程方式解析占位符,并用解析后的路径替换旧的资源模型路径。例如

@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);
}

就资源模型 API 而言

  • 这是一个Resource

    @Path("resource")
    public class SomeResource {
        @GET
        public String get() {}
    }
    

    注解为@Path的资源方法为ResourceMethods

  • 这是上面ResourcechildResource,因为它被注解了@Path

    @GET
    @Path("child-resource")
    public String get() {}
    

此信息应该可以让您对上述实现的工作原理有所了解。

下面是一个完整的测试,使用 Jersey Test Framework。使用了以下类路径属性文件

app.properties

resource=resource
sub.resource=sub-resource
sub.resource.locator=sub-resource-locator

您可以像任何其他 JUnit 测试一样运行执行以下操作。

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 
 * 
 * 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();
    }
}

此外,现在我想到了,我可以看到一种使用 resolve @ApplicationPath 的方法,但它涉及创建 Jersey servlet 容器 以编程方式在 Spring WebAppInitializer 中。老实说,我认为这会比它的价值更麻烦。我只是接受它,并将 @ApplicationPath 保留为静态字符串。


UDPATE

如果你使用的是Spring引导,那么应用程序路径肯定是可以配置的,通过spring.jersey.applicationPath属性。 Spring 引导加载 Jersey 的方式几乎就是我在上一段中想到的想法,您自己在其中创建 Jersey servlet 容器,并设置 servlet 映射。这就是使用 Spring Boot.

进行配置的方式