Есть ли способ автоматически распространять входящий HTTP-заголовок в запросе JAX-RS на исходящий запрос JAX-RS?

Я ищу подходящий способ: в приложении Джерси, чтобы прочитать заголовок из входящего запроса и автоматически установить его в любые исходящие запросы, которые могут быть сделаны клиентом JAX-RS, используемым моим приложением.

В идеале я бы хотел сделать это, не загрязняя любую внутреннюю логику моих классов, поэтому с помощью различных фильтров и перехватчиков.

В простых случаях я могу это сделать: у меня есть реализация ClientRequestFilter, которую я регистрирую на своем ClientBuilder, и эта реализация фильтра имеет:

@Context
private HttpHeaders headers;

... который является контекстно-зависимым прокси (по определению), поэтому в своем методе filter он может ссылаться на заголовки, которые присутствовали во входящем запросе, которые управляют всем этим, и устанавливать их на исходящий запрос. Для простых случаев это работает нормально.

Однако это не выполняется в случае асинхронности: если я использую асинхронные клиентские API JAX-RS для создания пучки GET s, фильтр все равно вызывается, но больше не может вызывать методы на этом headers переменная экземпляра; Джерси жалуется, что, насколько он знает, мы больше не занимаемся поиском. Это имеет смысл, если область запроса определена как потоковая: порожденная GET работает где-то в некотором управляемом Джерси потоке, а не в том же потоке, что и прокси-сервер headers, так что proxy выбрасывает IllegalStateException повсюду, когда мой фильтр пытается поговорить с ним.

Я чувствую, что есть некоторая комбинация ContainerRequestFilter и ClientRequestFilter, которая должна быть в состоянии выполнить работу даже в асинхронных случаях, но я ее не вижу.

Ответ 1

Что бы я сделал, сделайте инъекцию WebTarget, предварительно настроенную с помощью ClientRequestFilter, чтобы добавить заголовки. Лучше настроить WebTarget таким образом, в отличие от Client, поскольку Client - это дорогостоящий объект для создания.

Мы можем сделать инъекцию WebTarget с помощью пользовательской аннотации и InjectionResolver. В InjectionResolver мы можем получить ContainerRequest и получить заголовки от них, которые мы перейдем к ClientRequestFilter.

Здесь он находится в действии

Создать пользовательскую аннотацию

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

Сделайте InjectionResolver с пользовательским ClientRequestFilter

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

Одна вещь об этой реализации заключается в том, что она проверяет пустой headerNames в аннотации @WithHeadersTarget. Если он пуст, мы просто пересылаем все заголовки. Если пользователь указывает некоторые имена заголовков, он будет перенаправлять только те

Зарегистрируйте InjectionResolver

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

Используйте его

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

В этом примере, если headerNames отсутствует, то по умолчанию будет пустым массивом, который приведет к переадресации всех заголовков запросов.

Полное тестирование с использованием тестовой платформы для Джерси

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