diff --git a/.reviewboardrc b/.reviewboardrc
new file mode 100644
index 000000000000..af029b835767
--- /dev/null
+++ b/.reviewboardrc
@@ -0,0 +1,3 @@
+REVIEWBOARD_URL = 'https://rbcommons.com/s/armeria/'
+REPOSITORY = 'armeria'
+
diff --git a/NOTICE.txt b/NOTICE.txt
index b980c5ac51eb..4398caded82a 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -161,6 +161,11 @@ This product depends on Netty, distributed by Netty.io:
* License: licenses/LICENSE.netty.al20.txt (Apache License v2.0)
* Homepage: http://netty.io/
+This product depends on Reactive Streams API, distributed by Reactive-Streams.org:
+
+ * License: licenses/LICENSE.reactivestreams.cc0.txt
+ * Homepage: http://www.reactive-streams.org/
+
This product depends on Reflections, distributed by ronmamo:
* License: licenses/LICENSE.reflections.wtfpl.txt (WTFPL)
diff --git a/licenses/LICENSE.reactivestreams.cc0.txt b/licenses/LICENSE.reactivestreams.cc0.txt
new file mode 100644
index 000000000000..bb5bc373c41b
--- /dev/null
+++ b/licenses/LICENSE.reactivestreams.cc0.txt
@@ -0,0 +1,9 @@
+Licensed under Public Domain (CC0)
+
+To the extent possible under law, the person who associated CC0 with
+this code has waived all copyright and related or neighboring
+rights to this code.
+
+You should have received a copy of the CC0 legalcode along with this
+work. If not, see .
+
diff --git a/pom.xml b/pom.xml
index 0fdb1334a259..3917dfe7724e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,7 @@
1.1.73.1.24.1.0.CR7
+ 1.0.01.7.218.0.332.0.1
@@ -139,6 +140,13 @@
1.1.33.Fork17
+
+
+ org.reactivestreams
+ reactive-streams
+ ${reactive-streams.version}
+
+
org.eclipse.jetty.alpn
@@ -564,7 +572,7 @@
**/TestUtil*random
- ${argLine.alpnAgent} ${argLine.leak}
+ ${argLine.alpnAgent} ${argLine.leak} -Xmx128m
diff --git a/src/main/java/com/linecorp/armeria/client/AbstractClientFactory.java b/src/main/java/com/linecorp/armeria/client/AbstractClientFactory.java
new file mode 100644
index 000000000000..b909b4828f3e
--- /dev/null
+++ b/src/main/java/com/linecorp/armeria/client/AbstractClientFactory.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you 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:
+ *
+ * http://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 com.linecorp.armeria.client;
+
+import static java.util.Objects.requireNonNull;
+
+import java.net.URI;
+import java.util.Optional;
+import java.util.Set;
+
+import com.linecorp.armeria.common.Scheme;
+
+public abstract class AbstractClientFactory implements ClientFactory {
+
+ protected AbstractClientFactory() {}
+
+ @Override
+ public final T newClient(String uri, Class clientType, ClientOptionValue>... options) {
+ requireNonNull(uri, "uri");
+ requireNonNull(options, "options");
+ return newClient(URI.create(uri), clientType, ClientOptions.of(options));
+ }
+
+ @Override
+ public final T newClient(String uri, Class clientType, ClientOptions options) {
+ requireNonNull(uri, "uri");
+ return newClient(URI.create(uri), clientType, options);
+ }
+
+ @Override
+ public final T newClient(URI uri, Class clientType, ClientOptionValue>... options) {
+ requireNonNull(options, "options");
+ return newClient(uri, clientType, ClientOptions.of(options));
+ }
+
+ protected final Scheme validate(URI uri, Class> clientType, ClientOptions options) {
+ requireNonNull(uri, "uri");
+ requireNonNull(clientType, "clientType");
+ requireNonNull(options, "options");
+
+ final String scheme = uri.getScheme();
+ if (scheme == null) {
+ throw new IllegalArgumentException("URI with missing scheme: " + uri);
+ }
+
+ if (uri.getAuthority() == null) {
+ throw new IllegalArgumentException("URI with missing authority: " + uri);
+ }
+
+ final Optional parsedSchemeOpt = Scheme.tryParse(scheme);
+ if (!parsedSchemeOpt.isPresent()) {
+ throw new IllegalArgumentException("URI with unknown scheme: " + uri);
+ }
+
+ final Scheme parsedScheme = parsedSchemeOpt.get();
+ final Set supportedSchemes = supportedSchemes();
+ if (!supportedSchemes.contains(parsedScheme)) {
+ throw new IllegalArgumentException(
+ "URI with unsupported scheme: " + uri + " (expected: " + supportedSchemes + ')');
+ }
+
+ return parsedScheme;
+ }
+
+ protected static Endpoint newEndpoint(URI uri) {
+ return Endpoint.parse(uri.getAuthority());
+ }
+}
diff --git a/src/main/java/com/linecorp/armeria/client/AllInOneClientFactory.java b/src/main/java/com/linecorp/armeria/client/AllInOneClientFactory.java
new file mode 100644
index 000000000000..2b040c48bce4
--- /dev/null
+++ b/src/main/java/com/linecorp/armeria/client/AllInOneClientFactory.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you 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:
+ *
+ * http://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 com.linecorp.armeria.client;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableMap;
+
+import com.linecorp.armeria.client.http.HttpClientFactory;
+import com.linecorp.armeria.client.thrift.ThriftClientFactory;
+import com.linecorp.armeria.common.Scheme;
+
+import io.netty.channel.EventLoop;
+import io.netty.channel.EventLoopGroup;
+
+public class AllInOneClientFactory extends AbstractClientFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(AllInOneClientFactory.class);
+
+ static {
+ if (AllInOneClientFactory.class.getClassLoader() == ClassLoader.getSystemClassLoader()) {
+ Runtime.getRuntime().addShutdownHook(new Thread(ClientFactory::closeDefault));
+ }
+ }
+
+ private final ClientFactory mainFactory;
+ private final Map clientFactories;
+
+ public AllInOneClientFactory() {
+ this(SessionOptions.DEFAULT);
+ }
+
+ public AllInOneClientFactory(SessionOptions options) {
+ // TODO(trustin): Allow specifying different options for different session protocols.
+ // We have only one session protocol at the moment, so this is OK so far.
+ final HttpClientFactory httpClientFactory = new HttpClientFactory(options);
+ final ThriftClientFactory thriftClientFactory = new ThriftClientFactory(httpClientFactory);
+
+ final ImmutableMap.Builder builder = ImmutableMap.builder();
+ for (ClientFactory f : Arrays.asList(httpClientFactory, thriftClientFactory)) {
+ f.supportedSchemes().forEach(s -> builder.put(s, f));
+ }
+
+ clientFactories = builder.build();
+ mainFactory = httpClientFactory;
+ }
+
+ @Override
+ public Set supportedSchemes() {
+ return clientFactories.keySet();
+ }
+
+ @Override
+ public SessionOptions options() {
+ return mainFactory.options();
+ }
+
+ @Override
+ public EventLoopGroup eventLoopGroup() {
+ return mainFactory.eventLoopGroup();
+ }
+
+ @Override
+ public Supplier eventLoopSupplier() {
+ return mainFactory.eventLoopSupplier();
+ }
+
+ @Override
+ public T newClient(URI uri, Class clientType, ClientOptions options) {
+ final Scheme scheme = validate(uri, clientType, options);
+ return clientFactories.get(scheme).newClient(uri, clientType, options);
+ }
+
+ @Override
+ public void close() {
+ // The global default should never be closed.
+ if (this == ClientFactory.DEFAULT) {
+ logger.debug("Refusing to close the default {}; must be closed via closeDefault()",
+ ClientFactory.class.getSimpleName());
+ return;
+ }
+
+ doClose();
+ }
+
+ void doClose() {
+ clientFactories.values().forEach(ClientFactory::close);
+ }
+}
diff --git a/src/main/java/com/linecorp/armeria/client/Client.java b/src/main/java/com/linecorp/armeria/client/Client.java
index d7fdc9bf11ae..47e52b728685 100644
--- a/src/main/java/com/linecorp/armeria/client/Client.java
+++ b/src/main/java/com/linecorp/armeria/client/Client.java
@@ -16,76 +16,7 @@
package com.linecorp.armeria.client;
-import java.util.function.Function;
-
-/**
- * A set of components required for invoking a remote service.
- */
-public interface Client {
-
- /**
- * Creates a new function that decorates a {@link Client} with the specified {@code codecDecorator}
- * and {@code invokerDecorator}.
- *
- * This factory method is useful when you want to write a reusable factory method that returns
- * a decorator function and the returned decorator function is expected to be consumed by
- * {@link #decorate(Function)}. For example, this may be a factory which combines multiple decorators,
- * any of which could be decorating a codec and/or a handler.
- *
- * Consider using {@link #decorateCodec(Function)} or {@link #decorateInvoker(Function)}
- * instead for simplicity unless you are writing a factory method of a decorator function.
- *
- * If you need a function that decorates only a codec or an invoker, use {@link Function#identity()} for
- * the non-decorated property. e.g:
- *
- */
- @SuppressWarnings("unchecked")
- static
- Function newDecorator(Function codecDecorator, Function invokerDecorator) {
- return client -> new DecoratingClient(client, codecDecorator, invokerDecorator);
- }
-
- /**
- * Returns the {@link ClientCodec}.
- */
- ClientCodec codec();
-
- /**
- * Returns the {@link RemoteInvoker}.
- */
- RemoteInvoker invoker();
-
- /**
- * Creates a new {@link Client} that decorates this {@link Client} with the specified {@code decorator}.
- */
- default Client decorate(Function decorator) {
- @SuppressWarnings("unchecked")
- final Client newClient = decorator.apply(this);
- if (newClient == null) {
- throw new NullPointerException("decorator.apply() returned null: " + decorator);
- }
-
- return newClient;
- }
-
- /**
- * Creates a new {@link Client} that decorates the {@link ClientCodec} of this {@link Client} with the
- * specified {@code codecDecorator}.
- */
- default
- Client decorateCodec(Function codecDecorator) {
- return new DecoratingClient(this, codecDecorator, Function.identity());
- }
-
- /**
- * Creates a new {@link Client} that decorates the {@link RemoteInvoker} of this
- * {@link Client} with the specified {@code invokerDecorator}.
- */
- default
- Client decorateInvoker(Function invokerDecorator) {
- return new DecoratingClient(this, Function.identity(), invokerDecorator);
- }
+@FunctionalInterface
+public interface Client {
+ O execute(ClientRequestContext ctx, I req) throws Exception;
}
diff --git a/src/main/java/com/linecorp/armeria/client/ClientBuilder.java b/src/main/java/com/linecorp/armeria/client/ClientBuilder.java
index 3dfba2c1c1e8..f1e1a60b1ce8 100644
--- a/src/main/java/com/linecorp/armeria/client/ClientBuilder.java
+++ b/src/main/java/com/linecorp/armeria/client/ClientBuilder.java
@@ -17,27 +17,12 @@
import static java.util.Objects.requireNonNull;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Proxy;
import java.net.URI;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
-import com.linecorp.armeria.client.routing.EndpointGroup;
-import com.linecorp.armeria.client.routing.EndpointGroupInvoker;
-import com.linecorp.armeria.client.routing.EndpointGroupUtil;
-import org.apache.thrift.protocol.TProtocolFactory;
-
-import com.linecorp.armeria.client.http.SimpleHttpClientCodec;
-import com.linecorp.armeria.client.thrift.ThriftClientCodec;
-import com.linecorp.armeria.common.Scheme;
-import com.linecorp.armeria.common.SerializationFormat;
-import com.linecorp.armeria.common.SessionProtocol;
-import com.linecorp.armeria.common.TimeoutPolicy;
-import com.linecorp.armeria.common.thrift.ThriftProtocolFactories;
-
/**
* Creates a new client that connects to the specified {@link URI} using the builder pattern. Use the factory
* methods in {@link Clients} if you do not have many options to override.
@@ -47,8 +32,8 @@ public final class ClientBuilder {
private final URI uri;
private final Map, ClientOptionValue>> options = new LinkedHashMap<>();
- private RemoteInvokerFactory remoteInvokerFactory = RemoteInvokerFactory.DEFAULT;
- private Function decorator;
+ private ClientFactory factory = ClientFactory.DEFAULT;
+ private ClientDecorationBuilder decoration;
/**
* Creates a new {@link ClientBuilder} that builds the client that connects to the specified {@code uri}.
@@ -65,10 +50,10 @@ public ClientBuilder(URI uri) {
}
/**
- * Sets the {@link RemoteInvokerFactory} of the client. The default is {@link RemoteInvokerFactory#DEFAULT}.
+ * Sets the {@link ClientFactory} of the client. The default is {@link ClientFactory#DEFAULT}.
*/
- public ClientBuilder remoteInvokerFactory(RemoteInvokerFactory remoteInvokerFactory) {
- this.remoteInvokerFactory = requireNonNull(remoteInvokerFactory, "remoteInvokerFactory");
+ public ClientBuilder factory(ClientFactory factory) {
+ this.factory = requireNonNull(factory, "factory");
return this;
}
@@ -98,9 +83,9 @@ public ClientBuilder options(ClientOptionValue>... options) {
throw new NullPointerException("options[" + i + ']');
}
- if (o.option() == ClientOption.DECORATOR && decorator != null) {
+ if (o.option() == ClientOption.DECORATION && decoration != null) {
throw new IllegalArgumentException(
- "options[" + i + "]: option(" + ClientOption.DECORATOR +
+ "options[" + i + "]: option(" + ClientOption.DECORATION +
") and decorator() are mutually exclusive.");
}
@@ -120,171 +105,79 @@ public ClientBuilder option(ClientOption option, T value) {
private void validateOption(ClientOption> option) {
requireNonNull(option, "option");
- if (option == ClientOption.DECORATOR && decorator != null) {
+ if (option == ClientOption.DECORATION && decoration != null) {
throw new IllegalArgumentException(
- "option(" + ClientOption.DECORATOR + ") and decorator() are mutually exclusive.");
+ "option(" + ClientOption.DECORATION + ") and decorator() are mutually exclusive.");
}
}
/**
- * Sets the timeout of a socket write attempt in milliseconds.
+ * Sets the default timeout of a socket write attempt in milliseconds.
*
- * @param writeTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout.
+ * @param defaultWriteTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout.
*/
- public ClientBuilder writeTimeoutMillis(long writeTimeoutMillis) {
- return writeTimeout(Duration.ofMillis(writeTimeoutMillis));
+ public ClientBuilder defaultWriteTimeoutMillis(long defaultWriteTimeoutMillis) {
+ return option(ClientOption.DEFAULT_WRITE_TIMEOUT_MILLIS, defaultWriteTimeoutMillis);
}
/**
- * Sets the timeout of a socket write attempt.
+ * Sets the default timeout of a socket write attempt.
*
- * @param writeTimeout the timeout. {@code 0} disables the timeout.
- */
- public ClientBuilder writeTimeout(Duration writeTimeout) {
- return writeTimeout(TimeoutPolicy.ofFixed(requireNonNull(writeTimeout, "writeTimeout")));
- }
-
- /**
- * Sets the {@link TimeoutPolicy} of a socket write attempt.
+ * @param defaultWriteTimeout the timeout. {@code 0} disables the timeout.
*/
- public ClientBuilder writeTimeout(TimeoutPolicy writeTimeoutPolicy) {
- return option(ClientOption.WRITE_TIMEOUT_POLICY,
- requireNonNull(writeTimeoutPolicy, "writeTimeoutPolicy"));
+ public ClientBuilder defaultWriteTimeout(Duration defaultWriteTimeout) {
+ return defaultWriteTimeoutMillis(requireNonNull(defaultWriteTimeout, "defaultWriteTimeout").toMillis());
}
/**
- * Sets the timeout of a response in milliseconds.
+ * Sets the default timeout of a response in milliseconds.
*
- * @param responseTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout.
+ * @param defaultResponseTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout.
*/
- public ClientBuilder responseTimeoutMillis(long responseTimeoutMillis) {
- return responseTimeout(Duration.ofMillis(responseTimeoutMillis));
+ public ClientBuilder defaultResponseTimeoutMillis(long defaultResponseTimeoutMillis) {
+ return option(ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS, defaultResponseTimeoutMillis);
}
/**
- * Sets the timeout of a response.
+ * Sets the default timeout of a response.
*
- * @param responseTimeout the timeout. {@code 0} disables the timeout.
- */
- public ClientBuilder responseTimeout(Duration responseTimeout) {
- return responseTimeout(TimeoutPolicy.ofFixed(requireNonNull(responseTimeout, "responseTimeout")));
- }
-
- /**
- * Sets the {@link TimeoutPolicy} of a response.
+ * @param defaultResponseTimeout the timeout. {@code 0} disables the timeout.
*/
- public ClientBuilder responseTimeout(TimeoutPolicy responseTimeoutPolicy) {
- return option(ClientOption.RESPONSE_TIMEOUT_POLICY,
- requireNonNull(responseTimeoutPolicy, "responseTimeoutPolicy"));
+ public ClientBuilder defaultResponseTimeout(Duration defaultResponseTimeout) {
+ return defaultResponseTimeoutMillis(
+ requireNonNull(defaultResponseTimeout, "defaultResponseTimeout").toMillis());
}
/**
* Adds the specified {@code decorator}.
*/
- public ClientBuilder decorator(Function decorator) {
- requireNonNull(decorator, "decorator");
+ public ClientBuilder decorator(Class requestType, Class responseType,
+ Function extends Client, ?>, ? extends Client> decorator) {
- if (options.containsKey(ClientOption.DECORATOR)) {
+ if (options.containsKey(ClientOption.DECORATION)) {
throw new IllegalArgumentException(
- "decorator() and option(" + ClientOption.DECORATOR + ") are mutually exclusive.");
+ "decorator() and option(" + ClientOption.DECORATION + ") are mutually exclusive.");
}
- if (this.decorator == null) {
- this.decorator = decorator;
- } else {
- this.decorator = this.decorator.andThen(decorator);
+ if (decoration == null) {
+ decoration = new ClientDecorationBuilder();
}
+ decoration.add(requestType, responseType, decorator);
return this;
}
/**
- * Adds the specified {@code invokerDecorator} that decorates a {@link RemoteInvoker}.
- */
- public ClientBuilder invokerDecorator(
- Function extends RemoteInvoker, ? extends RemoteInvoker> invokerDecorator) {
- requireNonNull(invokerDecorator, "invokerDecorator");
-
- @SuppressWarnings("unchecked")
- final Function castInvokerDecorator =
- (Function) invokerDecorator;
-
- return decorator(d -> d.decorateInvoker(castInvokerDecorator));
- }
-
- /**
- * Adds the specified {@code codecDecorator} that decorates an {@link ClientCodec}.
- */
- public ClientBuilder codecDecorator(
- Function extends ClientCodec, ? extends ClientCodec> codecDecorator) {
- requireNonNull(codecDecorator, "codecDecorator");
-
- @SuppressWarnings("unchecked")
- final Function castCodecDecorator =
- (Function) codecDecorator;
-
- return decorator(d -> d.decorateCodec(castCodecDecorator));
- }
-
- /**
- * Creates a new client which implements the specified {@code interfaceClass}.
+ * Creates a new client which implements the specified {@code clientType}.
*/
@SuppressWarnings("unchecked")
- public T build(Class interfaceClass) {
- requireNonNull(interfaceClass, "interfaceClass");
-
- if (decorator != null) {
- options.put(ClientOption.DECORATOR, ClientOption.DECORATOR.newValue(decorator));
- }
-
- final ClientOptions options = ClientOptions.of(this.options.values());
-
- final Client client = options.decorator().apply(newClient(interfaceClass));
-
- final InvocationHandler handler = new ClientInvocationHandler(
- remoteInvokerFactory.eventLoopGroup(), uri, interfaceClass, client, options);
-
- return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
- new Class[] { interfaceClass },
- handler);
- }
-
- private Client newClient(Class> interfaceClass) {
- final Scheme scheme = Scheme.parse(uri.getScheme());
- final SessionProtocol sessionProtocol = scheme.sessionProtocol();
- URI codecURI = this.uri;
-
- RemoteInvoker remoteInvoker = remoteInvokerFactory.getInvoker(sessionProtocol);
- if (remoteInvoker == null) {
- throw new IllegalArgumentException("unsupported scheme: " + scheme);
- }
-
- final String groupName = EndpointGroupUtil.getEndpointGroupName(this.uri);
- if (groupName != null) {
- remoteInvoker = new EndpointGroupInvoker(remoteInvoker, groupName);
- // Ensure a valid URL is returned in business logic that accesses it
- // through the ServiceInvocationContext by stripping out our custom markers.
- codecURI = URI.create(EndpointGroupUtil.removeGroupMark(this.uri));
- }
-
- final ClientCodec codec = createCodec(codecURI, scheme, interfaceClass);
-
- return new SimpleClient(codec, remoteInvoker);
- }
-
- private static ClientCodec createCodec(URI uri, Scheme scheme, Class> interfaceClass) {
- SessionProtocol sessionProtocol = scheme.sessionProtocol();
- SerializationFormat serializationFormat = scheme.serializationFormat();
- if (SerializationFormat.ofThrift().contains(serializationFormat)) {
- TProtocolFactory protocolFactory = ThriftProtocolFactories.get(serializationFormat);
- return new ThriftClientCodec(uri, interfaceClass, protocolFactory);
- }
+ public T build(Class clientType) {
+ requireNonNull(clientType, "clientType");
- if (SessionProtocol.ofHttp().contains(sessionProtocol) &&
- serializationFormat == SerializationFormat.NONE) {
- return new SimpleHttpClientCodec(uri.getHost());
+ if (decoration != null) {
+ options.put(ClientOption.DECORATION, ClientOption.DECORATION.newValue(decoration.build()));
}
- throw new IllegalArgumentException("unsupported scheme:" + scheme);
+ return factory.newClient(uri, clientType, ClientOptions.of(options.values()));
}
}
diff --git a/src/main/java/com/linecorp/armeria/client/ClientCodec.java b/src/main/java/com/linecorp/armeria/client/ClientCodec.java
deleted file mode 100644
index 34c34ba9807a..000000000000
--- a/src/main/java/com/linecorp/armeria/client/ClientCodec.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2015 LINE Corporation
- *
- * LINE Corporation licenses this file to you 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:
- *
- * http://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 com.linecorp.armeria.client;
-
-import java.lang.reflect.Method;
-import java.util.Optional;
-
-import com.linecorp.armeria.common.Scheme;
-import com.linecorp.armeria.common.ServiceInvocationContext;
-import com.linecorp.armeria.common.SessionProtocol;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.Channel;
-import io.netty.util.concurrent.Promise;
-
-/**
- * Encodes an invocation into a {@link ByteBuf} and decodes its result ({@link ByteBuf}) into a Java object.
- */
-public interface ClientCodec {
-
- /**
- * Invoked when a {@link RemoteInvoker} prepares to perform an invocation.
- */
- void prepareRequest(Method method, Object[] args, Promise resultPromise);
-
- /**
- * Encodes a Java method invocation into a {@link ServiceInvocationContext}.
- */
- EncodeResult encodeRequest(Channel channel, SessionProtocol sessionProtocol, Method method, Object[] args);
-
- /**
- * Decodes the response bytes into a Java object.
- */
- T decodeResponse(ServiceInvocationContext ctx, ByteBuf content, Object originalResponse)
- throws Exception;
-
- /**
- * Returns {@code true} if the invocation result that will be returned to calling code asynchronously.
- */
- boolean isAsyncClient();
-
- /**
- * The result of {@link #encodeRequest(Channel, SessionProtocol, Method, Object[]) ClientCodec.encodeRequest()}.
- */
- interface EncodeResult {
- /**
- * Returns {@code true} if and only if the encode request has been successful.
- */
- boolean isSuccess();
-
- /**
- * Returns the {@link ServiceInvocationContext} created as the result of
- * {@link ClientCodec#decodeResponse(ServiceInvocationContext, ByteBuf, Object)
- * ClientCodec.decodeResponse()}
- *
- * @throws IllegalStateException if the encoding was not successful
- */
- ServiceInvocationContext invocationContext();
-
- /**
- * Returns the content resulting from encoding the request.
- *
- * @throws IllegalStateException if the encoding was not successful
- */
- Object content();
-
- /**
- * Returns the cause of the encode failure.
- *
- * @throws IllegalStateException if the encoding is successful and thus there is no error to report
- */
- Throwable cause();
-
- /**
- * Returns the host name of the invocation.
- */
- Optional encodedHost();
-
- /**
- * Returns the path of the invocation.
- */
- Optional encodedPath();
-
- /**
- * Returns the {@link Scheme} of the invocation.
- */
- Optional encodedScheme();
- }
-}
diff --git a/src/main/java/com/linecorp/armeria/client/ClientDecoration.java b/src/main/java/com/linecorp/armeria/client/ClientDecoration.java
new file mode 100644
index 000000000000..2617f7a61377
--- /dev/null
+++ b/src/main/java/com/linecorp/armeria/client/ClientDecoration.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you 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:
+ *
+ * http://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 com.linecorp.armeria.client;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import com.linecorp.armeria.client.ClientDecoration.Entry;
+
+public final class ClientDecoration implements Iterable {
+
+ public static final ClientDecoration NONE = new ClientDecoration(Collections.emptyList());
+
+ public static ClientDecoration of(
+ Class requestType, Class responseType,
+ Function extends Client, ?>, ? extends Client> decorator) {
+ return new ClientDecorationBuilder().add(requestType, responseType, decorator).build();
+ }
+
+ private final List entries;
+
+ ClientDecoration(List entries) {
+ this.entries = Collections.unmodifiableList(entries);
+ }
+
+ @Override
+ public Iterator iterator() {
+ return entries.iterator();
+ }
+
+ public Client decorate(Class requestType, Class responseType, Client client) {
+ for (Entry e : this) {
+ if (!requestType.isAssignableFrom(e.requestType()) ||
+ !responseType.isAssignableFrom(e.responseType())) {
+ continue;
+ }
+
+ final Function, Client> decorator = e.decorator();
+ client = decorator.apply(client);
+ }
+
+ return client;
+ }
+
+ public static final class Entry {
+ private final Class> requestType;
+ private final Class> responseType;
+ private final Function, ? extends Client, ?>> decorator;
+
+ Entry(Class> requestType, Class> responseType, Function, ? extends Client, ?>> decorator) {
+ this.requestType = requestType;
+ this.responseType = responseType;
+ this.decorator = decorator;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Class requestType() {
+ return (Class) requestType;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Class responseType() {
+ return (Class) responseType;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public Function, Client> decorator() {
+ return (Function, Client>) (Function) decorator;
+ }
+
+ @Override
+ public String toString() {
+ return '(' +
+ requestType.getSimpleName() + ", " +
+ responseType.getSimpleName() + ", " +
+ decorator +
+ ')';
+ }
+ }
+}
diff --git a/src/main/java/com/linecorp/armeria/client/ClientDecorationBuilder.java b/src/main/java/com/linecorp/armeria/client/ClientDecorationBuilder.java
new file mode 100644
index 000000000000..f1e9a643c45d
--- /dev/null
+++ b/src/main/java/com/linecorp/armeria/client/ClientDecorationBuilder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you 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:
+ *
+ * http://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 com.linecorp.armeria.client;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import com.linecorp.armeria.client.ClientDecoration.Entry;
+
+public final class ClientDecorationBuilder {
+
+ private final List entries = new ArrayList<>();
+
+ public ClientDecorationBuilder add(
+ Class requestType, Class responseType,
+ Function extends Client, ?>, ? extends Client> decorator) {
+
+ requireNonNull(requestType, "requestType");
+ requireNonNull(responseType, "responseType");
+ requireNonNull(decorator, "decorator");
+
+ entries.add(new Entry(requestType, responseType, decorator));
+ return this;
+ }
+
+ public ClientDecoration build() {
+ return new ClientDecoration(entries);
+ }
+}
diff --git a/src/main/java/com/linecorp/armeria/client/ClientFactory.java b/src/main/java/com/linecorp/armeria/client/ClientFactory.java
new file mode 100644
index 000000000000..6a6879801331
--- /dev/null
+++ b/src/main/java/com/linecorp/armeria/client/ClientFactory.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2016 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you 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:
+ *
+ * http://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 com.linecorp.armeria.client;
+
+import java.net.URI;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.slf4j.LoggerFactory;
+
+import com.linecorp.armeria.common.Scheme;
+
+import io.netty.channel.EventLoop;
+import io.netty.channel.EventLoopGroup;
+
+/**
+ * Creates and manages clients.
+ *
+ *
Life cycle of the default {@link ClientFactory}
+ *
+ * {@link Clients} or {@link ClientBuilder} uses {@link #DEFAULT}, the default {@link ClientFactory},
+ * unless you specified a {@link ClientFactory} explicitly. Calling {@link #close()} on the default
+ * {@link ClientFactory} won't terminate its I/O threads and release other related resources unlike
+ * other {@link ClientFactory} to protect itself from accidental premature termination.
+ *
+ * Instead, when the current {@link ClassLoader} is {@linkplain ClassLoader#getSystemClassLoader() the system
+ * class loader}, a {@link Runtime#addShutdownHook(Thread) shutdown hook} is registered so that they are
+ * released when the JVM exits.
+ *
+ * If you are in an environment managed by a container or you desire the early termination of the default
+ * {@link ClientFactory}, use {@link #closeDefault()}.
+ *
+ */
+public interface ClientFactory extends AutoCloseable {
+
+ /**
+ * The default {@link ClientFactory} implementation.
+ */
+ ClientFactory DEFAULT = new AllInOneClientFactory();
+
+ /**
+ * Closes the default {@link ClientFactory}.
+ */
+ static void closeDefault() {
+ LoggerFactory.getLogger(ClientFactory.class).debug(
+ "Closing the default {}", ClientFactory.class.getSimpleName());
+ ((AllInOneClientFactory) DEFAULT).doClose();
+ }
+
+ Set supportedSchemes();
+
+ SessionOptions options();
+
+ /**
+ * Returns the {@link EventLoopGroup} being used by this {@link ClientFactory}. Can be used to, e.g.,
+ * schedule a periodic task without creating a separate event loop.
+ */
+ EventLoopGroup eventLoopGroup();
+
+ Supplier eventLoopSupplier();
+
+ /**
+ * Creates a new client that connects to the specified {@code uri}.
+ *
+ * @param uri the URI of the server endpoint
+ * @param clientType the type of the new client
+ * @param options the {@link ClientOptionValue}s
+ */
+ T newClient(String uri, Class clientType, ClientOptionValue>... options);
+
+ /**
+ * Creates a new client that connects to the specified {@code uri}.
+ *
+ * @param uri the URI of the server endpoint
+ * @param clientType the type of the new client
+ * @param options the {@link ClientOptions}
+ */
+ T newClient(String uri, Class clientType, ClientOptions options);
+
+ /**
+ * Creates a new client that connects to the specified {@link URI} using the default
+ * {@link ClientFactory}.
+ *
+ * @param uri the URI of the server endpoint
+ * @param clientType the type of the new client
+ * @param options the {@link ClientOptionValue}s
+ */
+ T newClient(URI uri, Class clientType, ClientOptionValue>... options);
+
+ /**
+ * Creates a new client that connects to the specified {@link URI} using the default
+ * {@link ClientFactory}.
+ *
+ * @param uri the URI of the server endpoint
+ * @param clientType the type of the new client
+ * @param options the {@link ClientOptions}
+ */
+ T newClient(URI uri, Class clientType, ClientOptions options);
+
+ /**
+ * Closes all clients managed by this factory and shuts down the {@link EventLoopGroup}
+ * created implicitly by this factory.
+ */
+ @Override
+ void close();
+}
diff --git a/src/main/java/com/linecorp/armeria/client/ClientInvocationHandler.java b/src/main/java/com/linecorp/armeria/client/ClientInvocationHandler.java
deleted file mode 100644
index a3943301410a..000000000000
--- a/src/main/java/com/linecorp/armeria/client/ClientInvocationHandler.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2015 LINE Corporation
- *
- * LINE Corporation licenses this file to you 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:
- *
- * http://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 com.linecorp.armeria.client;
-
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
-import java.lang.reflect.UndeclaredThrowableException;
-import java.net.URI;
-import java.nio.channels.ClosedChannelException;
-
-import com.linecorp.armeria.common.ServiceInvocationContext;
-
-import io.netty.channel.EventLoop;
-import io.netty.channel.EventLoopGroup;
-import io.netty.util.concurrent.Future;
-
-final class ClientInvocationHandler implements InvocationHandler {
-
- private static final Object[] NO_ARGS = new Object[0];
-
- private final EventLoopGroup eventLoopGroup;
- private final URI uri;
- private final Class> interfaceClass;
- private final Client client;
- private final ClientOptions options;
-
- ClientInvocationHandler(EventLoopGroup eventLoopGroup, URI uri,
- Class> interfaceClass, Client client, ClientOptions options) {
-
- this.eventLoopGroup = eventLoopGroup;
- this.uri = uri;
- this.interfaceClass = interfaceClass;
- this.client = client;
- this.options = options;
- }
-
- EventLoopGroup eventLoopGroup() {
- return eventLoopGroup;
- }
-
- URI uri() {
- return uri;
- }
-
- Class> interfaceClass() {
- return interfaceClass;
- }
-
- Client client() {
- return client;
- }
-
- private RemoteInvoker invoker() {
- return client.invoker();
- }
-
- private ClientCodec codec() {
- return client.codec();
- }
-
- ClientOptions options() {
- return options;
- }
-
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- final Class> declaringClass = method.getDeclaringClass();
- if (declaringClass == Object.class) {
- // Handle the methods in Object
- return invokeObjectMethod(proxy, method, args);
- }
-
- assert declaringClass == interfaceClass;
- // Handle the methods in the interface.
- return invokeClientMethod(method, args);
- }
-
- private Object invokeObjectMethod(Object proxy, Method method, Object[] args) {
- final String methodName = method.getName();
-
- switch (methodName) {
- case "toString":
- return interfaceClass.getSimpleName() + '(' + uri + ')';
- case "hashCode":
- return System.identityHashCode(proxy);
- case "equals":
- return proxy == args[0];
- default:
- throw new Error("unknown method: " + methodName);
- }
- }
-
- private Object invokeClientMethod(Method method, Object[] args) throws Throwable {
- if (args == null) {
- args = NO_ARGS;
- }
-
- try {
- Future