有没有办法自动将JAX-RS请求中的传入HTTP头传播到传出的JAX-RS请求?

Lai*_*son 9 jax-rs jersey

我在Jersey应用程序中寻找正确的方法 - 从传入的请求中读取标头并自动将其安装在可能由我的应用程序正在使用的JAX-RS客户端发出的任何传出请求中.

理想情况下,我想这样做而不会污染任何类的内部逻辑,所以通过各种过滤器和拦截器.

对于简单的用例,我可以这样做:我有一个ClientRequestFilter我在我的注册实现ClientBuilder,并且该过滤器实现具有:

@Context
private HttpHeaders headers;
Run Code Online (Sandbox Code Playgroud)

...这是一个上下文相关的代理(根据定义),所以在它的filter方法中它可以引用入站请求中存在的标题,它驱动所有这些,并将它们安装在传出请求上.对于简单的情况,这似乎工作正常.

但是,在异步性的情况下,这会失败:如果我使用JAX-RS异步客户端API来生成一堆GETs,则仍然会调用过滤器,但是不能再调用该headers实例变量上的方法; 泽西岛抱怨说,据我们所知,我们不再处于要求范围内.如果将请求范围定义为每个线程,则这是有意义的:生成的GETs在某个Jersey管理的线程池中运行,而不是与headers代理关联的线程在同一个线程上运行,因此代理将IllegalStateException全部抛出我的过滤器尝试与之交谈的地方.

我觉得有一些组合ContainerRequestFilter,并ClientRequestFilter应能够得到即使是在异步的情况下所做的工作,但我没有看到它.

Pau*_*tha 4

我要做的是制作一个WebTarget预先配置的可注入文件ClientRequestFilter以添加标头。最好以WebTarget这种方式配置,而不是Client,因为Client创建对象的成本很高。

我们可以WebTarget使用自定义注释和InjectionResolver. 在 中InjectionResolver,我们可以获取ContainerRequest并从中获取标头,我们将其传递给ClientRequestFilter.

这是在行动

创建自定义注释

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WithHeadersTarget {
    String baseUri();
    String[] headerNames() default {};
}
Run Code Online (Sandbox Code Playgroud)

使用自定义ClientRequestFilter制作InjectionResolver

private static class WithHeadersTargetInjectionResolver
        implements InjectionResolver<WithHeadersTarget> {

    private final Provider<ContainerRequest> requestProvider;
    private final Client client;

    @Inject
    public WithHeadersTargetInjectionResolver(Provider<ContainerRequest> requestProvider) {
        this.requestProvider = requestProvider;
        this.client = ClientBuilder.newClient();
    }

    @Override
    public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
        if (injectee.getRequiredType() == WebTarget.class
                && injectee.getParent().isAnnotationPresent(WithHeadersTarget.class)) {
            WithHeadersTarget anno = injectee.getParent().getAnnotation(WithHeadersTarget.class);
            String uri = anno.baseUri();
            String[] headersNames = anno.headerNames();
            MultivaluedMap<String, String> requestHeaders = requestProvider.get().getRequestHeaders();

            return client.target(uri)
                    .register(new HeadersFilter(requestHeaders, headersNames));
        }
        return null;
    }

    @Override
    public boolean isConstructorParameterIndicator() {
        return false;
    }

    @Override
    public boolean isMethodParameterIndicator() {
        return false;
    }

    private class HeadersFilter implements ClientRequestFilter {

        private final MultivaluedMap<String, String> headers;
        private final String[] headerNames;

        private HeadersFilter(MultivaluedMap<String, String> headers, String[] headerNames) {
            this.headers = headers;
            this.headerNames = headerNames;
        }

        @Override
        public void filter(ClientRequestContext requestContext) throws IOException {
            // if headers names is empty, add all headers
            if (this.headerNames.length == 0) {
                for (Map.Entry<String, List<String>> entry: this.headers.entrySet()) {
                    requestContext.getHeaders().put(entry.getKey(), new ArrayList<>(entry.getValue()));
                }
            // else just add the headers from the annotation
            } else {
                for (String header: this.headerNames) {
                    requestContext.getHeaders().put(header, new ArrayList<>(this.headers.get(header)));
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

此实现的一件事是它检查注释headerNames中是否为空@WithHeadersTarget。如果它为空,那么我们只转发所有标头。如果用户指定了一些标头名称,那么它只会转发这些标头名称

注册InjectionResolver

new ResourceConfig()
   .register(new AbstractBinder() {
        @Override
        protected void configure() {
            bind(WithHeadersTargetInjectionResolver.class)
                  .to(new TypeLiteral<InjectionResolver<WithHeadersTarget>>() {
                   }).in(Singleton.class);
            }
        })
Run Code Online (Sandbox Code Playgroud)

用它

@Path("test")
public static class TestResource {

    @WithHeadersTarget(
            baseUri = BASE_URI
            headerNames = {TEST_HEADER_NAME})
    private WebTarget target;

    @GET
    public String get() {
        return target.path("client").request().get(String.class);
    }
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,如果headerNames省略,则它将默认为空数组,这将导致所有请求标头被转发。

使用 Jersey Test Framework 完成测试

import org.glassfish.hk2.api.Injectee;
import org.glassfish.hk2.api.InjectionResolver;
import org.glassfish.hk2.api.ServiceHandle;
import org.glassfish.hk2.api.TypeLiteral;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import static org.assertj.core.api.Assertions.assertThat;

public class ForwardHeadersTest extends JerseyTest {

    private static final String BASE_URI = "http://localhost:8000";
    private static final String TEST_HEADER_NAME = "X-Test-Header";

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WithHeadersTarget {
        String baseUri();
        String[] headerNames() default {};
    }

    @Path("test")
    public static class TestResource {

        @WithHeadersTarget(
                baseUri = BASE_URI
                )
        private WebTarget target;

        @GET
        public String get() {
            return target.path("client").request().get(String.class);
        }
    }

    @Path("client")
    public static class ClientResource {

        @GET
        public String getReversedHeader(@HeaderParam(TEST_HEADER_NAME) String header) {
            System.out.println(header);
            return new StringBuilder(header).reverse().toString();
        }
    }

    private static class WithHeadersTargetInjectionResolver
            implements InjectionResolver<WithHeadersTarget> {

        private final Provider<ContainerRequest> requestProvider;
        private final Client client;

        @Inject
        public WithHeadersTargetInjectionResolver(Provider<ContainerRequest> requestProvider) {
            this.requestProvider = requestProvider;
            this.client = ClientBuilder.newClient();
        }

        @Override
        public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
            if (injectee.getRequiredType() == WebTarget.class
                    && injectee.getParent().isAnnotationPresent(WithHeadersTarget.class)) {
                WithHeadersTarget anno = injectee.getParent().getAnnotation(WithHeadersTarget.class);
                String uri = anno.baseUri();
                String[] headersNames = anno.headerNames();
                MultivaluedMap<String, String> requestHeaders = requestProvider.get().getRequestHeaders();

                return client.target(uri)
                        .register(new HeadersFilter(requestHeaders, headersNames));
            }
            return null;
        }

        @Override
        public boolean isConstructorParameterIndicator() {
            return false;
        }

        @Override
        public boolean isMethodParameterIndicator() {
            return false;
        }

        private class HeadersFilter implements ClientRequestFilter {

            private final MultivaluedMap<String, String> headers;
            private final String[] headerNames;

            private HeadersFilter(MultivaluedMap<String, String> headers, String[] headerNames) {
                this.headers = headers;
                this.headerNames = headerNames;
            }

            @Override
            public void filter(ClientRequestContext requestContext) throws IOException {
                // if headers names is empty, add all headers
                if (this.headerNames.length == 0) {
                    for (Map.Entry<String, List<String>> entry: this.headers.entrySet()) {
                        requestContext.getHeaders().put(entry.getKey(), new ArrayList<>(entry.getValue()));
                    }
                // else just add the headers from the annotation
                } else {
                    for (String header: this.headerNames) {
                        requestContext.getHeaders().put(header, new ArrayList<>(this.headers.get(header)));
                    }
                }
            }
        }
    }

    @Override
    public ResourceConfig configure() {
        return new ResourceConfig()
                .register(TestResource.class)
                .register(ClientResource.class)
                .register(new AbstractBinder() {
                    @Override
                    protected void configure() {
                        bind(WithHeadersTargetInjectionResolver.class)
                                .to(new TypeLiteral<InjectionResolver<WithHeadersTarget>>() {
                                }).in(Singleton.class);
                    }
                })
                .register(new LoggingFilter(Logger.getAnonymousLogger(), true))
                .register(new ExceptionMapper<Throwable>() {
                    @Override
                    public Response toResponse(Throwable t) {
                        t.printStackTrace();
                        return Response.serverError().entity(t.getMessage()).build();
                    }
                });
    }

    @Override
    public URI getBaseUri() {
        return URI.create(BASE_URI);
    }

    @Test
    public void testIt() {
        final String response = target("test")
                .request()
                .header(TEST_HEADER_NAME, "HelloWorld")
                .get(String.class);

        assertThat(response).isEqualTo("dlroWolleH");
    }
}
Run Code Online (Sandbox Code Playgroud)