All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.http.client.netty.DefaultNettyHttpClientRegistry Maven / Gradle / Ivy

There is a newer version: 4.7.6
Show newest version
/*
 * Copyright 2017-2020 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.http.client.netty;

import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.context.annotation.Primary;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.FilterMatcher;
import io.micronaut.http.bind.DefaultRequestBinderRegistry;
import io.micronaut.http.bind.RequestBinderRegistry;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.body.MessageBodyReader;
import io.micronaut.http.body.MessageBodyWriter;
import io.micronaut.http.client.DefaultHttpClientConfiguration;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.HttpClientRegistry;
import io.micronaut.http.client.HttpVersionSelection;
import io.micronaut.http.client.LoadBalancer;
import io.micronaut.http.client.LoadBalancerResolver;
import io.micronaut.http.client.ProxyHttpClient;
import io.micronaut.http.client.ProxyHttpClientRegistry;
import io.micronaut.http.client.ServiceHttpClientConfiguration;
import io.micronaut.http.client.StreamingHttpClient;
import io.micronaut.http.client.StreamingHttpClientRegistry;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientException;
import io.micronaut.http.client.filter.ClientFilterResolutionContext;
import io.micronaut.http.client.netty.ssl.ClientSslBuilder;
import io.micronaut.http.client.sse.SseClient;
import io.micronaut.http.client.sse.SseClientRegistry;
import io.micronaut.http.codec.MediaTypeCodec;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.filter.HttpClientFilterResolver;
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer;
import io.micronaut.http.netty.channel.ChannelPipelineListener;
import io.micronaut.http.netty.channel.DefaultEventLoopGroupConfiguration;
import io.micronaut.http.netty.channel.EventLoopGroupConfiguration;
import io.micronaut.http.netty.channel.EventLoopGroupFactory;
import io.micronaut.http.netty.channel.EventLoopGroupRegistry;
import io.micronaut.http.netty.channel.NettyChannelType;
import io.micronaut.http.ssl.SslConfiguration;
import io.micronaut.inject.InjectionPoint;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.json.JsonFeatures;
import io.micronaut.json.JsonMapper;
import io.micronaut.json.body.CustomizableJsonHandler;
import io.micronaut.json.codec.MapperMediaTypeCodec;
import io.micronaut.runtime.context.scope.refresh.RefreshEvent;
import io.micronaut.runtime.context.scope.refresh.RefreshEventListener;
import io.micronaut.websocket.WebSocketClient;
import io.micronaut.websocket.WebSocketClientRegistry;
import io.micronaut.websocket.context.WebSocketBeanRegistry;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.resolver.AddressResolverGroup;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ThreadFactory;

/**
 * Factory for the default implementation of the {@link HttpClient} interface based on Netty.
 *
 * @author Graeme Rocher
 * @since 2.0
 */
@Factory
@BootstrapContextCompatible
@Internal
public
class DefaultNettyHttpClientRegistry implements AutoCloseable,
        HttpClientRegistry,
        SseClientRegistry,
        StreamingHttpClientRegistry,
        WebSocketClientRegistry,
        ProxyHttpClientRegistry,
        ChannelPipelineCustomizer,
        NettyClientCustomizer.Registry,
        RefreshEventListener {
    private static final Logger LOG = LoggerFactory.getLogger(DefaultNettyHttpClientRegistry.class);
    private final Map unbalancedClients = new ConcurrentHashMap<>(10);
    private final List balancedClients = Collections.synchronizedList(new ArrayList<>());
    private final LoadBalancerResolver loadBalancerResolver;
    private final ClientSslBuilder nettyClientSslBuilder;
    private final ThreadFactory threadFactory;
    private final MediaTypeCodecRegistry codecRegistry;
    private final MessageBodyHandlerRegistry handlerRegistry;
    private final BeanContext beanContext;
    private final HttpClientConfiguration defaultHttpClientConfiguration;
    private final EventLoopGroupRegistry eventLoopGroupRegistry;
    private final EventLoopGroupFactory eventLoopGroupFactory;
    private final HttpClientFilterResolver clientFilterResolver;
    private final JsonMapper jsonMapper;
    private final Collection pipelineListeners = new CopyOnWriteArrayList<>();
    private final CompositeNettyClientCustomizer clientCustomizer = new CompositeNettyClientCustomizer();

    /**
     * Default constructor.
     *
     * @param defaultHttpClientConfiguration  The default HTTP client configuration
     * @param httpClientFilterResolver        The HTTP client filter resolver
     * @param loadBalancerResolver            The load balancer resolver
     * @param nettyClientSslBuilder           The client SSL builder
     * @param threadFactory                   The thread factory
     * @param codecRegistry                   The codec registry
     * @param handlerRegistry                 The handler registry
     * @param eventLoopGroupRegistry          The event loop group registry
     * @param eventLoopGroupFactory           The event loop group factory
     * @param beanContext                     The bean context
     * @param jsonMapper                      JSON Mapper
     */
    public DefaultNettyHttpClientRegistry(
            HttpClientConfiguration defaultHttpClientConfiguration,
            HttpClientFilterResolver httpClientFilterResolver,
            LoadBalancerResolver loadBalancerResolver,
            ClientSslBuilder nettyClientSslBuilder,
            ThreadFactory threadFactory,
            MediaTypeCodecRegistry codecRegistry,
            MessageBodyHandlerRegistry handlerRegistry,
            EventLoopGroupRegistry eventLoopGroupRegistry,
            EventLoopGroupFactory eventLoopGroupFactory,
            BeanContext beanContext,
            JsonMapper jsonMapper) {
        this.clientFilterResolver = httpClientFilterResolver;
        this.defaultHttpClientConfiguration = defaultHttpClientConfiguration;
        this.loadBalancerResolver = loadBalancerResolver;
        this.nettyClientSslBuilder = nettyClientSslBuilder;
        this.threadFactory = threadFactory;
        this.codecRegistry = codecRegistry;
        this.handlerRegistry = handlerRegistry;
        this.beanContext = beanContext;
        this.eventLoopGroupFactory = eventLoopGroupFactory;
        this.eventLoopGroupRegistry = eventLoopGroupRegistry;
        this.jsonMapper = jsonMapper;
    }

    @NonNull
    @Override
    public HttpClient getClient(@NonNull HttpVersionSelection httpVersion, @NonNull String clientId, @Nullable String path) {
        final ClientKey key = new ClientKey(
                httpVersion,
                clientId,
                null,
                path,
                null,
                null
        );
        return getClient(key, beanContext, AnnotationMetadata.EMPTY_METADATA);
    }

    @Override
    @NonNull
    public DefaultHttpClient getClient(@NonNull AnnotationMetadata metadata) {
        final ClientKey key = getClientKey(metadata);
        return getClient(key, beanContext, metadata);
    }

    @Override
    @NonNull
    public DefaultHttpClient getSseClient(@NonNull AnnotationMetadata metadata) {
        return getClient(metadata);
    }

    @Override
    @NonNull
    public DefaultHttpClient getStreamingHttpClient(@NonNull AnnotationMetadata metadata) {
        return getClient(metadata);
    }

    @Override
    @NonNull
    public DefaultHttpClient getProxyHttpClient(@NonNull AnnotationMetadata metadata) {
        return getClient(metadata);
    }

    @Override
    @NonNull
    public DefaultHttpClient getWebSocketClient(@NonNull AnnotationMetadata metadata) {
        return getClient(metadata);
    }

    @Override
    @PreDestroy
    public void close() {
        for (HttpClient httpClient : unbalancedClients.values()) {
            try {
                httpClient.close();
            } catch (Throwable e) {
                if (LOG.isWarnEnabled()) {
                    LOG.warn("Error shutting down HTTP client: {}", e.getMessage(), e);
                }
            }
        }
        unbalancedClients.clear();
    }

    @Override
    public void disposeClient(AnnotationMetadata annotationMetadata) {
        final ClientKey key = getClientKey(annotationMetadata);
        final StreamingHttpClient streamingHttpClient = unbalancedClients.get(key);
        if (streamingHttpClient != null && streamingHttpClient.isRunning()) {
            streamingHttpClient.close();
            unbalancedClients.remove(key);
        }
    }

    /**
     * Creates a new {@link HttpClient} for the given injection point.
     *
     * @param injectionPoint The injection point
     * @param loadBalancer   The load balancer to use (Optional)
     * @param configuration  The configuration (Optional)
     * @param beanContext    The bean context to use
     * @return The client
     */
    @Bean
    @BootstrapContextCompatible
    @Primary
    protected DefaultHttpClient httpClient(
            @Nullable InjectionPoint injectionPoint,
            @Parameter @Nullable LoadBalancer loadBalancer,
            @Parameter @Nullable HttpClientConfiguration configuration,
            BeanContext beanContext) {
        return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext);
    }

    @Override
    @NonNull
    public HttpClient resolveClient(@Nullable InjectionPoint  injectionPoint,
                                    @Nullable LoadBalancer loadBalancer,
                                    @Nullable HttpClientConfiguration configuration,
                                    @NonNull BeanContext beanContext) {
        return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext);
    }

    @Override
    @NonNull
    public ProxyHttpClient resolveProxyHttpClient(@Nullable InjectionPoint  injectionPoint,
                                                  @Nullable LoadBalancer loadBalancer,
                                                  @Nullable HttpClientConfiguration configuration,
                                                  @NonNull BeanContext beanContext) {
        return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext);
    }

    @Override
    @NonNull
    public SseClient resolveSseClient(@Nullable InjectionPoint  injectionPoint,
                                      @Nullable LoadBalancer loadBalancer,
                                      @Nullable HttpClientConfiguration configuration,
                                      @NonNull BeanContext beanContext) {
        return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext);
    }

    @Override
    @NonNull
    public StreamingHttpClient resolveStreamingHttpClient(@Nullable InjectionPoint  injectionPoint,
                                                          @Nullable LoadBalancer loadBalancer,
                                                          @Nullable HttpClientConfiguration configuration,
                                                          @NonNull BeanContext beanContext) {
        return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext);
    }

    @Override
    @NonNull
    public WebSocketClient resolveWebSocketClient(@Nullable InjectionPoint injectionPoint,
                                                  @Nullable LoadBalancer loadBalancer,
                                                  @Nullable HttpClientConfiguration configuration,
                                                  @NonNull BeanContext beanContext) {
        return resolveDefaultHttpClient(injectionPoint, loadBalancer, configuration, beanContext);
    }

    @Override
    public boolean isClientChannel() {
        return true;
    }

    @Override
    public void doOnConnect(@NonNull ChannelPipelineListener listener) {
        Objects.requireNonNull(listener, "listener");
        pipelineListeners.add(listener);
    }

    @Override
    public void register(@NonNull NettyClientCustomizer customizer) {
        Objects.requireNonNull(customizer, "customizer");
        clientCustomizer.add(customizer);
    }

    private DefaultHttpClient getClient(ClientKey key, BeanContext beanContext, AnnotationMetadata annotationMetadata) {
        return unbalancedClients.computeIfAbsent(key, clientKey -> {
            DefaultHttpClient clientBean = null;
            final String clientId = clientKey.clientId;
            final Class configurationClass = clientKey.configurationClass;

            if (clientId != null) {
                clientBean = (DefaultHttpClient) this.beanContext
                        .findBean(HttpClient.class, Qualifiers.byName(clientId)).orElse(null);
            }

            if (configurationClass != null && !HttpClientConfiguration.class.isAssignableFrom(configurationClass)) {
                throw new IllegalStateException("Referenced HTTP client configuration class must be an instance of HttpClientConfiguration for injection point: " + configurationClass);
            }

            final List filterAnnotations = clientKey.filterAnnotations;
            final String path = clientKey.path;
            if (clientBean != null && path == null && configurationClass == null && filterAnnotations.isEmpty()) {
                return clientBean;
            }

            LoadBalancer loadBalancer = null;
            final HttpClientConfiguration configuration;
            if (configurationClass != null) {
                configuration = (HttpClientConfiguration) this.beanContext.getBean(configurationClass);
            } else if (clientId != null) {
                configuration = this.beanContext.findBean(
                        HttpClientConfiguration.class,
                        Qualifiers.byName(clientId)
                ).orElse(defaultHttpClientConfiguration);
            } else {
                configuration = defaultHttpClientConfiguration;
            }

            if (clientId != null) {

                loadBalancer = loadBalancerResolver.resolve(clientId)
                        .orElseThrow(() ->
                                new HttpClientException("Invalid service reference [" + clientId + "] specified to @Client"));
            }

            String contextPath = null;
            if (StringUtils.isNotEmpty(path)) {
                contextPath = path;
            } else if (StringUtils.isNotEmpty(clientId) && clientId.startsWith("/")) {
                contextPath = clientId;
            } else {
                if (loadBalancer != null) {
                    contextPath = loadBalancer.getContextPath().orElse(null);
                }
            }


            final DefaultHttpClient client = buildClient(
                    loadBalancer,
                    clientKey.httpVersion,
                    configuration,
                    clientId,
                    contextPath,
                    beanContext,
                    annotationMetadata
            );
            final JsonFeatures jsonFeatures = clientKey.jsonFeatures;
            if (jsonFeatures != null) {
                List codecs = new ArrayList<>(2);
                MediaTypeCodecRegistry codecRegistry = client.getMediaTypeCodecRegistry();
                for (MediaTypeCodec codec : codecRegistry.getCodecs()) {
                    if (codec instanceof MapperMediaTypeCodec typeCodec) {
                        codecs.add(typeCodec.cloneWithFeatures(jsonFeatures));
                    } else {
                        codecs.add(codec);
                    }
                }
                if (!codecRegistry.findCodec(MediaType.APPLICATION_JSON_TYPE).isPresent()) {
                    codecs.add(createNewJsonCodec(this.beanContext, jsonFeatures));
                }
                client.setMediaTypeCodecRegistry(MediaTypeCodecRegistry.of(codecs));

                client.setHandlerRegistry(new MessageBodyHandlerRegistry() {
                    final MessageBodyHandlerRegistry delegate = client.getHandlerRegistry();

                    @SuppressWarnings("unchecked")
                    private  T customize(T handler) {
                        if (handler instanceof CustomizableJsonHandler cnjh) {
                            return (T) cnjh.customize(jsonFeatures);
                        }
                        return handler;
                    }

                    @Override
                    public  Optional> findReader(Argument type, List mediaType) {
                        return delegate.findReader(type, mediaType).map(this::customize);
                    }

                    @Override
                    public  Optional> findWriter(Argument type, List mediaType) {
                        return delegate.findWriter(type, mediaType).map(this::customize);
                    }
                });
            }
            return client;
        });
    }

    private DefaultHttpClient buildClient(
            LoadBalancer loadBalancer,
            HttpVersionSelection httpVersion,
            HttpClientConfiguration configuration,
            String clientId,
            String contextPath,
            BeanContext beanContext,
            AnnotationMetadata annotationMetadata) {

        EventLoopGroup eventLoopGroup = resolveEventLoopGroup(configuration, beanContext);
        ConversionService conversionService = beanContext.getBean(ConversionService.class);
        String addressResolverGroupName = configuration.getAddressResolverGroupName();
        AddressResolverGroup resolverGroup = addressResolverGroupName == null ? null : beanContext.getBean(AddressResolverGroup.class, Qualifiers.byName(addressResolverGroupName));
        return new DefaultHttpClient(
                loadBalancer,
                httpVersion,
                configuration,
                contextPath,
                clientFilterResolver,
                clientFilterResolver.resolveFilterEntries(new ClientFilterResolutionContext(
                        clientId == null ? null : Collections.singletonList(clientId),
                        annotationMetadata
                )),
                threadFactory,
                nettyClientSslBuilder,
                codecRegistry,
                handlerRegistry,
                WebSocketBeanRegistry.forClient(beanContext),
                beanContext.findBean(RequestBinderRegistry.class).orElseGet(() ->
                        new DefaultRequestBinderRegistry(conversionService)
                ),
                eventLoopGroup,
                resolveSocketChannelFactory(NettyChannelType.CLIENT_SOCKET, SocketChannel.class, configuration, beanContext),
                resolveSocketChannelFactory(NettyChannelType.DATAGRAM_SOCKET, DatagramChannel.class, configuration, beanContext),
                clientCustomizer,
                clientId,
                conversionService,
                resolverGroup
        );
    }

    private EventLoopGroup resolveEventLoopGroup(HttpClientConfiguration configuration, BeanContext beanContext) {
        final String eventLoopGroupName = configuration.getEventLoopGroup();
        EventLoopGroup eventLoopGroup;
        if (EventLoopGroupConfiguration.DEFAULT.equals(eventLoopGroupName)) {
            eventLoopGroup = eventLoopGroupRegistry.getDefaultEventLoopGroup();
        } else {
            eventLoopGroup = beanContext.findBean(EventLoopGroup.class, Qualifiers.byName(eventLoopGroupName))
                    .orElseThrow(() -> new HttpClientException("Specified event loop group is not defined: " + eventLoopGroupName));
        }
        return eventLoopGroup;
    }

    private DefaultHttpClient resolveDefaultHttpClient(
            @Nullable InjectionPoint injectionPoint,
            @Nullable LoadBalancer loadBalancer,
            @Nullable HttpClientConfiguration configuration,
            @NonNull BeanContext beanContext) {
        if (loadBalancer != null) {
            if (configuration == null) {
                configuration = defaultHttpClientConfiguration;
            }
            DefaultHttpClient c = buildClient(
                loadBalancer,
                null,
                configuration,
                null,
                loadBalancer.getContextPath().orElse(null),
                beanContext,
                AnnotationMetadata.EMPTY_METADATA
            );
            balancedClients.add(c);
            return c;
        } else {
            return getClient(injectionPoint != null ? injectionPoint.getAnnotationMetadata() : AnnotationMetadata.EMPTY_METADATA);
        }
    }

    private  ChannelFactory resolveSocketChannelFactory(NettyChannelType type, Class channelClass, HttpClientConfiguration configuration, BeanContext beanContext) {
        final String eventLoopGroup = configuration.getEventLoopGroup();

        final EventLoopGroupConfiguration eventLoopGroupConfiguration = beanContext.findBean(EventLoopGroupConfiguration.class, Qualifiers.byName(eventLoopGroup))
                .orElseGet(() -> {
                    if (EventLoopGroupConfiguration.DEFAULT.equals(eventLoopGroup)) {
                        return new DefaultEventLoopGroupConfiguration();
                    } else {
                        throw new HttpClientException("Specified event loop group is not defined: " + eventLoopGroup);
                    }
                });

        return () -> channelClass.cast(eventLoopGroupFactory.channelInstance(type, eventLoopGroupConfiguration));
    }

    private ClientKey getClientKey(AnnotationMetadata metadata) {
        HttpVersionSelection httpVersionSelection = HttpVersionSelection.forClientAnnotation(metadata);
        String clientId = metadata.stringValue(Client.class).orElse(null);
        String path = metadata.stringValue(Client.class, "path").orElse(null);
        List filterAnnotation = metadata
                .getAnnotationNamesByStereotype(FilterMatcher.class);
        final Class configurationClass =
                metadata.classValue(Client.class, "configuration").orElse(null);
        JsonFeatures jsonFeatures = jsonMapper.detectFeatures(metadata).orElse(null);

        return new ClientKey(httpVersionSelection, clientId, filterAnnotation, path, configurationClass, jsonFeatures);
    }

    private static MediaTypeCodec createNewJsonCodec(BeanContext beanContext, JsonFeatures jsonFeatures) {
        return getJsonCodec(beanContext).cloneWithFeatures(jsonFeatures);
    }

    private static MapperMediaTypeCodec getJsonCodec(BeanContext beanContext) {
        return beanContext.getBean(MapperMediaTypeCodec.class, Qualifiers.byName(MapperMediaTypeCodec.REGULAR_JSON_MEDIA_TYPE_CODEC_NAME));
    }

    @Override
    public Set getObservedConfigurationPrefixes() {
        return Set.of(DefaultHttpClientConfiguration.PREFIX, ServiceHttpClientConfiguration.PREFIX, SslConfiguration.PREFIX);
    }

    @Override
    public void onApplicationEvent(RefreshEvent event) {
        for (DefaultHttpClient client : unbalancedClients.values()) {
            client.connectionManager.refresh();
        }
        for (DefaultHttpClient client : balancedClients) {
            client.connectionManager.refresh();
        }
    }

    /**
     * Client key.
     */
    @Internal
    private static final class ClientKey {
        final HttpVersionSelection httpVersion;
        final String clientId;
        final List filterAnnotations;
        final String path;
        final Class configurationClass;
        final JsonFeatures jsonFeatures;

        ClientKey(
            HttpVersionSelection httpVersion,
                String clientId,
                List filterAnnotations,
                String path,
                Class configurationClass,
                JsonFeatures jsonFeatures) {
            this.httpVersion = httpVersion;
            this.clientId = clientId;
            this.filterAnnotations = filterAnnotations;
            this.path = path;
            this.configurationClass = configurationClass;
            this.jsonFeatures = jsonFeatures;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ClientKey clientKey = (ClientKey) o;
            return Objects.equals(httpVersion, clientKey.httpVersion) &&
                    Objects.equals(clientId, clientKey.clientId) &&
                    Objects.equals(filterAnnotations, clientKey.filterAnnotations) &&
                    Objects.equals(path, clientKey.path) &&
                    Objects.equals(configurationClass, clientKey.configurationClass) &&
                    Objects.equals(jsonFeatures, clientKey.jsonFeatures);
        }

        @Override
        public int hashCode() {
            return Objects.hash(httpVersion, clientId, filterAnnotations, path, configurationClass, jsonFeatures);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy