Skip to content

Commit

Permalink
Merge pull request #182 from jamezp/useInterceptionFactory
Browse files Browse the repository at this point in the history
Attempt to use CDI InterceptionFactory
  • Loading branch information
jamezp authored Jun 20, 2023
2 parents 7dfbc91 + 66abcff commit 15e5fa2
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,12 @@
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.InterceptionType;
import jakarta.enterprise.inject.spi.Interceptor;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ResponseProcessingException;
import jakarta.ws.rs.ext.ParamConverter;
Expand All @@ -59,29 +47,18 @@ public class ProxyInvocationHandler implements InvocationHandler {

private final Set<Object> providerInstances;

private final Map<Method, List<InvocationContextImpl.InterceptorInvocation>> interceptorChains;

private final ResteasyClient client;

private final CreationalContext<?> creationalContext;

private final AtomicBoolean closed;

public ProxyInvocationHandler(final Class<?> restClientInterface,
final Object target,
final Set<Object> providerInstances,
final ResteasyClient client, final BeanManager beanManager) {
final ResteasyClient client) {
this.target = target;
this.providerInstances = providerInstances;
this.client = client;
this.closed = new AtomicBoolean();
if (beanManager != null) {
this.creationalContext = beanManager.createCreationalContext(null);
this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface);
} else {
this.creationalContext = null;
this.interceptorChains = Collections.emptyMap();
}
}

@Override
Expand Down Expand Up @@ -159,40 +136,34 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
args = argsReplacement;
}

List<InvocationContextImpl.InterceptorInvocation> chain = interceptorChains.get(method);
if (chain != null) {
// Invoke business method interceptors
return new InvocationContextImpl(target, method, args, chain).proceed();
} else {
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof CompletionException) {
cause = cause.getCause();
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof CompletionException) {
cause = cause.getCause();
}
if (cause instanceof ExceptionMapping.HandlerException) {
((ExceptionMapping.HandlerException) cause).mapException(method);
// no applicable exception mapper found or applicable mapper returned null
return null;
}
if (cause instanceof ResponseProcessingException) {
ResponseProcessingException rpe = (ResponseProcessingException) cause;
cause = rpe.getCause();
if (cause instanceof RuntimeException) {
throw cause;
}
if (cause instanceof ExceptionMapping.HandlerException) {
((ExceptionMapping.HandlerException) cause).mapException(method);
// no applicable exception mapper found or applicable mapper returned null
return null;
} else {
if (cause instanceof ProcessingException &&
cause.getCause() instanceof ClientHeaderFillingException) {
throw cause.getCause().getCause();
}
if (cause instanceof ResponseProcessingException) {
ResponseProcessingException rpe = (ResponseProcessingException) cause;
cause = rpe.getCause();
if (cause instanceof RuntimeException) {
throw cause;
}
} else {
if (cause instanceof ProcessingException &&
cause.getCause() instanceof ClientHeaderFillingException) {
throw cause.getCause().getCause();
}
if (cause instanceof RuntimeException) {
throw cause;
}
if (cause instanceof RuntimeException) {
throw cause;
}
throw e;
}
throw e;
}
}

Expand All @@ -210,9 +181,6 @@ private Object invokeRestClientProxyMethod(Object proxy, Method method, Object[]

private void close() {
if (closed.compareAndSet(false, true)) {
if (creationalContext != null) {
creationalContext.release();
}
client.close();
}
}
Expand All @@ -227,77 +195,4 @@ private Type[] getGenericTypes(Class<?> aClass) {
}
return genericTypes;
}

private static List<Annotation> getBindings(Annotation[] annotations, BeanManager beanManager) {
if (annotations.length == 0) {
return Collections.emptyList();
}
List<Annotation> bindings = new ArrayList<>();
for (Annotation annotation : annotations) {
if (beanManager.isInterceptorBinding(annotation.annotationType())) {
bindings.add(annotation);
}
}
return bindings;
}

private static BeanManager getBeanManager(Class<?> restClientInterface) {
try {
CDI<Object> current = CDI.current();
return current != null ? current.getBeanManager() : null;
} catch (IllegalStateException e) {
LOGGER.warnf("CDI container is not available - interceptor bindings declared on %s will be ignored",
restClientInterface.getSimpleName());
return null;
}
}

private static Map<Method, List<InvocationContextImpl.InterceptorInvocation>> initInterceptorChains(
BeanManager beanManager, CreationalContext<?> creationalContext, Class<?> restClientInterface) {

Map<Method, List<InvocationContextImpl.InterceptorInvocation>> chains = new HashMap<>();
// Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases
Map<Interceptor<?>, Object> interceptorInstances = new HashMap<>();

List<Annotation> classLevelBindings = getBindings(restClientInterface.getAnnotations(), beanManager);

for (Method method : restClientInterface.getMethods()) {
if (method.isDefault() || Modifier.isStatic(method.getModifiers())) {
continue;
}
List<Annotation> methodLevelBindings = getBindings(method.getAnnotations(), beanManager);

if (!classLevelBindings.isEmpty() || !methodLevelBindings.isEmpty()) {

Annotation[] interceptorBindings = merge(methodLevelBindings, classLevelBindings);

List<Interceptor<?>> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE,
interceptorBindings);
if (!interceptors.isEmpty()) {
List<InvocationContextImpl.InterceptorInvocation> chain = new ArrayList<>();
for (Interceptor<?> interceptor : interceptors) {
chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor,
interceptorInstances.computeIfAbsent(interceptor,
i -> beanManager.getReference(i, i.getBeanClass(), creationalContext))));
}
chains.put(method, chain);
}
}
}
return chains.isEmpty() ? Collections.emptyMap() : chains;
}

private static Annotation[] merge(List<Annotation> methodLevelBindings, List<Annotation> classLevelBindings) {
Set<Class<? extends Annotation>> types = methodLevelBindings.stream()
.map(a -> a.annotationType())
.collect(Collectors.toSet());
List<Annotation> merged = new ArrayList<>(methodLevelBindings);
for (Annotation annotation : classLevelBindings) {
if (!types.contains(annotation.annotationType())) {
merged.add(annotation);
}
}
return merged.toArray(new Annotation[] {});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ public <T> T build(Class<T> aClass, ClientHttpEngine httpEngine)
interfaces[2] = Closeable.class;

T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces,
new ProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client, beanManager));
new ProxyInvocationHandler(aClass, actualClient, getLocalProviderInstances(), client));
ClientHeaderProviders.registerForClass(aClass, proxy, beanManager);
return proxy;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.enterprise.inject.spi.InterceptionFactory;
import jakarta.enterprise.inject.spi.PassivationCapable;
import jakarta.enterprise.util.AnnotationLiteral;

Expand All @@ -65,7 +65,7 @@
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;

public class RestClientDelegateBean implements Bean<Object>, PassivationCapable {
public class RestClientDelegateBean<T> implements Bean<T>, PassivationCapable {
private static final Logger LOGGER = Logger.getLogger(RestClientDelegateBean.class);

public static final String REST_URL_FORMAT = "%s/mp-rest/url";
Expand Down Expand Up @@ -96,7 +96,7 @@ public class RestClientDelegateBean implements Bean<Object>, PassivationCapable

private static final String PROPERTY_PREFIX = "%s/property/";

private final Class<?> proxyType;
private final Class<T> proxyType;

private final Class<? extends Annotation> scope;

Expand All @@ -108,7 +108,7 @@ public class RestClientDelegateBean implements Bean<Object>, PassivationCapable

private final Optional<String> configKey;

RestClientDelegateBean(final Class<?> proxyType, final BeanManager beanManager, final Optional<String> baseUri,
RestClientDelegateBean(final Class<T> proxyType, final BeanManager beanManager, final Optional<String> baseUri,
final Optional<String> configKey) {
this.proxyType = proxyType;
this.beanManager = beanManager;
Expand All @@ -134,7 +134,7 @@ public Set<InjectionPoint> getInjectionPoints() {
}

@Override
public Object create(CreationalContext<Object> creationalContext) {
public T create(CreationalContext<T> creationalContext) {
RestClientBuilder builder;
// This can be removed once the below issue is resolved. However, for now we can handle this safely here.
// See https://github.com/eclipse/microprofile-rest-client/issues/353
Expand All @@ -153,7 +153,14 @@ public Object create(CreationalContext<Object> creationalContext) {
configureSsl(builder);

getConfigProperties().forEach(builder::property);
return builder.build(proxyType);

// We want to use the interception factory to let CDI handle all interception
InterceptionFactory<T> interceptionFactory = beanManager.createInterceptionFactory(creationalContext, proxyType);
// Weld takes the interceptor bindings from the class (proxyType) used to create InterceptionFactory
// NOTE: This is somewhat grey area, it might be safer (but way more complex) to properly look up all bindings and
// register them here via - interceptionFactory.configure().add(SomeBinding.Literal.INSTANCE)
// Finally, create the proxy type and feed it to the interception factory
return interceptionFactory.createInterceptedInstance(builder.build(proxyType));
}

private void configureSsl(RestClientBuilder builder) {
Expand Down Expand Up @@ -318,26 +325,29 @@ private static URI uriFromString(String uriString) {
}

@Override
public void destroy(Object instance, CreationalContext<Object> creationalContext) {
public void destroy(T instance, CreationalContext<T> creationalContext) {
if (instance instanceof AutoCloseable) {
try {
((AutoCloseable) instance).close();
} catch (Exception e) {
LOGGER.debugf(e, "Failed to close client %s", instance);
}
}
// release all possibly created dependent objects of this creational context
// this will most likely be a no-op
creationalContext.release();
}

@Override
public Set<Type> getTypes() {
// only add the interface type
// NOTE: if there is a hierarchy of interfaces, should all be added as bean types?
return Collections.singleton(proxyType);
}

@Override
public Set<Annotation> getQualifiers() {
Set<Annotation> qualifiers = new HashSet<Annotation>();
qualifiers.add(new AnnotationLiteral<Default>() {
});
qualifiers.add(new AnnotationLiteral<Any>() {
});
qualifiers.add(RestClient.LITERAL);
Expand All @@ -351,6 +361,7 @@ public Class<? extends Annotation> getScope() {

@Override
public String getName() {
// NOTE: this is an EL bean name, chances are this could just be null?
return proxyType.getName();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void registerRestClient(@Observes @WithAnnotations(RegisterRestClient.cla
Optional<String> maybeConfigKey = extractConfigKey(annotation);

proxyTypes.add(new RestClientData(javaClass, maybeUri, maybeConfigKey));
type.veto();
// no need to veto() these types because interfaces cannot become beans anyway
} else {
errors.add(new IllegalArgumentException("Rest client needs to be an interface " + javaClass));
}
Expand Down Expand Up @@ -107,12 +107,12 @@ public static void clearBeanManager() {
// nothing to do
}

private static class RestClientData {
private final Class<?> javaClass;
private static class RestClientData<T> {
private final Class<T> javaClass;
private final Optional<String> baseUri;
private final Optional<String> configKey;

private RestClientData(final Class<?> javaClass, final Optional<String> baseUri,
private RestClientData(final Class<T> javaClass, final Optional<String> baseUri,
final Optional<String> configKey) {
this.javaClass = javaClass;
this.baseUri = baseUri;
Expand Down
Loading

0 comments on commit 15e5fa2

Please sign in to comment.