@ -22,217 +22,568 @@ import java.util.ArrayList;
@@ -22,217 +22,568 @@ import java.util.ArrayList;
import java.util.Arrays ;
import java.util.List ;
import java.util.Set ;
import java.util.function.Consumer ;
import java.util.function.Predicate ;
import java.util.function.Supplier ;
import java.util.stream.Collectors ;
import org.aopalliance.intercept.MethodInterceptor ;
import org.apache.commons.logging.Log ;
import org.apache.commons.logging.LogFactory ;
import org.springframework.aop.framework.ProxyFactory ;
import org.springframework.aop.target.EmptyTargetSource ;
import org.springframework.cglib.core.SpringNamingPolicy ;
import org.springframework.cglib.proxy.Callback ;
import org.springframework.cglib.proxy.Enhancer ;
import org.springframework.cglib.proxy.Factory ;
import org.springframework.cglib.proxy.MethodProxy ;
import org.springframework.core.MethodIntrospector ;
import org.springframework.core.MethodParameter ;
import org.springframework.core.ResolvableType ;
import org.springframework.core.annotation.AnnotatedElementUtils ;
import org.springframework.core.annotation.AnnotationUtils ;
import org.springframework.objenesis.ObjenesisException ;
import org.springframework.objenesis.SpringObjenesis ;
import org.springframework.util.Assert ;
import org.springframework.util.ObjectUtils ;
import org.springframework.util.ReflectionUtils ;
import org.springframework.web.reactive.result.method.InvocableHandlerMethod ;
/ * *
* Convenience class for use in tests to resolve a { @link Method } and / or any of
* its { @link MethodParameter } s based on some hints .
* Convenience class to resolve a method and its parameters based on hints .
*
* < h1 > Background < / h1 >
*
* < p > When testing annotated methods we create test classes such as
* "TestController" with a diverse range of method signatures representing
* supported annotations and argument types . It becomes challenging to use
* naming strategies to keep track of methods and arguments especially in
* combination variables for reflection metadata .
*
* < p > The idea with { @link ResolvableMethod } is NOT to rely on naming techniques
* but to use hints to zero in on method parameters . Especially in combination
* with { @link ResolvableType } such hints can be strongly typed and make tests
* more readable by being explicit about what is being tested and more robust
* since the provided hints have to match .
*
* < p > Common use cases :
*
* < p > In tests we often create a class ( e . g . TestController ) with diverse method
* signatures and annotations to test with . Use of descriptive method and argument
* names combined with using reflection , it becomes challenging to read and write
* tests and it becomes necessary to navigate to the actual method declaration
* which is cumbersome and involves several steps .
* < h2 > 1 . Declared Return Type < / h2 >
*
* < p > The idea here is to provide enough hints to resolving a method uniquely
* where the hints document exactly what is being tested and there is usually no
* need to navigate to the actual method declaration . For example if testing
* response handling , the return type may be used as a hint :
* When testing return types it ' s common to have many methods with a unique
* return type , possibly with or without an annotation .
*
* < pre >
* ResolvableMethod resolvableMethod = ResolvableMethod . onClass ( TestController . class ) ;
* ResolvableType type = ResolvableType . forClassWithGenerics ( Mono . class , View . class ) ;
* Method method = resolvableMethod . returning ( type ) . resolve ( ) ;
*
* type = ResolvableType . forClassWithGenerics ( Mono . class , String . class ) ;
* method = resolvableMethod . returning ( type ) . resolve ( ) ;
* import static org.springframework.web.reactive.result.ResolvableMethod.on ;
*
* // Return type
* on ( TestController . class ) . resolveReturnType ( Foo . class ) ;
*
* // Annotation + return type
* on ( TestController . class ) . annotated ( ResponseBody . class ) . resolveReturnType ( Bar . class ) ;
*
* // Annotation not present
* on ( TestController . class ) . isNotAnnotated ( ResponseBody . class ) . resolveReturnType ( ) ;
*
* // Annotation properties
* on ( TestController . class )
* . annotated ( RequestMapping . class , patterns ( "/foo" ) , params ( "p" ) )
* . annotated ( ResponseBody . class )
* . resolveReturnType ( ) ;
* < / pre >
*
* < h2 > 2 . Method Arguments < / h2 >
*
* When testing method arguments it ' s more likely to have one or a small number
* of methods with a wide array of argument types and parameter annotations .
*
* < pre >
*
* // ...
* ResolvableMethod testMethod = ResolvableMethod . on ( getClass ( ) ) . named ( "handle" ) . build ( ) ;
*
* testMethod . arg ( Foo . class ) ;
* testMethod . annotated ( RequestBody . class ) ) . arg ( Bar . class ) ;
* testMethod . annotated ( RequestBody . class ) , required ( ) ) . arg ( Bar . class ) ;
* testMethod . notAnnotated ( RequestBody . class ) ) . arg ( Bar . class ) ;
* < / pre >
*
* < p > Additional { @code resolve } methods provide options to obtain one of the method
* arguments or return type as a { @link MethodParameter } .
* < h3 > 3 . Mock Handler Method Invocation < / h3 >
*
* Locate a method by invoking it through a proxy of the target handler :
*
* < pre >
*
* ResolvableMethod . on ( TestController . class ) . mockCall ( o - > o . handle ( null ) ) . method ( ) ;
* < / pre >
*
* @author Rossen Stoyanchev
* /
public class ResolvableMethod {
private final Class < ? > objectClass ;
private final Object object ;
private String methodName ;
private static final Log logger = LogFactory . getLog ( ResolvableMethod . class ) ;
private Class < ? > [ ] argumentTypes ;
private static final SpringObjenesis objenesis = new SpringObjenesis ( ) ;
private ResolvableType returnType ;
private final List < Class < ? extends Annotation > > annotationTypes = new ArrayList < > ( 4 ) ;
private final Method method ;
private final List < Predicate < Method > > predicates = new ArrayList < > ( 4 ) ;
private ResolvableMethod ( Method method ) {
Assert . notNull ( method , "method is required" ) ;
this . method = method ;
}
private ResolvableMethod ( Class < ? > objectClass ) {
Assert . notNull ( objectClass , "Class must not be null" ) ;
this . objectClass = objectClass ;
this . object = null ;
/ * *
* Return the resolved method .
* /
public Method method ( ) {
return this . method ;
}
private ResolvableMethod ( Object object ) {
Assert . notNull ( object , "Object must not be null" ) ;
this . object = object ;
this . objectClass = object . getClass ( ) ;
/ * *
* Return the declared return type of the resolved method .
* /
public MethodParameter returnType ( ) {
return new MethodParameter ( this . method , - 1 ) ;
}
/ * *
* Find a unique argument matching the given type .
* @param type the expected type
* /
public MethodParameter arg ( Class < ? > type ) {
return new ArgResolver ( ) . arg ( type ) ;
}
/ * *
* Methods that match the given name ( regardless of arguments ) .
* Find a unique argument matching the given type .
* @param type the expected type
* /
public ResolvableMethod name ( String methodName ) {
this . methodName = methodName ;
return this ;
public MethodParameter arg ( ResolvableType type ) {
return new ArgResolver ( ) . arg ( type ) ;
}
/ * *
* Methods that match the given argument types .
* Filter on method arguments that have the given annotation .
* @param annotationType the annotation type
* @param filter optional filters on the annotation
* /
public ResolvableMethod argumentTypes ( Class < ? > . . . argumentTypes ) {
this . argumentTypes = argumentTypes ;
return this ;
@SafeVarargs
public final < A extends Annotation > ArgResolver annotated ( Class < A > annotationType , Predicate < A > . . . filter ) {
return new ArgResolver ( ) . annotated ( annotationType , filter ) ;
}
/ * *
* Methods declared to return the given type .
* Filter on method arguments that don ' t have the given annotation type ( s ) .
* @param annotationTypes the annotation types
* /
public ResolvableMethod returning ( ResolvableType resolvableType ) {
this . returnType = resolvableType ;
return this ;
@SafeVarargs
public final ArgResolver notAnnotated ( Class < ? extends Annotation > . . . annotationTypes ) {
return new ArgResolver ( ) . notAnnotated ( annotationTypes ) ;
}
/ * *
* Methods with the given annotation .
* Filter on method arguments using customer predicates .
* /
public ResolvableMethod annotated ( Class < ? extends Annotation > annotationType ) {
this . annotationTypes . add ( annotationType ) ;
return this ;
@SafeVarargs
public final ArgResolver filtered ( Predicate < MethodParameter > . . . filter ) {
return new ArgResolver ( ) . filtered ( filter ) ;
}
@Override
public String toString ( ) {
return "ResolvableMethod=" + formatMethod ( ) ;
}
private String formatMethod ( ) {
return this . method ( ) . getName ( ) +
Arrays . stream ( this . method . getParameters ( ) )
. map ( p - > {
Annotation [ ] annots = p . getAnnotations ( ) ;
return ( annots . length ! = 0 ? Arrays . toString ( annots ) : "" ) + " " + p ;
} )
. collect ( Collectors . joining ( ",\n\t" , "(\n\t" , "\n)" ) ) ;
}
/ * *
* Methods matching the given predicate .
* Main entry point providing access to a { @code ResolvableMethod } builder .
* /
public final ResolvableMethod matching ( Predicate < Method > methodPredicate ) {
this . predicates . add ( methodPredicate ) ;
return this ;
}
// Resolve methods
public Method resolve ( ) {
Set < Method > methods = MethodIntrospector . selectMethods ( this . objectClass ,
( ReflectionUtils . MethodFilter ) method - > {
if ( this . methodName ! = null & & ! this . methodName . equals ( method . getName ( ) ) ) {
return false ;
}
if ( getReturnType ( ) ! = null ) {
// String comparison (ResolvableType's with different providers)
String actual = ResolvableType . forMethodReturnType ( method ) . toString ( ) ;
if ( ! actual . equals ( getReturnType ( ) ) & & ! Object . class . equals ( method . getDeclaringClass ( ) ) ) {
return false ;
}
}
else if ( ! ObjectUtils . isEmpty ( this . argumentTypes ) ) {
if ( ! Arrays . equals ( this . argumentTypes , method . getParameterTypes ( ) ) ) {
return false ;
}
}
else if ( this . annotationTypes . stream ( )
. filter ( annotType - > AnnotationUtils . findAnnotation ( method , annotType ) = = null )
. findFirst ( )
. isPresent ( ) ) {
return false ;
}
else if ( this . predicates . stream ( ) . filter ( p - > ! p . test ( method ) ) . findFirst ( ) . isPresent ( ) ) {
return false ;
}
return true ;
} ) ;
Assert . state ( ! methods . isEmpty ( ) , ( ) - > "No matching method: " + this ) ;
Assert . state ( methods . size ( ) = = 1 , ( ) - > "Multiple matching methods: " + this ) ;
return methods . iterator ( ) . next ( ) ;
}
private String getReturnType ( ) {
return ( this . returnType ! = null ? this . returnType . toString ( ) : null ) ;
}
public InvocableHandlerMethod resolveHandlerMethod ( ) {
Assert . state ( this . object ! = null , "Object must not be null" ) ;
return new InvocableHandlerMethod ( this . object , resolve ( ) ) ;
}
public MethodParameter resolveReturnType ( ) {
Method method = resolve ( ) ;
return new MethodParameter ( method , - 1 ) ;
public static < T > Builder < T > on ( Class < T > objectClass ) {
return new Builder < > ( objectClass ) ;
}
@SafeVarargs
public final MethodParameter resolveParam ( Predicate < MethodParameter > . . . predicates ) {
return resolveParam ( null , predicates ) ;
/ * *
* Builder for { @code ResolvableMethod } .
* /
public static class Builder < T > {
private final Class < ? > objectClass ;
private final List < Predicate < Method > > filters = new ArrayList < > ( 4 ) ;
private Builder ( Class < ? > objectClass ) {
Assert . notNull ( objectClass , "Class must not be null" ) ;
this . objectClass = objectClass ;
}
private void addFilter ( String message , Predicate < Method > filter ) {
this . filters . add ( new LabeledPredicate < > ( message , filter ) ) ;
}
/ * *
* Filter on methods with the given name .
* /
public Builder named ( String methodName ) {
addFilter ( "methodName=" + methodName , m - > m . getName ( ) . equals ( methodName ) ) ;
return this ;
}
/ * *
* Filter on methods with the given annotation type .
* @param annotationType the expected annotation type
* @param filter optional filters on the actual annotation
* /
@SafeVarargs
public final < A extends Annotation > Builder annotated ( Class < A > annotationType , Predicate < A > . . . filter ) {
String message = "annotated=" + annotationType . getName ( ) ;
addFilter ( message , m - > {
A annot = AnnotatedElementUtils . findMergedAnnotation ( m , annotationType ) ;
return ( annot ! = null & & Arrays . stream ( filter ) . allMatch ( f - > f . test ( annot ) ) ) ;
} ) ;
return this ;
}
/ * *
* Filter on methods not annotated with the given annotation type .
* /
public final < A extends Annotation > Builder isNotAnnotated ( Class < A > annotationType ) {
String message = "notAnnotated=" + annotationType . getName ( ) ;
addFilter ( message , m - > AnnotationUtils . findAnnotation ( m , annotationType ) = = null ) ;
return this ;
}
/ * *
* Filter on methods returning the given type .
* /
public Builder returning ( Class < ? > returnType ) {
return returning ( ResolvableType . forClass ( returnType ) ) ;
}
/ * *
* Filter on methods returning the given type .
* /
public Builder returning ( ResolvableType resolvableType ) {
String expected = resolvableType . toString ( ) ;
String message = "returnType=" + expected ;
addFilter ( message , m - > expected . equals ( ResolvableType . forMethodReturnType ( m ) . toString ( ) ) ) ;
return this ;
}
/ * *
* Add custom filters for matching methods .
* /
@SafeVarargs
public final Builder filtered ( Predicate < Method > . . . filters ) {
this . filters . addAll ( Arrays . asList ( filters ) ) ;
return this ;
}
/ * *
* Build a { @code ResolvableMethod } from the provided filters which must
* resolve to a unique , single method .
*
* < p > See additional resolveXxx shortcut methods going directly to
* { @link Method } or return type parameter .
*
* @throws IllegalStateException for no match or multiple matches
* /
public ResolvableMethod build ( ) {
Set < Method > methods = MethodIntrospector . selectMethods ( this . objectClass , this : : isMatch ) ;
Assert . state ( ! methods . isEmpty ( ) , "No matching method: " + this ) ;
Assert . state ( methods . size ( ) = = 1 , "Multiple matching methods: " + this + formatMethods ( methods ) ) ;
return new ResolvableMethod ( methods . iterator ( ) . next ( ) ) ;
}
private boolean isMatch ( Method method ) {
return this . filters . stream ( ) . allMatch ( p - > p . test ( method ) ) ;
}
private String formatMethods ( Set < Method > methods ) {
return "\nMatched:\n" + methods . stream ( )
. map ( Method : : toGenericString ) . collect ( Collectors . joining ( ",\n\t" , "[\n\t" , "\n]" ) ) ;
}
public ResolvableMethod mockCall ( Consumer < T > invoker ) {
MethodInvocationInterceptor interceptor = new MethodInvocationInterceptor ( ) ;
T proxy = initProxy ( this . objectClass , interceptor ) ;
invoker . accept ( proxy ) ;
Method method = interceptor . getInvokedMethod ( ) ;
return new ResolvableMethod ( method ) ;
}
// Build & Resolve shortcuts...
/ * *
* Resolve and return the { @code Method } equivalent to :
* < p > { @code build ( ) . method ( ) }
* /
public final Method resolveMethod ( ) {
return build ( ) . method ( ) ;
}
/ * *
* Resolve and return the { @code Method } equivalent to :
* < p > { @code named ( methodName ) . build ( ) . method ( ) }
* /
public Method resolveMethod ( String methodName ) {
return named ( methodName ) . build ( ) . method ( ) ;
}
/ * *
* Resolve and return the declared return type equivalent to :
* < p > { @code build ( ) . returnType ( ) }
* /
public final MethodParameter resolveReturnType ( ) {
return build ( ) . returnType ( ) ;
}
/ * *
* Shortcut to the unique return type equivalent to :
* < p > { @code returning ( returnType ) . build ( ) . returnType ( ) }
* /
public MethodParameter resolveReturnType ( Class < ? > returnType ) {
return returning ( returnType ) . build ( ) . returnType ( ) ;
}
/ * *
* Shortcut to the unique return type equivalent to :
* < p > { @code returning ( returnType ) . build ( ) . returnType ( ) }
* /
public MethodParameter resolveReturnType ( ResolvableType returnType ) {
return returning ( returnType ) . build ( ) . returnType ( ) ;
}
@Override
public String toString ( ) {
return "ResolvableMethod.Builder[\n" +
"\tobjectClass = " + this . objectClass . getName ( ) + ",\n" +
"\tfilters = " + formatFilters ( ) + "\n]" ;
}
private String formatFilters ( ) {
return this . filters . stream ( ) . map ( Object : : toString )
. collect ( Collectors . joining ( ",\n\t\t" , "[\n\t\t" , "\n\t]" ) ) ;
}
}
@SafeVarargs
public final MethodParameter resolveParam ( ResolvableType type , Predicate < MethodParameter > . . . predicates ) {
List < MethodParameter > matches = new ArrayList < > ( ) ;
Method method = resolve ( ) ;
for ( int i = 0 ; i < method . getParameterCount ( ) ; i + + ) {
MethodParameter param = new MethodParameter ( method , i ) ;
if ( type ! = null ) {
if ( ! ResolvableType . forMethodParameter ( param ) . toString ( ) . equals ( type . toString ( ) ) ) {
continue ;
@SuppressWarnings ( "unchecked" )
private static < T > T initProxy ( Class < ? > type , MethodInvocationInterceptor interceptor ) {
Assert . notNull ( type , "'type' must not be null" ) ;
if ( type . isInterface ( ) ) {
ProxyFactory factory = new ProxyFactory ( EmptyTargetSource . INSTANCE ) ;
factory . addInterface ( type ) ;
factory . addInterface ( Supplier . class ) ;
factory . addAdvice ( interceptor ) ;
return ( T ) factory . getProxy ( ) ;
}
else {
Enhancer enhancer = new Enhancer ( ) ;
enhancer . setSuperclass ( type ) ;
enhancer . setInterfaces ( new Class < ? > [ ] { Supplier . class } ) ;
enhancer . setNamingPolicy ( SpringNamingPolicy . INSTANCE ) ;
enhancer . setCallbackType ( org . springframework . cglib . proxy . MethodInterceptor . class ) ;
Class < ? > proxyClass = enhancer . createClass ( ) ;
Object proxy = null ;
if ( objenesis . isWorthTrying ( ) ) {
try {
proxy = objenesis . newInstance ( proxyClass , enhancer . getUseCache ( ) ) ;
}
catch ( ObjenesisException ex ) {
logger . debug ( "Objenesis failed, falling back to default constructor" , ex ) ;
}
}
if ( ! ObjectUtils . isEmpty ( predicates ) ) {
if ( Arrays . stream ( predicates ) . filter ( p - > ! p . test ( param ) ) . findFirst ( ) . isPresent ( ) ) {
continue ;
if ( proxy = = null ) {
try {
proxy = ReflectionUtils . accessibleConstructor ( proxyClass ) . newInstance ( ) ;
}
catch ( Throwable ex ) {
throw new IllegalStateException ( "Unable to instantiate proxy " +
"via both Objenesis and default constructor fails as well" , ex ) ;
}
}
matches . add ( param ) ;
}
Assert . state ( ! matches . isEmpty ( ) , ( ) - > "No matching arg on " + method . toString ( ) ) ;
Assert . state ( matches . size ( ) = = 1 , ( ) - > "Multiple matching args: " + matches + " on " + method . toString ( ) ) ;
return matches . get ( 0 ) ;
( ( Factory ) proxy ) . setCallbacks ( new Callback [ ] { interceptor } ) ;
return ( T ) proxy ;
}
}
@Override
public String toString ( ) {
return "Class=" + this . objectClass +
", name=" + ( this . methodName ! = null ? this . methodName : "<not specified>" ) +
", returnType=" + ( this . returnType ! = null ? this . returnType : "<not specified>" ) +
", annotations=" + this . annotationTypes ;
/ * *
* Predicate with a descriptive label .
* /
private static class LabeledPredicate < T > implements Predicate < T > {
private final String label ;
private final Predicate < T > delegate ;
private LabeledPredicate ( String label , Predicate < T > delegate ) {
this . label = label ;
this . delegate = delegate ;
}
@Override
public boolean test ( T method ) {
return this . delegate . test ( method ) ;
}
@Override
public Predicate < T > and ( Predicate < ? super T > other ) {
return this . delegate . and ( other ) ;
}
@Override
public Predicate < T > negate ( ) {
return this . delegate . negate ( ) ;
}
@Override
public Predicate < T > or ( Predicate < ? super T > other ) {
return this . delegate . or ( other ) ;
}
@Override
public String toString ( ) {
return this . label ;
}
}
/ * *
* Resolver for method arguments .
* /
public class ArgResolver {
private final List < Predicate < MethodParameter > > filters = new ArrayList < > ( 4 ) ;
@SafeVarargs
private ArgResolver ( Predicate < MethodParameter > . . . filter ) {
this . filters . addAll ( Arrays . asList ( filter ) ) ;
}
/ * *
* Filter on method arguments that have the given annotation .
* @param annotationType the annotation type
* @param filter optional filters on the annotation
* /
@SafeVarargs
public final < A extends Annotation > ArgResolver annotated ( Class < A > annotationType , Predicate < A > . . . filter ) {
this . filters . add ( param - > {
A annot = param . getParameterAnnotation ( annotationType ) ;
return ( annot ! = null & & Arrays . stream ( filter ) . allMatch ( f - > f . test ( annot ) ) ) ;
} ) ;
return this ;
}
/ * *
* Filter on method arguments that don ' t have the given annotations .
* @param annotationTypes the annotation types
* /
@SafeVarargs
public final ArgResolver notAnnotated ( Class < ? extends Annotation > . . . annotationTypes ) {
this . filters . add ( p - > Arrays . stream ( annotationTypes ) . noneMatch ( p : : hasParameterAnnotation ) ) ;
return this ;
}
/ * *
* Filter on method arguments using customer predicates .
* /
@SafeVarargs
public final ArgResolver filtered ( Predicate < MethodParameter > . . . filter ) {
this . filters . addAll ( Arrays . asList ( filter ) ) ;
return this ;
}
/ * *
* Resolve the argument also matching to the given type .
* @param type the expected type
* /
public MethodParameter arg ( Class < ? > type ) {
this . filters . add ( p - > type . equals ( p . getParameterType ( ) ) ) ;
return arg ( ResolvableType . forClass ( type ) ) ;
}
/ * *
* Resolve the argument also matching to the given type .
* @param type the expected type
* /
public MethodParameter arg ( ResolvableType type ) {
this . filters . add ( p - > type . toString ( ) . equals ( ResolvableType . forMethodParameter ( p ) . toString ( ) ) ) ;
return arg ( ) ;
}
/ * *
* Resolve the argument .
* /
public final MethodParameter arg ( ) {
List < MethodParameter > matches = applyFilters ( ) ;
Assert . state ( ! matches . isEmpty ( ) , ( ) - > "No matching arg in method\n" + formatMethod ( ) ) ;
Assert . state ( matches . size ( ) = = 1 , ( ) - > "Multiple matching args in method\n" + formatMethod ( ) ) ;
return matches . get ( 0 ) ;
}
public static ResolvableMethod onClass ( Class < ? > clazz ) {
return new ResolvableMethod ( clazz ) ;
private List < MethodParameter > applyFilters ( ) {
List < MethodParameter > matches = new ArrayList < > ( ) ;
for ( int i = 0 ; i < method . getParameterCount ( ) ; i + + ) {
MethodParameter param = new MethodParameter ( method , i ) ;
if ( this . filters . stream ( ) . allMatch ( p - > p . test ( param ) ) ) {
matches . add ( param ) ;
}
}
return matches ;
}
}
public static ResolvableMethod on ( Object object ) {
return new ResolvableMethod ( object ) ;
private static class MethodInvocationInterceptor
implements org . springframework . cglib . proxy . MethodInterceptor , MethodInterceptor {
private Method invokedMethod ;
Method getInvokedMethod ( ) {
return this . invokedMethod ;
}
@Override
public Object intercept ( Object object , Method method , Object [ ] args , MethodProxy proxy ) {
if ( ReflectionUtils . isObjectMethod ( method ) ) {
return ReflectionUtils . invokeMethod ( method , object , args ) ;
}
else {
this . invokedMethod = method ;
return null ;
}
}
@Override
public Object invoke ( org . aopalliance . intercept . MethodInvocation inv ) throws Throwable {
return intercept ( inv . getThis ( ) , inv . getMethod ( ) , inv . getArguments ( ) , null ) ;
}
}
}