Skip to content

Commit

Permalink
Add support for AOP Alliance method interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
dnault committed Nov 25, 2016
1 parent 0da83f5 commit 2a95005
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 38 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ gradle-app.setting
!gradle-wrapper.jar

# IntelliJ IDEA
.idea/
classes/
*.iml
*.ipr
*.iws
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
compile 'com.google.code.findbugs:jsr305:3.0.0'
compile 'com.google.guava:guava:19.0'
compile 'org.apache.commons:commons-lang3:3.4'
compile 'aopalliance:aopalliance:1.0'

compile "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
compile "com.fasterxml.jackson.module:jackson-module-parameter-names:${jacksonVersion}"
Expand Down
137 changes: 99 additions & 38 deletions src/main/java/com/github/therapi/core/MethodRegistry.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
package com.github.therapi.core;

import static com.github.therapi.core.internal.JacksonHelper.isLikeNull;
import static com.google.common.base.Throwables.propagate;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.getLevenshteinDistance;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Predicate;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import com.github.therapi.core.interceptor.SimpleMethodInvocation;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static com.github.therapi.core.internal.JacksonHelper.isLikeNull;
import static com.google.common.base.Throwables.propagate;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.getLevenshteinDistance;

public class MethodRegistry {
private static final Logger log = LoggerFactory.getLogger(MethodRegistry.class);

Expand All @@ -38,6 +44,22 @@ public class MethodRegistry {
private final ObjectMapper objectMapper;
private String namespaceSeparator = ".";

private static class InterceptorRegistration {
private final Predicate<MethodDefinition> predicate;
private final MethodInterceptor interceptor;

private InterceptorRegistration(Predicate<MethodDefinition> predicate, MethodInterceptor interceptor) {
this.predicate = predicate;
this.interceptor = interceptor;
}
}

// populated as the user registers interceptors
private final List<InterceptorRegistration> interceptorRegistrations = new ArrayList<>();

// populated on-the-fly based on the values in interceptorRegistrations at the time a method is first invoked
private final ConcurrentMap<MethodDefinition, ImmutableList<MethodInterceptor>> methodDefinitionToInterceptors = new ConcurrentHashMap<>();

private boolean suggestMethods = true;

public boolean isSuggestMethods() {
Expand All @@ -61,6 +83,59 @@ public ObjectMapper getObjectMapper() {
return objectMapper;
}

/**
* Registers the given method interceptor to be applied to all methods
* matching the given predicate. Interceptors will be invoked in the same order
* they are registered.
* <p>
* The effective list of interceptors for a method is determined when the method
* is first invoked; subesequent interceptor registrations will not affect the method.
* <p>
* Here's an example that matches any method and prints how long the invocation takes:
* <pre>
* methodRegistry.intercept(MethodPredicates.any(), invocation -> {
* Stopwatch timer = Stopwatch.createStarted();
* String methodName = MethodDefinitionInvocation.getQualifiedName(invocation);
* try {
* return invocation.proceed();
* } finally {
* System.out.println("Method '" + methodName + "' completed in " + timer);
* }
* });
* </pre>
*
* @see com.github.therapi.core.interceptor.MethodPredicates
*/
public void intercept(Predicate<MethodDefinition> predicate, MethodInterceptor interceptor) {
requireNonNull(predicate);
requireNonNull(interceptor);
interceptorRegistrations.add(new InterceptorRegistration(predicate, interceptor));
}

protected ImmutableList<MethodInterceptor> getInterceptors(MethodDefinition methodDef) {
return computeIfAbsent(methodDefinitionToInterceptors, methodDef, methodDefintion -> {
ImmutableList.Builder<MethodInterceptor> builder = ImmutableList.builder();
interceptorRegistrations.stream()
.filter(registration -> registration.predicate.test(methodDefintion))
.forEach(registration -> builder.add(registration.interceptor));
return builder.build();
});
}

protected MethodInvocation newMethodInvocation(MethodDefinition methodDef, Object[] args, List<MethodInterceptor> interceptors) {
return new SimpleMethodInvocation(methodDef, args, interceptors);
}

/**
* Unlike {@link ConcurrentHashMap#computeIfAbsent(Object, Function)} this method
* is optimized for the case where the entry already exists, and does not always
* use synchronization.
*/
private static <K, V> V computeIfAbsent(ConcurrentMap<K, V> map, K key, Function<? super K, ? extends V> mappingFunction) {
V result = map.get(key);
return result != null ? result : map.computeIfAbsent(key, mappingFunction);
}

public List<String> scan(Object o) {
List<String> methodNames = new ArrayList<>();
for (MethodDefinition methodDef : scanner.findMethods(o)) {
Expand Down Expand Up @@ -100,34 +175,20 @@ public JsonNode invoke(String methodName, JsonNode args) throws MethodNotFoundEx
}

Object[] boundArgs = bindArgs(method, args);
try {
return invoke(method, boundArgs);

} catch (IllegalAccessException e) {
method.getMethod().setAccessible(true);

try {
return invoke(method, boundArgs);

} catch (IOException | IllegalAccessException e2) {
throw propagate(e2);
}

} catch (IOException e) {
throw propagate(e);
}
return invoke(method, boundArgs);
}

private JsonNode invoke(MethodDefinition method, Object[] boundArgs) throws IOException, IllegalAccessException {
private JsonNode invoke(MethodDefinition method, Object[] args) {
try {
Object result = method.getMethod().invoke(method.getOwner(), boundArgs);
MethodInvocation invocation = newMethodInvocation(method, args, getInterceptors(method));
Object result = invocation.proceed();

TokenBuffer buffer = new TokenBuffer(objectMapper, false);
objectMapper.writerFor(method.getReturnTypeRef()).writeValue(buffer, result);
return objectMapper.readTree(buffer.asParser());

} catch (InvocationTargetException e) {
throw propagate(e.getCause());
} catch (Throwable e) {
throw propagate(e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.therapi.core.interceptor;

import com.github.therapi.core.MethodDefinition;
import org.aopalliance.intercept.MethodInvocation;

/**
* A specialized method invocation that provides access to
* the MethodDefinition of the method being invoked.
*/
public interface MethodDefinitionInvocation extends MethodInvocation {
MethodDefinition getMethodDefinition();

/**
* Returns the fully qualified name of the remotable method being invoked.
* Intended for use by MethodInterceptors that want to know the name of the method being intercepted.
*
* @throws ClassCastException if the given invocation does not implement {@code MethodDefinitionInvocation}.
*/
static String getQualifiedName(MethodInvocation invocation) {
return ((MethodDefinitionInvocation) invocation).getMethodDefinition().getQualifiedName(".");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.github.therapi.core.interceptor;

import java.lang.annotation.Annotation;
import java.util.function.Predicate;

import com.github.therapi.core.MethodDefinition;
import org.aopalliance.intercept.MethodInterceptor;

/**
* Factory methods for common method predicates, useful for registering method interceptors.
*
* @see com.github.therapi.core.MethodRegistry#intercept(Predicate, MethodInterceptor)
*/
public class MethodPredicates {
private MethodPredicates() {
}

public static Predicate<MethodDefinition> any() {
return methodDef -> true;
}

public static Predicate<MethodDefinition> methodAnnotatedWith(Class<? extends Annotation> annotationClass) {
return methodDef -> methodDef.getMethod().getAnnotation(annotationClass) != null;
}

public static Predicate<MethodDefinition> qualifiedName(String name) {
return methodDef -> methodDef.getQualifiedName(".").equals(name);
}

public static Predicate<MethodDefinition> namespace(String namespace) {
return methodDef -> methodDef.getNamespace().orElse("").equals(namespace);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.github.therapi.core.interceptor;

import static java.util.Objects.requireNonNull;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;

import com.github.therapi.core.MethodDefinition;
import com.google.common.collect.ImmutableList;
import org.aopalliance.intercept.MethodInterceptor;

/**
* No-frills implementation of the AOP Alliance MethodInvocation interface.
* Supports only static matching. If dynamic argument matching or other advanced
* features are required, consider subclassing Spring's ReflectiveMethodInvocation
* class instead.
*/
public class SimpleMethodInvocation implements MethodDefinitionInvocation {
private final MethodDefinition methodDefinition;
private final Object[] arguments;
private final ImmutableList<MethodInterceptor> interceptors;
private int currentInterceptorIndex = -1;

public SimpleMethodInvocation(MethodDefinition methodDefinition, Object[] args, List<MethodInterceptor> interceptors) {
this.methodDefinition = requireNonNull(methodDefinition);
this.arguments = requireNonNull(args);
this.interceptors = ImmutableList.copyOf(interceptors);
}

@Override
public MethodDefinition getMethodDefinition() {
return methodDefinition;
}

@Override
public Method getMethod() {
return methodDefinition.getMethod();
}

@Override
public Object[] getArguments() {
return arguments;
}

@Override
public Object proceed() throws Throwable {
return currentInterceptorIndex == interceptors.size() - 1
? invokeTargetMethod()
: interceptors.get(++currentInterceptorIndex).invoke(this);
}

protected Object invokeTargetMethod() throws Throwable {
Method method = methodDefinition.getMethod();

try {
return method.invoke(methodDefinition.getOwner(), arguments);

} catch (IllegalAccessException e) {
method.setAccessible(true);
return method.invoke(methodDefinition.getOwner(), arguments);

} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}

@Override
public Object getThis() {
return methodDefinition.getOwner();
}

@Override
public AccessibleObject getStaticPart() {
return methodDefinition.getMethod();
}
}
Loading

0 comments on commit 2a95005

Please sign in to comment.